ruby 73 lines · 3 tabs

Validate Nested Attributes for an Order and Its Line Items in Rails

Shared by codesnips Jul 2026
3 tabs
class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order, dependent: :destroy

  accepts_nested_attributes_for :line_items,
    allow_destroy: true,
    reject_if: ->(attrs) { attrs["product_id"].blank? && attrs["quantity"].blank? }

  validates :customer_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :total_cents, numericality: { greater_than_or_equal_to: 0 }
  validates_associated :line_items
  validate :must_have_line_items

  before_validation :recalculate_total

  private

  def recalculate_total
    self.total_cents = line_items.reject(&:marked_for_destruction?).sum do |item|
      item.quantity.to_i * item.unit_price_cents.to_i
    end
  end

  def must_have_line_items
    remaining = line_items.reject(&:marked_for_destruction?)
    errors.add(:base, "must contain at least one line item") if remaining.empty?
  end
end
3 files · ruby Explain with highlit

This snippet shows how a Rails Order aggregate validates itself together with its child LineItem records using accepts_nested_attributes_for, so that a single form submission either persists as a consistent whole or fails atomically with useful error messages.

In the Order model, has_many :line_items is paired with accepts_nested_attributes_for :line_items so nested params like line_items_attributes map directly onto associated records. reject_if drops rows where both product_id and quantity are blank, which prevents empty form rows from creating junk records, and allow_destroy: true lets the form mark existing items for removal with a _destroy flag. Because nested attributes wrap child saves in the parent's transaction, an invalid line item rolls back the whole order rather than leaving a half-saved record.

A subtle but important detail is validates_associated :line_items: without it, Rails still validates autosaved children, but declaring it makes the intent explicit and surfaces child errors on the parent. The custom validate :must_have_line_items guards against the empty-cart case, which association validations alone cannot express. The before_validation hook computes total_cents from the children so the total is never trusted from the client — a common security pitfall when totals arrive in params.

The LineItem model carries its own field-level rules: presence of product_id, a positive integer quantity, and a non-negative unit_price_cents. Keeping these on the child means each row is validated independently and errors are reported per item, which is exactly what the nested form needs to highlight the offending row.

In the OrdersController, strong parameters explicitly permit the nested array via line_items_attributes, including id and _destroy so updates and deletions work. The action relies on save returning false to render the form with @order.errors already populated across both levels.

This pattern is worth reaching for whenever a parent and its children are edited in one form and must stay consistent. The trade-offs are real: nested attributes couple the form shape to the model, reject_if logic can silently discard data if written carelessly, and deeply nested graphs become hard to reason about — at which point a dedicated form object is usually cleaner.


Related snips

ruby
require 'application_system_test_case'

class TasksTest < ApplicationSystemTestCase
  test 'deleting a task removes it from the list' do
    task = tasks(:one)

System test: asserting Turbo Stream responses

rails hotwire turbo
by Henry Kim 2 tabs
ruby
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

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
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

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Validate Nested Attributes for an Order and Its Line Items in Rails — share card
Link copied