<%= form_with url: bulk_update_orders_path, method: :patch, data: { turbo_confirm: "Update selected orders?" } do %>
<div class="batch-toolbar">
<%= select_tag :status,
options_for_select(Order.statuses.keys.map { |s| [s.humanize, s] }),
prompt: "Set status to\u2026" %>
<%= submit_tag "Apply to selected", class: "btn" %>
</div>
<table class="orders">
<thead>
<tr><th></th><th>Order</th><th>Customer</th><th>Status</th></tr>
</thead>
<tbody>
<% @orders.each do |order| %>
<tr>
<td>
<%= check_box_tag "order_ids[]", order.id, false,
disabled: !order.updatable?, id: dom_id(order, :select) %>
</td>
<td>#<%= order.number %></td>
<td><%= order.customer_name %></td>
<td><span class="badge badge-<%= order.status %>"><%= order.status.humanize %></span></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
class OrdersController < ApplicationController
def index
@orders = Order.order(created_at: :desc).page(params[:page])
end
def bulk_update
ids, status = bulk_update_params
assert_valid_status!(status)
count = Order.bulk_transition_to(status, ids)
if count.positive?
redirect_to orders_path, notice: "Updated #{count} order#{'s' unless count == 1}."
else
redirect_to orders_path, alert: "No eligible orders were updated."
end
rescue ArgumentError => e
redirect_to orders_path, alert: e.message
end
private
def bulk_update_params
ids = Array(params[:order_ids]).map(&:to_i).reject(&:zero?)
[ids, params[:status].to_s]
end
def assert_valid_status!(status)
return if Order.statuses.key?(status)
raise ArgumentError, "#{status.inspect} is not a valid status"
end
end
class Order < ApplicationRecord
enum status: {
pending: 0,
processing: 1,
packed: 2,
shipped: 3,
cancelled: 4
}
scope :updatable, -> { where(status: %i[pending processing packed]) }
def updatable?
self.class.updatable.exists?(id)
end
def self.bulk_transition_to(status, ids)
return 0 if ids.blank?
scope = updatable.where(id: ids)
affected_ids = scope.pluck(:id)
return 0 if affected_ids.empty?
count = scope.update_all(status: statuses.fetch(status), updated_at: Time.current)
OrderTransitionNotificationJob.perform_later(affected_ids, status)
count
end
end
This snippet shows a batch-action pattern in Rails where a user selects several orders via checkboxes and transitions them to a new status in a single request. The point is to do the work as one set-based UPDATE rather than looping and saving each record, which is faster and avoids N callbacks firing per row.
The orders/index view renders a form_with posting to a member-less collection route (bulk_update_orders_path). Each row includes check_box_tag with the shared name order_ids[], so the browser submits an array of selected primary keys. A select_tag lets the operator choose the target status, and the same form is reused for filtering — the important detail is that the checkbox name uses [] so Rails parses params[:order_ids] into an array.
In OrdersController, bulk_update reads and sanitizes those params. bulk_update_params whitelists the incoming array and coerces the ids to integers, while assert_valid_status! guards against arbitrary status strings by checking against Order.statuses, the hash defined by the enum. The scope Order.where(id: ids) is narrowed further by updatable so already-shipped or cancelled orders are excluded. The call to .update_all writes every matching row in one statement and returns the affected count, which is surfaced back to the operator via flash. Because update_all bypasses validations and callbacks, updated_at is set explicitly so timestamps stay honest.
The Order model defines the status enum and an updatable scope listing the states that may still transition. The bulk_transition_to class method centralizes the guard-plus-update_all logic so it is testable in isolation and reusable from a background job. The trade-off of update_all is real: no paper_trail versions, no after_update hooks, no email side effects — so anything requiring per-record logic must be enqueued separately, which bulk_transition_to does by collecting ids before the write. This pattern fits admin dashboards and moderation queues where throughput matters and the transition is a simple column change. A pitfall to watch is that update_all does not run optimistic-locking checks, so concurrent edits can be silently overwritten; scoping by updatable limits that blast radius.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.