Active Record Query Interface


Is a part of Active Record

Retrieving Objects from DB

Find

Customer.find(10)
SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1

Customer.find([1, 10])
SELECT * FROM customers WHERE (customers.id IN (1,10))

customers = Customer.find([3, 17]) -- composite primary key
SELECT * FROM customers WHERE store_id = 3 AND id = 17

Take

customer = Customer.take
SELECT * FROM customers LIMIT 1

customers = Customer.take(2)
SELECT * FROM customers LIMIT 2

First

customer = Customer.first
SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1

customers = Customer.first(3)
SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3

Last

customer = Customer.last
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1

customers = Customer.last(3)
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3

Find by

Customer.find_by first_name: 'Lifo'
SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1
-- same as
Customer.where(first_name: "Lifo").take

Find each

Loads batches of 1'000 entries to save memory.

Customer.find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

Options for find_each

:batch_size
:start # it is by default ordered by the primary key in ascending order
:finish
:order # :asc or :desc

Find in Batches

Similar to find_each but loads the batch into an array.

Customer.find_in_batches do |customers|
  export.add_customers(customers)
end

Options for find_in_batches

:batch_size
:start # it is by default ordered by the primary key in ascending order
:finish

Conditions

Book.where("title = ? AND out_of_print = ?", params[:title], false)

Book.where("created_at >= :start_date AND created_at <= :end_date", { start_date: params[:start_date], end_date: params[:end_date] })

Book.where("title LIKE ?", Book.sanitize_sql_like(params[:title]) + "%")

Hash Conditions

Book.where(out_of_print: true)

Book.where([:author_id, :id] => [[15, 1], [15, 2]])

Book.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) # range condition

Customer.where(orders_count: [1, 3, 5])

Logical conditions

Customer.where.not(orders_count: [1, 3, 5]) # NOT

Customer.where(last_name: "Smith").or(Customer.where(orders_count: [1, 3, 5])) # OR

Customer.where(last_name: "Smith").where(orders_count: [1, 3, 5]) # AND
Customer.where(id: [1, 2]).and(Customer.where(id: [2, 3])) # AND

Ordering

Book.order(:created_at) # orders ascending
Book.order("created_at")



Book.order(created_at: :desc)
Book.order(created_at: :asc)

Book.order("created_at DESC")
Book.order("created_at ASC")

# Multiple fields
Book.order(title: :asc, created_at: :desc)
Book.order(:title, created_at: :desc)

Book.order("title ASC, created_at DESC")
Book.order("title ASC", "created_at DESC")

Book.order("title ASC").order("created_at DESC")

Selecting specific Fields

Book.select(:isbn, :out_of_print)
Book.select("isbn, out_of_print")

Customer.select(:last_name).distinct

Limit and Offset

Customer.limit(5)
Customer.limit(5).offset(30)

Grouping

Order.select("created_at").group("created_at")

Order.group(:status).count

HAVING conditions

HAVING adds conditions to a group
big_orders = Order.select("created_at, sum(total) as total_price")
                  .group("created_at")
                  .having("sum(total) > ?", 200)

big_orders[0].total_price
# Returns the total price for the first Order object
SELECT created_at, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200

Readonly Objects

customer = Customer.readonly.first
customer.visits += 1
customer.save # Raises an ActiveRecord::ReadOnlyRecord

Locking Records

See also 223_Multi-User App#5.1 Locking

Optimistic Locking

For explaination of the concept look here.
In Rails Locking works by adding a lock_version column with an integer, that gets incremented after every update. To enable locking you just have to add a new column lock_version of type integer, Active Record handles the rest.

c1 = Customer.find(1)
c2 = Customer.find(1)

c1.first_name = "Sandra"
c1.save

c2.first_name = "Michael"
c2.save # Raises an ActiveRecord::StaleObjectError

Pessimistic Locking

Book.transaction do
  book = Book.lock.first
  book.title = "Algorithms, second edition"
  book.save!
end
When there is already an object instance
book = Book.first
book.with_lock do
  # This block is called within a transaction,
  # book is already locked.
  book.increment!(:views)
end

Joining Tables

# Raw SQL
Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")

Book.joins(:reviews)
# SELECT books.* FROM books
#  INNER JOIN reviews ON reviews.book_id = books.id

Book.joins(:author, :reviews) 
# all books that have an author and at least one review
# SELECT books.* FROM books
#  INNER JOIN authors ON authors.id = books.author_id
#  INNER JOIN reviews ON reviews.book_id = books.id

Book.joins(reviews: :customer)
# all books that have a review by a customer.
# SELECT books.* FROM books
#  INNER JOIN reviews ON reviews.book_id = books.id
#  INNER JOIN customers ON customers.id = reviews.customer_id

Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])
# all authors that have books with reviews and have been ordered by a customer, and the suppliers for those books.

Left outer joins

Customer.left_outer_joins(:reviews).distinct.select("customers.*, COUNT(reviews.*) AS reviews_count").group("customers.id")
# all customers with their count of reviews, whether or not they have any reviews at all
# SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
# LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY # customers.id

Scopes

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
end

Book.out_of_print # => #<ActiveRecord::Relation> # all out of print books

There is a lot more possible. See: 14. Scopes and 16. Enums

Find or Build a New Object

Customer.find_or_create_by(first_name: 'Andy')
# doesn't get persisted if validations fail

Customer.find_or_create_by!(first_name: 'Andy')
# throws exception if validations fail

nina = Customer.find_or_initialize_by(first_name: 'Nina')
# won't persist the customer. You have to call nina.save

Finding by SQL

Customer.find_by_sql("SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.created_at desc")

Customer.lease_connection.select_all("SELECT first_name, created_at FROM customers WHERE id = '1'").to_a # => [{...}, {...}, ...] (hash array of database entries)

Book.where(out_of_print: true).pluck(:id) # => [1, 5, 8]

Customer.where(id: 1).pick(:id)
# same as
Customer.where(id: 1).pluck(:id).first

Customer.ids
# same as
Customer.pluck(:id)

Existence of Objects

Customer.exists?(1)
Customer.exists?(first_name: ["Jane", "Sergei"])
Customer.where(first_name: "Ryan").exists?

Calculations

Customer.count(:title)
Order.average("subtotal")
Order.minimum("subtotal")
Order.maximum("subtotal")
Order.sum("subtotal")