Using Callbacks for Associated Model Consistency
Rails callbacks help maintain data consistency across associated models. When an Order is created, corresponding OrderItem records must be generated from the user’s cart. When an Order is destroyed, its OrderItems should be cleaned up.
Schema Setup
Two models, Order and OrderItem, share an orderID field for association:
create_table "order_items", force: :cascade do |t|
t.string "orderID"
t.integer "product_id"
t.decimal "price", precision: 10, scale: 2
t.decimal "amount", precision: 10, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["product_id"], name: "index_order_items_on_product_id", using: :btree
end
create_table "orders", force: :cascade do |t|
t.integer "user_id"
t.string "orderID"
t.text "message"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "fixed_address"
t.decimal "total_price", precision: 10, scale: 2
t.index ["user_id"], name: "index_orders_on_user_id", using: :btree
end
The key requirement: when an Order is placed, OrderItems must be created from the current cart. When an Order is deleted, its OrderItems must cascade.
Callback Implementation
class Order < ApplicationRecord
belongs_to :user
belongs_to :address
after_create :create_order_items
after_destroy :destroy_all_same_orderID
protected
def create_order_items
current_user = self.user
current_user.carts.each do |cart|
OrderItem.new(
user_id: current_user.id,
orderID: self.orderID,
product_id: cart.product_id,
price: cart.product.price,
amount: cart.amount
).save!
end
Cart.where(user_id: current_user.id).destroy_all
end
def destroy_all_same_orderID
OrderItem.where(orderID: self.orderID).destroy_all
end
end
Using save! (with bang) ensures the operation fails loudly if validation fails, preventing partial order creation.
Controller Integration
In the OrdersController#create action, you simply build and save the Order. The callback handles OrderItem creation automatically, keeping controller logic thin:
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
if @order.save
redirect_to @order, notice: 'Order created successfully.'
else
render :new
end
end
end
Callbacks are declared as protected or private to prevent external callers from invoking them directly. This encapsulation maintains a clear boundary around the model’s lifecycle logic.
Avoiding Callback Hell
Callbacks become problematic when they chain across multiple models or have conditional side effects. Follow these guidelines to keep them manageable:
- Keep callbacks focused on the model’s own data. An Order callback should create OrderItems, but should not send emails or charge credit cards — use service objects or background jobs for those.
- Prefer
after_createoverbefore_createwhen the operation depends on the record being persisted. - Use
has_manydependent callbacks (dependent: :destroy) instead of manualafter_destroywhere the relationship is a simple foreign key. - Extract complex callback logic into service objects. For example, an
OrderProcessorservice can handle cart-to-order conversion and callbacks independently.
Resources
- Rails Callbacks Guide
- Rails Associations: dependent
- Rails Ordering and Callback Best Practices
- Using Service Objects in Rails
Comments