Skip to content

ActiveRecord Deep — Query Interface and Advanced ORM Patterns

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about ActiveRecord Deep. We cover key concepts, practical examples, and best practices to help you master this topic.

Rails ActiveRecord provides a powerful query interface with eager loading, subqueries, calculations, pessimistic and optimistic locking, transactions, and SQL Injection prevention.

What You'll Learn

By the end of this tutorial, you'll write complex ActiveRecord queries, use eager loading with conditions, implement locking, compose subqueries, and optimize query performance.

Why ActiveRecord Matters

ActiveRecord is the ORM layer of Rails. Deep knowledge of its query interface leads to efficient database access and prevents N+1 queries and performance issues.

Real-World Use

A reporting system uses ActiveRecord calculations for revenue by month, eager loading with conditions for filtered relations, and pessimistic locking for inventory management.

ActiveRecord Path

flowchart LR
  A[Rails Models] --> B[ActiveRecord Deep]
  B --> C[Query Interface]
  B --> D[Eager Loading]
  B --> E[Locking]
  B --> F[Subqueries]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Advanced Eager Loading

Eager load with conditions and select.

# Eager load with conditions
products = Product.includes(:reviews).where(reviews: { approved: true })
# Equivalent to:
products = Product.left_joins(:reviews).where(reviews: { approved: true }).includes(:reviews)

# Select specific columns in eager loading
users = User.includes(:profile).where(profiles: { id: nil })
# Using references
products = Product.includes(:category).where.not(categories: { id: nil }).references(:categories)

Subqueries

Use subqueries in ActiveRecord.

# WHERE clause subquery
Product.where("price > (SELECT AVG(price) FROM products)")

# Using from
recent_orders = Order.where("created_at > ?", 30.days.ago)
top_customers = Customer.where(id: recent_orders.select(:customer_id).group(:customer_id)
  .having("COUNT(*) >= 5").select(:customer_id))

# Subquery in select
Product.select(
  "products.*",
  "(SELECT COUNT(*) FROM reviews WHERE reviews.product_id = products.id) AS review_count"
)

Optimistic Locking

Prevent concurrent updates with a lock column.

# Add lock_version column to table
class AddLockVersionToProducts < ActiveRecord::Migration[7.1]
  def change
    add_column :products, :lock_version, :integer, default: 0
  end
end

# ActiveRecord automatically manages lock_version
product = Product.find(1)
# Another request loads the same product...
product.update!(price: 20.0) # Raises ActiveRecord::StaleObjectError if version changed

# Retry logic
begin
  product.update!(price: 20.0)
rescue ActiveRecord::StaleObjectError
  product.reload
  retry
end

Pessimistic Locking

Lock rows in the database during a Transaction.

# Row-level lock
Order.transaction do
  order = Order.lock("FOR UPDATE").find(1)
  inventory = Inventory.lock!("FOR UPDATE").find(order.product_id)
  if inventory.quantity >= order.quantity
    inventory.decrement!(:quantity, order.quantity)
    order.update!(status: :confirmed)
  else
    raise "Insufficient inventory"
  end
end

# Different lock types
Product.lock("FOR SHARE").find(1)      # Shared lock
Product.lock("FOR UPDATE NOWAIT").find(1) # No wait
Product.lock("FOR UPDATE SKIP LOCKED").find(1) # Skip locked rows

Common Mistakes

1. N+1 Queries with Includes

includes without where on the associated table still causes N+1. Use references for where conditions.

2. Forgetting Database-Level Constraints

Rails validations are not enough. Add database constraints for data integrity.

3. Over-Locking

Pessimistic locking everywhere hurts performance. Use optimistic locking by default.

4. Not Using Transactions for Multiple Writes

Multiple saves without a transaction can cause partial updates. Wrap in transaction.

5. SQL Injection via String Conditions

Using string conditions with interpolation is dangerous. Use parameterized queries.

Practice Questions

1. How do you prevent N+1 queries in ActiveRecord?

Use includes or eager_load to load associations upfront.

2. What is the difference between optimistic and pessimistic locking?

Optimistic checks version on update. Pessimistic locks rows in the database.

3. How do you write a subquery in ActiveRecord?

Use .where("column > (SELECT ...)") or .where(id: OtherModel.select(:column)).

4. What does lock("FOR UPDATE") do?

Locks the selected rows for update until the transaction ends.

5. Challenge: Write a query with subquery, eager loading, and locking.

Product.transaction do
  top_products = Product.where("sales_count > (SELECT AVG(sales_count) FROM products)")
    .includes(:category)
    .lock("FOR UPDATE")
    .limit(10)
  top_products.each { |p| p.update!(featured: true) }
end

FAQ

What is the difference between includes and eager_load?

includes uses LEFT JOIN. eager_load uses LEFT OUTER JOIN. Both prevent N+1.

Can I use ActiveRecord without a database?

Yes. Use Active Model for non-persistent models.

What is a scope vs class method?

Scopes return ActiveRecord::Relation. Class methods can return anything.

How do I check if a query will be executed?

Inspect the .to_sql method to see the generated SQL.

What is the N+1 query problem?

Loading parent records and then lazily loading associated records in a loop.

Mini Project: Inventory Management with Locking

Build inventory management with proper locking.

class InventoryManager
  def process_order(order)
    Order.transaction do
      product = Product.lock("FOR UPDATE").find(order.product_id)
      if product.quantity >= order.quantity
        product.decrement!(:quantity, order.quantity)
        order.update!(status: :confirmed)
      else
        order.update!(status: :failed, failure_reason: "Insufficient stock")
      end
    end
  end
end

What's Next

Rails Migrations Deep Rails Validations Deep Rails Associations Deep

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro