class Order < ApplicationRecord
has_many :line_items, dependent: :destroy, inverse_of: :order
accepts_nested_attributes_for :line_items,
reject_if: :all_blank,
allow_destroy: true
validates :customer_name, presence: true
validates :line_items, presence: true
before_validation :recalculate_total
private
def recalculate_total
self.total_cents = line_items
.reject(&:marked_for_destruction?)
.sum { |item| item.unit_price_cents.to_i * item.quantity.to_i }
end
end
class LineItem < ApplicationRecord
belongs_to :order, inverse_of: :line_items
validates :sku, presence: true
validates :quantity, numericality: { greater_than: 0, only_integer: true }
validates :unit_price_cents, numericality: { greater_than_or_equal_to: 0 }
def subtotal_cents
unit_price_cents.to_i * quantity.to_i
end
end
class OrdersController < ApplicationController
def new
@order = Order.new
@order.line_items.build
end
def create
@order = Order.new(order_params)
if @order.save
redirect_to @order, notice: "Order created with #{@order.line_items.size} items."
else
flash.now[:alert] = "Could not create order."
render :new, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(
:customer_name,
line_items_attributes: [:id, :sku, :quantity, :unit_price_cents, :_destroy]
)
end
end
This snippet shows how Rails builds a parent record and its children in a single atomic operation using accepts_nested_attributes_for. The core idea is that one call to save on the parent wraps the entire graph — the Order and all of its LineItem rows — in a single database transaction. If any child fails validation, the parent is not persisted and no partial data is written, which is exactly the guarantee needed when a parent is meaningless without its children.
In Order model, accepts_nested_attributes_for :line_items enables the parent to accept a nested hash or array of child attributes and translate them into associated records. The reject_if proc drops blank rows so empty form fields do not create junk line items, and allow_destroy: true lets an existing child be removed by passing _destroy: true. The validates :line_items, presence: true rule enforces that an order must have at least one item; because nested saves share the parent's transaction, this validation failure rolls the whole thing back. The before_validation callback normalizes derived data before the child records are validated, keeping the parent's total consistent with its items.
LineItem model is an ordinary child that belongs_to :order and validates its own fields. Its validations run as part of the parent's valid? check, and any errors bubble up into order.errors so the form can display them.
OrdersController ties it together. order_params uses strong parameters with a nested line_items_attributes key — note the array-of-permitted-keys form including :id and :_destroy, which is what makes updates and deletions work. The create action simply calls @order.save; there is no manual transaction block because ActiveRecord already opens one. When the save fails, the same @order object carries validation errors for re-rendering the form.
The main trade-off is that autosaved associations run all child callbacks and validations inside one transaction, which can be heavy for very large collections and can hide which specific child failed. It is also easy to forget to permit :id, causing Rails to create duplicates instead of updating. For bounded parent/child graphs like orders and line items, though, this pattern gives clean forms and strong data-integrity guarantees with very little code.
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.