Интерфейс запросов Active Record

Это руководство раскрывает различные способы получения данных из базы данных, используя Active Record.

После его прочтения, вы узнаете:

  • Как искать записи, используя различные методы и условия.
  • Как определять порядок, получаемые атрибуты, группировку и другие свойства поиска записей.
  • Как использовать нетерпеливую загрузку (eager loading) для уменьшения числа запросов к базе данных, необходимых для получения данных.
  • Как использовать методы динамического поиска.
  • Как использовать цепочки методов (method chaining), для использования нескольких методов Active Record одновременно.
  • Как проверять существование отдельных записей.
  • Как выполнять различные вычисления в моделях Active Record.
  • Как запускать EXPLAIN на relations.

Если вы использовали чистый SQL для поиска записей в базе данных, то скорее всего обнаружите, что в Rails есть лучшие способы выполнения тех же операций. Active Record ограждает вас от необходимости использования SQL во многих случаях.

Примеры кода далее в этом руководстве будут относиться к некоторым из этих моделей:

TIP: Все модели используют id как первичный ключ, если не указано иное.

class Client < ApplicationRecord
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end
class Address < ApplicationRecord
  belongs_to :client
end
class Order < ApplicationRecord
  belongs_to :client, counter_cache: true
end
class Role < ApplicationRecord
  has_and_belongs_to_many :clients
end

Active Record выполнит запросы в базу данных за вас, он совместим с большинством СУБД, включая MySQL, MariaDB, PostgreSQL и SQLite. Независимо от того, какая используется СУБД, формат методов Active Record будет всегда одинаковый.

Получение объектов из базы данных

Для получения объектов из базы данных Active Record предоставляет несколько методов поиска. В каждый метод поиска можно передавать аргументы для выполнения определенных запросов в базу данных без необходимости писать на чистом SQL.

Методы следующие:

  • find
  • create_with
  • distinct
  • eager_load
  • extending
  • from
  • group
  • having
  • includes
  • joins
  • left_outer_joins
  • limit
  • lock
  • none
  • offset
  • order
  • preload
  • readonly
  • references
  • reorder
  • reverse_order
  • select
  • where

Методы поиска, возвращающие коллекцию, такие как where и group, возвращают экземпляр ActiveRecord::Relation. Методы, ищущие отдельную сущность, такие как find и first, возвращают отдельный экземпляр модели.

Вкратце основные операции Model.find(options) таковы:

  • Преобразовать предоставленные опции в эквивалентный запрос SQL.
  • Выполнить запрос SQL и получить соответствующие результаты из базы данных.
  • Создать экземпляр эквивалентного объекта Ruby подходящей модели для каждой строки результата запроса.
  • Запустить колбэки after_find и далее after_initialize, если таковые имеются.

Получение одиночного объекта

Active Record предоставляет несколько различных способов получения одиночного объекта.

find

Используя метод find, можно получить объект, соответствующий определенному первичному ключу (primary key) и предоставленным опциям. Например:

# Ищет клиента с первичным ключом (id) 10.
client = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">

SQL эквивалент этого такой:

SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1

Метод find вызывает исключение ActiveRecord::RecordNotFound, если соответствующей записи не было найдено.

Этот метод также можно использовать для получения нескольких объектов. Вызовите метод find и передайте в него массив первичных ключей. Возвращенным результатом будет массив, содержащий все записи, соответствующие представленным первичным ключам. Например:

# Найдем клиентов с первичными ключами 1 и 10.
clients = Client.find([1, 10]) # Или даже Client.find(1, 10)
# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]

SQL эквивалент этого такой:

SELECT * FROM clients WHERE (clients.id IN (1,10))

take

Метод take получает запись без какого-либо явного упорядочивания. Например:

client = Client.take
# => #<Client id: 1, first_name: "Lifo">

SQL эквивалент этого такой:

SELECT * FROM clients LIMIT 1

Метод take возвращает nil, если ни одной записи не найдено, и исключение не будет вызвано.

В метод take можно передать числовой аргумент, чтобы вернуть это количество результатов. Например

clients = Client.take(2)
# => [
#   #<Client id: 1, first_name: "Lifo">,
#   #<Client id: 220, first_name: "Sara">
# ]

SQL эквивалент этого такой:

SELECT * FROM clients LIMIT 2

Метод take! ведет себя подобно take, за исключением того, что он вызовет ActiveRecord::RecordNotFound, если не найдено ни одной соответствующей записи.

TIP: Получаемая запись может отличаться в зависимости от подсистемы хранения СУБД.

first

Метод first находит первую запись, упорядоченную по первичному ключу (по умолчанию). Например:

client = Client.first
# => #<Client id: 1, first_name: "Lifo">

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1

Метод first возвращает nil, если не найдено соответствующей записи, и исключение не вызывается.

Если скоуп по умолчанию содержит метод order, first возвратит первую запись в соответствии с этим упорядочиванием.

В метод first можно передать числовой аргумент, чтобы вернуть это количество результатов. Например

clients = Client.first(3)
# => [
#   #<Client id: 1, first_name: "Lifo">,
#   #<Client id: 2, first_name: "Fifo">,
#   #<Client id: 3, first_name: "Filo">
# ]

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3

На коллекции, упорядоченной с помощью order, first вернет первую запись, упорядоченную по указанному в order атрибуту.

client = Client.order(:first_name).first
# => #<Client id: 2, first_name: "Fifo">

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1

Метод first! ведет себя подобно first, за исключением того, что он вызовет ActiveRecord::RecordNotFound, если не найдено ни одной соответствующей записи.

last

Метод last находит последнюю запись, упорядоченную по первичному ключу (по умолчанию). Например:

client = Client.last
# => #<Client id: 221, first_name: "Russel">

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1

Метод last возвращает nil, если не найдено соответствующей записи, и исключение не вызывается.

Если скоуп по умолчанию содержит метод order, last возвратит последнюю запись в соответствии с этим упорядочиванием.

В метод last можно передать числовой аргумент, чтобы вернуть это количество результатов. Например

clients = Client.last(3)
# => [
#   #<Client id: 219, first_name: "James">,
#   #<Client id: 220, first_name: "Sara">,
#   #<Client id: 221, first_name: "Russel">
# ]

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3

На коллекции, упорядоченной с помощью order, last вернет последнюю запись, упорядоченную по указанному в order атрибуту.

client = Client.order(:first_name).last
# => #<Client id: 220, first_name: "Sara">

SQL эквивалент этого такой:

SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1

Метод last! ведет себя подобно last, за исключением того, что он вызовет ActiveRecord::RecordNotFound, если не найдено ни одной соответствующей записи.

find_by

Метод find_by ищет первую запись, соответствующую некоторым условиям. Например:

Client.find_by first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">

Client.find_by first_name: 'Jon'
# => nil

Это эквивалент записи:

Client.where(first_name: 'Lifo').take

SQL эквивалент выражения выше, следующий:

SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1

Метод find_by! ведет себя подобно find_by, за исключением того, что он вызовет ActiveRecord::RecordNotFound, если не найдено ни одной соответствующей записи. Например:

Client.find_by! first_name: 'does not exist'
# => ActiveRecord::RecordNotFound

Это эквивалент записи:

Client.where(first_name: 'does not exist').take!

Получение нескольких объектов пакетами

Часто необходимо перебрать огромный набор записей, когда рассылаем письма всем пользователям или импортируем некоторые данные.

Это может показаться простым:

# Это может потребить слишком много памяти, если таблица большая.
User.all.each do |user|
  NewsMailer.weekly(user).deliver_now
end

Но этот подход становится очень непрактичным с увеличением размера таблицы, поскольку User.all.each говорит Active Record извлечь таблицу полностью за один проход, создать объект модели для каждой строки и держать этот массив в памяти. В реальности, если имеется огромное количество записей, полная коллекция может превысить количество доступной памяти.

Rails предоставляет два метода, которые решают эту проблему путем разделения записей на дружелюбные к памяти пакеты для обработки. Первый метод, find_each, получает пакет записей и затем вкладывает каждую запись в блок отдельно как модель. Второй метод, find_in_batches, получает пакет записей и затем вкладывает весь пакет в блок как массив моделей.

TIP: Методы find_each и find_in_batches предназначены для пакетной обработки большого числа записей, которые не поместятся в памяти за раз. Если нужно просто перебрать тысячу записей, более предпочтителен вариант обычных методов поиска.

find_each

Метод find_each получает пакет записей и затем передает каждую запись в блок. В следующем примере find_each получает пользователей пакетами по 1000 записей, а затем передает их в блок один за другим:

User.find_each do |user|
  NewsMailer.weekly(user).deliver_now
end

Этот процесс повторяется, извлекая больше пакетов при необходимости, пока не будут обработаны все записи.

find_each работает на классах модели, как показано выше, а также на relation:

User.where(weekly_subscriber: true).find_each do |user|
  NewsMailer.weekly(user).deliver_now
end

только у них нет упорядочивания, так как методу необходимо собственное упорядочивание для работы.

Если у получателя есть упорядочивание, то поведение зависит от флажка config.active_record.error_on_ignored_order. Если true, вызывается ArgumentError, в противном случае упорядочивание игнорируется, что является поведением по умолчанию. Это можно переопределить с помощью опции :error_on_ignore, описанной ниже.

Опции для find_each

:batch_size

Опция :batch_size позволяет определить число записей, подлежащих получению в одном пакете, до передачи отдельной записи в блок. Например, для получения 5000 записей в пакете:

User.find_each(batch_size: 5000) do |user|
  NewsMailer.weekly(user).deliver_now
end

:start

По умолчанию записи извлекаются в порядке увеличения первичного ключа, который должен быть числом. Опция :start позволяет вам настроить первый ID последовательности, когда наименьший ID не тот, что вам нужен. Это может быть полезно, например, если хотите возобновить прерванный процесс пакетирования, предоставив последний обработанный ID как контрольную точку.

Например, чтобы выслать письма только пользователям с первичным ключом, начинающимся от 2000:

User.find_each(start: 2000) do |user|
  NewsMailer.weekly(user).deliver_now
end

:finish

Подобно опции :start, :finish позволяет указать последний ID последовательности, когда наибольший ID не тот, что вам нужен. Это может быть полезно, например, если хотите запустить процесс пакетирования, используя подмножество записей на основании :start и :finish

Например, чтобы выслать письма только пользователям с первичным ключом от 2000 до 10000:

User.find_each(start: 2000, finish: 10000) do |user|
  NewsMailer.weekly(user).deliver_now
end

Другим примером является наличие нескольких воркеров, работающих с одной и той же очередью обработки. Можно было бы обрабатывать каждым воркером 10000 записей, установив подходящие опции :start и :finish в каждом воркере.

:error_on_ignore

Переопределяет настройку приложения, указывающую, должна ли быть вызвана ошибка, если в relation присутствует упорядочивание.

find_in_batches

Метод find_in_batches похож на find_each тем, что они оба получают пакеты записей. Различие в том, что find_in_batches передает в блок пакеты как массив моделей, вместо отдельной модели. Следующий пример передаст в представленный блок массив из 1000 счетов за раз, а в последний блок содержащий все оставшиеся счета:

# Передает в add_invoices массив из 1000 счетов за раз.
Invoice.find_in_batches do |invoices|
  export.add_invoices(invoices)
end

find_in_batches работает на классах модели, как показано выше, а также на relation:

Invoice.pending.find_in_batches do |invoice|
  pending_invoices_export.add_invoices(invoices)
end

только у них нет упорядочивания, так как методу необходимо собственное упорядочивание для работы.

Опции для find_in_batches

Метод find_in_batches принимает те же опции, что и find_each.

Условия

Метод where позволяет определить условия для ограничения возвращаемых записей, которые представляют WHERE-часть выражения SQL. Условия могут быть заданы как строка, массив или хэш.

(pure-string-conditions) Чисто строковые условия

Если вы хотите добавить условия в свой поиск, можете просто определить их там, подобно Client.where("orders_count = '2'"). Это найдет всех клиентов, где значение поля orders_count равно 2.

WARNING: Создание условий в чистой строке подвергает вас риску SQL инъекций. Например, Client.where("first_name LIKE '%#{params[:first_name]}%'") не безопасно. Смотрите следующий раздел для более предпочтительного способа обработки условий с использованием массива.

(array-conditions) Условия с использованием массива

Что если количество может изменяться, скажем, как аргумент откуда-то извне, возможно даже от пользователя? Поиск тогда принимает такую форму:

Client.where("orders_count = ?", params[:orders])

Active Record примет первый аргумент в качестве строки условия, а все остальные элементы подставит вместо знаков вопроса (?) в ней.

Если хотите определить несколько условий:

Client.where("orders_count = ? AND locked = ?", params[:orders], false)

В этом примере первый знак вопроса будет заменен на значение в params[:orders] и второй будет заменен SQL аналогом false, который зависит от адаптера.

Этот код значительно предпочтительнее:

Client.where("orders_count = ?", params[:orders])

чем такой код:

Client.where("orders_count = #{params[:orders]}")

по причине безопасности аргумента. Помещение переменной прямо в строку условий передает переменную в базу данных как есть. Это означает, что неэкранированная переменная, переданная пользователем, может иметь злой умысел. Если так сделать, вы подвергаете базу данных риску, так как если пользователь обнаружит, что он может использовать вашу базу данных, то он сможет сделать с ней что угодно. Никогда не помещайте аргументы прямо в строку условий!

TIP: Подробнее об опасности SQL инъекций можно узнать из Руководства Ruby On Rails по безопасности.

Символы-заполнители в условиях

Подобно тому, как (?) заменяют параметры, можно использовать ключи в условиях совместно с соответствующим хэшем ключей/значений:

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

Читаемость улучшится, в случае если вы используете большое количество переменных в условиях.

(hash-conditions) Условия с использованием хэша

Active Record также позволяет передавать условия в хэше, что улучшает читаемость синтаксиса условий. В этом случае передается хэш с ключами, соответствующими полям, которые хотите уточнить, и с значениями, которые вы хотите к ним применить:

NOTE: Хэшем можно передать условия проверки только равенства, интервала и подмножества.

Условия равенства

Client.where(locked: true)

Это сгенерирует такой SQL:

SELECT * FROM clients WHERE (clients.locked = 1)

Имя поля также может быть строкой, а не символом:

Client.where('locked' => true)

В случае отношений belongs_to, может быть использован ключ связи для указания модели, если как значение используется объект Active Record. Этот метод также работает с полиморфными отношениями.

Article.where(author: author)
Author.joins(:articles).where(articles: { author: author })

Интервальные условия

Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

Это найдет всех клиентов, созданных вчера, с использованием SQL выражения BETWEEN:

SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')

Это была демонстрация более короткого синтаксиса для примеров в Условия с использованием массива

Условия подмножества

Если хотите найти записи, используя выражение IN, можете передать массив в хэш условий:

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

Этот код сгенерирует подобный SQL:

SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))

Условия NOT

Запросы NOT в SQL могут быть созданы с помощью where.not:

Client.where.not(locked: true)

Другими словами, этот запрос может быть сгенерирован с помощью вызова where без аргументов и далее присоединенным not с переданными условиями для where. Это сгенерирует такой SQL:

SELECT * FROM clients WHERE (clients.locked != 1)

Условия OR

Условия OR между двумя отношениями могут быть построены путем вызова or на первом отношении и передачи второго в качестве аргумента.

Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))
SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))

(ordering) Сортировка

Чтобы получить записи из базы данных в определенном порядке, можете использовать метод order.

Например, если вы получаете ряд записей и хотите упорядочить их в порядке возрастания поля created_at в таблице:

Client.order(:created_at)
# ИЛИ
Client.order("created_at")

Также можете определить ASC или DESC:

Client.order(created_at: :desc)
# ИЛИ
Client.order(created_at: :asc)
# ИЛИ
Client.order("created_at DESC")
# ИЛИ
Client.order("created_at ASC")

Или сортировку по нескольким полям:

Client.order(orders_count: :asc, created_at: :desc)
# ИЛИ
Client.order(:orders_count, created_at: :desc)
# ИЛИ
Client.order("orders_count ASC, created_at DESC")
# ИЛИ
Client.order("orders_count ASC", "created_at DESC")

Если хотите вызвать order несколько раз, последующие сортировки будут добавлены к первой:

Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC

WARNING: Если используется MySQL 5.7.5 и выше, то при выборе полей из результирующей выборки с помощью методов, таких как select, pluck и ids, метод order вызовет исключение ActiveRecord::StatementInvalid, если поля, используемые в выражении order не включены в список выбора. Смотрите следующий раздел по выбору полей из результирующей выборки.

Выбор определенных полей

По умолчанию Model.find выбирает все множество полей результата, используя select *.

Чтобы выбрать подмножество полей из всего множества, можете определить его, используя метод select.

Например, чтобы выбрать только столбцы viewable_by и locked:

Client.select("viewable_by, locked")

Используемый для этого запрос SQL будет иметь подобный вид:

SELECT viewable_by, locked FROM clients

Будьте осторожны, поскольку это также означает, что будет инициализирован объект модели только с теми полями, которые вы выбрали. Если вы попытаетесь обратиться к полям, которых нет в инициализированной записи, то получите:

ActiveModel::MissingAttributeError: missing attribute: <attribute>

Где <attribute> это атрибут, который был запрошен. Метод id не вызывает ActiveRecord::MissingAttributeError, поэтому будьте аккуратны при работе со связями, так как они нуждаются в методе id для правильной работы.

Если хотите вытащить только по одной записи для каждого уникального значения в определенном поле, можно использовать distinct:

Client.select(:name).distinct

Это сгенерирует такой SQL:

SELECT DISTINCT name FROM clients

Также можно убрать ограничение уникальности:

query = Client.select(:name).distinct
# => Возвратит уникальные имена

query.distinct(false)
# => Возвратит все имена, даже если есть дубликаты

Ограничение и смещение

Чтобы применить LIMIT к SQL, запущенному с помощью Model.find, нужно определить LIMIT, используя методы limit и offset на relation.

Используйте limit для определения количества записей, которые будут получены, и offset - для числа записей, которые будут пропущены до начала возврата записей. Например:

Client.limit(5)

возвратит максимум 5 клиентов, и, поскольку не определено смещение, будут возвращены первые 5 клиентов в таблице. Запускаемый SQL будет выглядеть подобным образом:

SELECT * FROM clients LIMIT 5

Добавление offset к этому

Client.limit(5).offset(30)

Возвратит максимум 5 клиентов, начиная с 31-го. SQL выглядит так:

SELECT * FROM clients LIMIT 5 OFFSET 30

Группировка

Чтобы применить условие GROUP BY к SQL, можно использовать метод group.

Например, если хотите найти коллекцию дат, в которые были созданы заказы:

Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")

Это выдаст вам отдельный объект Order на каждую дату, для которой были заказы в базе данных.

SQL, который будет выполнен, будет выглядеть так:

SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)

Общее количество сгруппированных элементов

Чтобы получить общее количество сгруппированных элементов одним запросом, вызовите count после group.

Order.group(:status).count
# => { 'awaiting_approval' => 7, 'paid' => 12 }

SQL, который будет исполнен, будет выглядеть как-то так:

SELECT COUNT (*) AS count_all, status AS status
FROM "orders"
GROUP BY status

Having

SQL использует условие HAVING для определения условий для полей, указанных в GROUP BY. Условие HAVING, определенное в SQL, запускается в Model.find с использованием метода having для поиска.

Например:

Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)").having("sum(price) > ?", 100)

SQL, который будет выполнен, выглядит так:

SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
HAVING sum(price) > 100

Это возвращает дату и итоговую цену для каждого объекта заказа, сгруппированные по дню, когда они были заказаны, и где цена больше $100.

Переопределяющие условия

unscope

Можете указать определенные условия, которые будут убраны, используя метод unscope. Например:

Article.where('id > 10').limit(20).order('id asc').unscope(:order)

SQL, который будет выполнен:

SELECT * FROM articles WHERE id > 10 LIMIT 20

# Оригинальный запрос без `unscope`
SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20

Также можно убрать определенные условия where. Например:

Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0

Relation, использующий unscope повлияет на любой relation, в который он слит:

Article.order('id asc').merge(Article.unscope(:order))
# SELECT "articles".* FROM "articles"

only

Также можно переопределить условия, используя метод only. Например:

Article.where('id > 10').limit(20).order('id desc').only(:order, :where)

SQL, который будет выполнен:

SELECT * FROM articles WHERE id > 10 ORDER BY id DESC

# Оригинальный запрос без `only`
SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20

reorder

Метод reorder переопределяет сортировку скоупа по умолчанию. Например:

class Article < ApplicationRecord
  ..
  ..
  has_many :comments, -> { order('posted_at DESC') }
end

Article.find(10).comments.reorder('name')

SQL, который будет выполнен:

SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY name

В случае, когда условие reorder не было использовано, запущенный SQL будет:

SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC

reverse_order

Метод reverse_order меняет направление условия сортировки, если оно определено:

Client.where("orders_count > 10").order(:name).reverse_order

SQL, который будет выполнен:

SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC

Если условие сортировки не было определено в запросе, reverse_order сортирует по первичному ключу в обратном порядке:

Client.where("orders_count > 10").reverse_order

SQL, который будет выполнен:

SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC

Этот метод не принимает аргументы.

rewhere

Метод rewhere переопределяет существующее именованное условие where. Например:

Article.where(trashed: true).rewhere(trashed: false)

SQL, который будет выполнен:

SELECT * FROM articles WHERE `trashed` = 0

В случае, когда не используется условие rewhere,

Article.where(trashed: true).where(trashed: false)

выполненный SQL будет следующий:

SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0

Нулевой Relation

Метод none возвращает сцепляемый relation без записей. Любые последующие условия, сцепленные с возвращенным relation, продолжат генерировать пустые relation. Это полезно в случаях, когда необходим сцепляемый отклик на метод или скоуп, который может вернуть пустые результаты.

Article.none # возвращает пустой Relation и не вызывает запросов.
# От метода visible_articles ожидается, что он вернет Relation.
@articles = current_user.visible_articles.where(name: params[:name])

def visible_articles
  case role
  when 'Country Manager'
    Article.where(country: country)
  when 'Reviewer'
    Article.published
  when 'Bad User'
    Article.none # => если бы вернули [] или nil, код поломался бы в этом случае
  end
end

Объекты только для чтения

Active Record предоставляет relation метод readonly для явного запрета изменения любого возвращаемого объекта. Любая попытка изменить запись доступную только для чтения не удастся, вызвав исключение ActiveRecord::ReadOnlyRecord.

client = Client.readonly.first
client.visits += 1
client.save

Так как client явно указан как объект доступный только для чтения, выполнение вышеуказанного кода выдаст исключение ActiveRecord::ReadOnlyRecord при вызове client.save с обновленным значением visits.

Блокировка записей для обновления

Блокировка полезна для предотвращения гонки условий при обновлении записей в базе данных и обеспечения атомарного обновления.

Active Record предоставляет два механизма блокировки:

  • Оптимистичная блокировка
  • Пессимистичная блокировка

Оптимистичная блокировка

Оптимистичная блокировка позволяет нескольким пользователям обращаться к одной и той же записи для редактирования и предполагает минимум конфликтов с данными. Она осуществляет это с помощью проверки, внес ли другой процесс изменения в записи, с тех пор как она была открыта. Если это происходит, вызывается исключение ActiveRecord::StaleObjectError, и обновление игнорируется.

Столбец оптимистичной блокировки

Чтобы начать использовать оптимистичную блокировку, таблица должна иметь столбец, называющийся lock_version, с типом integer. Каждый раз, когда запись обновляется, Active Record увеличивает значение lock_version, и средства блокирования обеспечивают, что для записи, вызванной дважды, та, которая первая успеет будет сохранена, а для второй будет вызвано исключение ActiveRecord::StaleObjectError. Пример:

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

c1.first_name = "Michael"
c1.save

c2.name = "should fail"
c2.save # вызывает исключение ActiveRecord::StaleObjectError

Вы ответственны за разрешение конфликта с помощью обработки исключения и либо отката, либо объединения, либо применения бизнес-логики, необходимой для разрешения конфликта.

Это поведение может быть отключено, если установить ActiveRecord::Base.lock_optimistically = false.

Для переопределения имени столбца lock_version, ActiveRecord::Base предоставляет атрибут класса locking_column:

class Client < ApplicationRecord
  self.locking_column = :lock_client_column
end

Пессимистичная блокировка

Пессимистичная блокировка использует механизм блокировки, предоставленный лежащей в основе базой данных. Использование lock при построении relation применяет эксклюзивную блокировку для выбранных строк. Relations, которые используют lock, обычно упакованы внутри transaction для предотвращения условий взаимной блокировки (дедлока).

Например:

Item.transaction do
  i = Item.lock.first
  i.name = 'Jones'
  i.save!
end

Вышеописанная сессия осуществляет следующие SQL для бэкенда MySQL:

SQL (0.2ms)   BEGIN
Item Load (0.3ms)   SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item Update (0.4ms)   UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms)   COMMIT

Также можно передать чистый SQL в опцию lock для разрешения различных типов блокировок. Например, в MySQL есть выражение, называющееся LOCK IN SHARE MODE, которым можно заблокировать запись, но все же разрешить другим запросам читать ее. Чтобы указать это выражения, просто передайте его как опцию блокировки:

Item.transaction do
  i = Item.lock("LOCK IN SHARE MODE").find(1)
  i.increment!(:views)
end

Если у вас уже имеется экземпляр модели, можно одновременно начать транзакцию и затребовать блокировку, используя следующий код:

item = Item.first
item.with_lock do
  # Этот блок вызывается в транзакции,
  # элемент уже заблокирован.
  item.increment!(:views)
end

(joining-tables) Соединительные таблицы

Active Record предоставляет два метода поиска для определения условия JOIN в результирующем SQL: joins и left_outer_joins. В то время, как joins следует использовать для INNER JOIN или пользовательских запросов, left_outer_joins используется для запросов с помощью LEFT OUTER JOIN.

joins

Существует несколько способов использования метода joins.

Использование строкового фрагмента SQL

Можно просто передать чистый SQL, определяющий условие JOIN в joins.

Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")

Это приведет к следующему SQL:

SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'

Использование массива/хэша именованных связей

Active Record позволяет использовать имена связей, определенных в модели, как ярлыки для определения условия JOIN этих связей при использовании метода joins.

Например, рассмотрим следующие модели Category, Article, Comment, Guest и Tag:

class Category < ApplicationRecord
  has_many :articles
end

class Article < ApplicationRecord
  belongs_to :category
  has_many :comments
  has_many :tags
end

class Comment < ApplicationRecord
  belongs_to :article
  has_one :guest
end

class Guest < ApplicationRecord
  belongs_to :comment
end

class Tag < ApplicationRecord
  belongs_to :article
end

Сейчас все нижеследующее создаст ожидаемые соединительные запросы с использованием INNER JOIN:

Соединение одиночной связи
Category.joins(:articles)

Это создаст:

SELECT categories.* FROM categories
  INNER JOIN articles ON articles.category_id = categories.id

Или, по-русски, "возвратить объект Category для всех категорий со статьями". Обратите внимание, что будут дублирующиеся категории, если более одной статьи имеют одинаковые категорию. Если нужны уникальные категории, можно использовать Category.joins(:articles).distinct.

Соединение нескольких связей

Article.joins(:category, :comments)

Это создаст:

SELECT articles.* FROM articles
  INNER JOIN categories ON articles.category_id = categories.id
  INNER JOIN comments ON comments.article_id = articles.id

Или, по-русски, "возвратить все статьи, у которых есть категория и как минимум один комментарий". Отметьте, что статьи с несколькими комментариями будут показаны несколько раз.

Соединение вложенных связей (одного уровня)
Article.joins(comments: :guest)

Это создаст:

SELECT articles.* FROM articles
  INNER JOIN comments ON comments.article_id = articles.id
  INNER JOIN guests ON guests.comment_id = comments.id

Или, по-русски, "возвратить все статьи, в которых есть комментарий, оставленный гостем".

Соединение вложенных связей (разных уровней)
Category.joins(articles: [{ comments: :guest }, :tags])

Это создаст:

SELECT categories.* FROM categories
  INNER JOIN articles ON articles.category_id = categories.id
  INNER JOIN comments ON comments.article_id = articles.id
  INNER JOIN guests ON guests.comment_id = comments.id
  INNER JOIN tags ON tags.article_id = articles.id

Или, по-русски: "возвратить все категории, в которых есть статьи, и в этих статьях есть комментарий, оставленный гостем, а также в этих статьях есть тег".

Определение условий в соединительных таблицах

В соединительных таблицах можно определить условия, используя надлежащие массивные и строковые условия. Условия с использованием хэша предоставляют специальный синтаксис для определения условий в соединительных таблицах:

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)

Альтернативный и более чистый синтаксис для этого - вложенные хэш-условия:

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: { created_at: time_range })

Будут найдены все клиенты, имеющие созданные вчера заказы, снова используя выражение SQL BETWEEN.

left_outer_joins

Если хотите выбрать ряд записей, независимо от того, имеют ли они связанные записи, можно использовать метод left_outer_joins.

Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')

Который создаст:

SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id

Что означает: "возвратить всех авторов и количество их публикаций, независимо от того, имеются ли у них вообще публикации".

Нетерпеливая загрузка связей

Нетерпеливая загрузка - это механизм загрузки связанных записей объекта, возвращаемых Model.find, с использованием как можно меньшего количества запросов.

Проблема N + 1 запроса

Рассмотрим следующий код, который находит 10 клиентов и выводит их почтовые индексы:

clients = Client.limit(10)

clients.each do |client|
  puts client.address.postcode
end

На первый взгляд выглядит хорошо. Но проблема лежит в общем количестве выполненных запросов. Вышеупомянутый код выполняет 1 (чтобы найти 10 клиентов) + 10 (каждый на одного клиента для загрузки адреса) = итого 11 запросов.

Решение проблемы N + 1 запроса

Active Record позволяет заранее указать все связи, которые должны быть загружены. Это возможно с помощью указания метода includes на вызове Model.find. Посредством includes, Active Record обеспечивает то, что все указанные связи загружаются с использованием минимально возможного количества запросов.

Пересмотрев вышеупомянутую задачу, можно переписать Client.limit(10), чтобы нетерпеливо загрузить адреса:

clients = Client.includes(:address).limit(10)

clients.each do |client|
  puts client.address.postcode
end

Этот код выполнит всего 2 запроса, вместо 11 запросов из прошлого примера:

SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))

Нетерпеливая загрузка нескольких связей

Active Record позволяет нетерпеливо загружать любое количество связей в одном вызове Model.find с использованием массива, хэша или вложенного хэша массивов/хэшей с помощью метода includes.

Массив нескольких связей

Article.includes(:category, :comments)

Это загрузит все статьи и связанные категорию, и комментарии для каждой статьи.

Вложенный хэш связей

Category.includes(articles: [{ comments: :guest }, :tags]).find(1)

Вышеприведенный код находит категории с id 1 и нетерпеливо загружает все связанные статьи, теги и комментарии каждой статьи, а также гостей, связанных с комментариями.

Определение условий для нетерпеливой загрузки связей

Хотя Active Record и позволяет определить условия для нетерпеливой загрузки связей точно так же, как и в joins, рекомендуем использовать вместо этого joins.

Однако, если сделать так, то можно использовать where как обычно.

Article.includes(:comments).where("comments.visible" => true)

Это сгенерирует запрос с ограничением LEFT OUTER JOIN, в то время как метод joins сгенерировал бы его с использованием функции INNER JOIN.

  SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles"
    LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)

Если бы не было условия where, то сгенерировался бы обычный набор из двух запросов.

NOTE: Использование where подобным образом будет работать только, если передавать в него хэш. Для фрагментов SQL необходимо использовать references для принуждения соединения таблиц:

Article.includes(:comments).where("comments.visible = true").references(:comments)

Если, в случае с этим запросом includes, не будет ни одного комментария ни для одной статьи, все статьи все равно будут загружены. При использовании joins (INNER JOIN), соединительные условия должны соответствовать, иначе ни одной записи не будет возвращено.

NOTE: Если связь нетерпеливо загружена как часть join, любые поля из произвольного выражения select не будут присутствовать в загруженных моделях. Это так, потому что это избыточность, которая должна появиться или в родительской модели, или в дочерней.

(scopes) Скоупы

Скоупинг позволяет задавать часто используемые запросы, к которым можно обращаться как к вызовам метода в связанных объектах или моделях. С помощью этих скоупов можно использовать каждый ранее раскрытый метод, такой как where, joins и includes. Все методы скоупов возвращают объект ActiveRecord::Relation, который позволяет вызывать на нем дополнительные методы (такие как другие скоупы).

Для определения простого скоупа мы используем метод scope внутри класса, передав запрос, который хотим запустить при вызове этого скоупа:

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

Это в точности то же самое, что определение метода класса, и то, что именно вы используете, является вопросом профессионального предпочтения:

class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end

Скоупы также сцепляются с другими скоупами:

class Article < ApplicationRecord
  scope :published,               -> { where(published: true) }
  scope :published_and_commented, -> { published.where("comments_count > 0") }
end

Для вызова скоупа published, можно вызвать его либо на классе:

Article.published # => [опубликованные статьи]

Либо на связи, состоящей из объектов Article:

category = Category.first
category.articles.published # => [опубликованные статьи, принадлежащие этой категории]

Передача аргумента

Скоуп может принимать аргументы:

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) }
end

Вызывайте скоуп, как будто это метод класса:

Article.created_before(Time.zone.now)

Однако, это всего лишь дублирование функциональности, которая должна быть предоставлена методом класса.

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time)
  end
end

Использование метода класса - более предпочтительный способ принятию аргументов скоупом. Эти методы также будут доступны на связанных объектах:

category.articles.created_before(time)

Использование условий

Ваши скоупы могут использовать условия:

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end

Подобно остальным примерам, это ведет себя подобно методу класса.

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end

Однако, имеется одно важное предостережение: скоуп всегда должен возвращать объект ActiveRecord::Relation, даже если условие вычисляется false, в отличие от метода класса, возвращающего nil. Это может вызвать NoMethodError при сцеплении методов класса с условиями, если одно из условий вернет false.

(applying-a-default-scope) Применение скоупа по умолчанию

Если хотите, чтобы скоуп был применен ко всем запросам модели, можно использовать метод default_scope в самой модели.

class Client < ApplicationRecord
  default_scope { where("removed_at IS NULL") }
end

Когда запросы для этой модели будут выполняться, запрос SQL теперь будет выглядеть примерно так:

SELECT * FROM clients WHERE removed_at IS NULL

Если необходимо сделать более сложные вещи со скоупом по умолчанию, альтернативно его можно определить как метод класса:

class Client < ApplicationRecord
  def self.default_scope
    # Должен возвращать ActiveRecord::Relation.
  end
end

NOTE: default_scope также применяется при создании записи, когда аргументы скоупа передаются как Hash. Он не применяется при обновлении записи. То есть:

class Client < ApplicationRecord
  default_scope { where(active: true) }
end

Client.new          # => #<Client id: nil, active: true>
Client.unscoped.new # => #<Client id: nil, active: nil>

Имейте в виду, что когда передаются в формате Array, аргументы запроса default_scope не могут быть преобразованы в Hash для назначения атрибутов по умолчанию. То есть:

class Client < ApplicationRecord
  default_scope { where("active = ?", true) }
end

Client.new # => #<Client id: nil, active: nil>

Объединение скоупов

Подобно условиям where, скоупы объединяются с использованием AND.

class User < ApplicationRecord
  scope :active, -> { where state: 'active' }
  scope :inactive, -> { where state: 'inactive' }
end

User.active.inactive
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'

Можно комбинировать условия scope и where, и результирующий sql будет содержать все условия, соединенные с помощью AND.

User.active.where(state: 'finished')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'

Если необходимо, чтобы сработало только последнее условие where, тогда можно использовать Relation#merge.

User.active.merge(User.inactive)
# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'

Важным предостережением является то, что default_scope переопределяется условиями scope и where.

class User < ApplicationRecord
  default_scope { where state: 'pending' }
  scope :active, -> { where state: 'active' }
  scope :inactive, -> { where state: 'inactive' }
end

User.all
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'

User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'

User.where(state: 'inactive')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'

Как видите, default_scope объединяется как со scope, так и с where условиями.

Удаление всех скоупов

Если хотите удалить скоупы по какой-то причине, можете использовать метод unscoped. Это особенно полезно, если в модели определен default_scope, и он не должен быть применен для конкретно этого запроса.

Client.unscoped.load

Этот метод удаляет все скоупы и выполняет обычный запрос к таблице.

Client.unscoped.all
# SELECT "clients".* FROM "clients"

Client.where(published: false).unscoped.all
# SELECT "clients".* FROM "clients"

unscoped также может принимать блок.

Client.unscoped {
  Client.created_before(Time.zone.now)
}

(dynamic-finders) Динамический поиск

Для каждого поля (также называемого атрибутом), определенного в вашей таблице, Active Record предоставляет метод поиска. Например, если есть поле first_name в вашей модели Client, вы автоматически получаете find_by_first_name от Active Record. Если также есть поле locked в модели Client, вы также получаете find_by_locked метод.

Можете определить восклицательный знак (!) в конце динамического поиска, чтобы он вызвал ошибку ActiveRecord::RecordNotFound, если не возвратит ни одной записи, например так Client.find_by_name!("Ryan")

Если хотите искать и по first_name, и по locked, можете сцепить эти поиски вместе, просто написав "and" между полями, например, Client.find_by_first_name_and_locked("Ryan", true).

Enum

Макрос enum связывает числовую колонку с набором возможных значений.

class Book < ApplicationRecord
  enum availability: [:available, :unavailable]
end

Это автоматически создаст соответствующие скоупы для запроса модели. Также добавляются методы для перехода между состояниями и запроса текущего состояния.

# Оба примера ниже запрашивают только доступные книги.
Book.available
# или
Book.where(availability: :available)

book = Book.new(availability: :available)
book.available?   # => true
book.unavailable! # => true
book.available?   # => false

Полную документацию об enum можно прочитать в документации Rails API.

(Method Chaining) Цепочки методов

В Active Record есть полезный приём программирования Method Chaining, который позволяет нам комбинировать множество Active Record методов.

Можно сцепить несколько методов в единое выражение, если предыдущий вызываемый метод возвращает ActiveRecord::Relation, такие как all, where и joins. Методы, которые возвращают одиночный объект (смотрите раздел Получение одиночного объекта) должны вызываться в конце.

Ниже представлены несколько примеров. Это руководство не покрывает все возможности, а только некоторые, для ознакомления. Когда вызывается Active Record метод, запрос не сразу генерируется и отправляется в базу, это происходит только тогда, когда данные реально необходимы. Таким образом, каждый пример ниже генерирует только один запрос.

Получение отфильтрованных данных из нескольких таблиц

Person
  .select('people.id, people.name, comments.text')
  .joins(:comments)
  .where('comments.created_at > ?', 1.week.ago)

Результат должен быть примерно следующим:

SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
  ON comments.person_id = people.id
WHERE comments.created_at > '2015-01-01'

Получение определённых данных из нескольких таблиц

Person
  .select('people.id, people.name, companies.name')
  .joins(:company)
  .find_by('people.name' => 'John') # это должно быть в конце

Выражение выше, сгенерирует следующий SQL запрос:

SELECT people.id, people.name, companies.name
FROM people
INNER JOIN companies
  ON companies.person_id = people.id
WHERE people.name = 'John'
LIMIT 1

NOTE: Обратите внимание, что если запросу соответствует несколько записей, find_by вернет только первую запись и проигнорирует остальные (смотрите LIMIT 1 выше).

Поиск или создание нового объекта

Часто бывает, что вам нужно найти запись или создать ее, если она не существует. Вы можете сделать это с помощью методов find_or_create_by и find_or_create_by!.

find_or_create_by

Метод find_or_create_by проверяет, существует ли запись с определенными атрибутами. Если нет, то вызывается create. Давайте рассмотрим пример.

Предположим, вы хотите найти клиента по имени 'Andy', и, если такого нет, создать его. Это можно сделать, выполнив:

Client.find_or_create_by(first_name: 'Andy')
# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">

SQL, генерируемый этим методом, выглядит так:

SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT

find_or_create_by возвращает либо уже существующую запись, либо новую запись. В нашем случае, у нас еще нет клиента с именем Andy, поэтому запись будет создана и возвращена.

Новая запись может быть не сохранена в базу данных; это зависит от того, прошли валидации или нет (подобно create).

Предположим, мы хотим установить атрибут 'locked' как false, если создаем новую запись, но не хотим включать его в запрос. Таким образом, мы хотим найти клиента по имени "Andy" или, если этот клиент не существует, создать клиента по имени "Andy", который не заблокирован.

Этого можно достичь двумя способами. Первый - это использование create_with:

Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')

Второй способ - это использование блока:

Client.find_or_create_by(first_name: 'Andy') do |c|
  c.locked = false
end

Блок будет запущен только если клиент был создан. Во второй раз при запуске этого кода блок будет проигнорирован.

find_or_create_by!

Можно также использовать find_or_create_by!, чтобы вызвать исключение, если новая запись невалидна. Валидации не раскрываются в этом руководстве, но давайте на момент предположим, что вы временно добавили

validates :orders_count, presence: true

в модель Client. Если попытаетесь создать нового Client без передачи orders_count, запись будет невалидной и будет вызвано исключение:

Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank

find_or_initialize_by

Метод find_or_initialize_by работает похоже на find_or_create_by, но он вызывает не create, а new. Это означает, что новый экземпляр модели будет создан в памяти, но не будет сохранен в базу данных. Продолжая пример с find_or_create_by, теперь нам нужен клиент по имени 'Nick':

nick = Client.find_or_initialize_by(first_name: 'Nick')
# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">

nick.persisted?
# => false

nick.new_record?
# => true

Поскольку объект еще не сохранен в базу данных, сгенерированный SQL выглядит так:

SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1

Когда захотите сохранить его в базу данных, просто вызовите save:

nick.save
# => true

Поиск с помощью SQL

Если вы предпочитаете использовать собственные запросы SQL для поиска записей в таблице, можете использовать find_by_sql. Метод find_by_sql возвратит массив объектов, даже если лежащий в основе запрос вернет всего лишь одну запись. Например, можете запустить такой запрос:

Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER BY clients.created_at desc")
# =>  [
#   #<Client id: 1, first_name: "Lucas" >,
#   #<Client id: 2, first_name: "Jan" >,
#   ...
# ]

find_by_sql предоставляет простой способ создания произвольных запросов к базе данных и получения экземпляров объектов.

select_all

У find_by_sql есть близкий родственник, называемый connection#select_all. select_all получит объекты из базы данных, используя произвольный SQL, как и в find_by_sql, но не создаст их экземпляры. Вместо этого, вы получите массив хэшей, где каждый хэш указывает на запись.

Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
# => [
#   {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
#   {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
# ]

pluck

pluck может быть использован для запроса с одним или несколькими столбцами из таблицы, лежащей в основе модели. Он принимает список имен столбцов как аргумент и возвращает массив значений определенных столбцов соответствующего типа данных.

Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]

Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']

Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]

pluck позволяет заменить такой код:

Client.select(:id).map { |c| c.id }
# или
Client.select(:id).map(&:id)
# или
Client.select(:id, :name).map { |c| [c.id, c.name] }

на:

Client.pluck(:id)
# или
Client.pluck(:id, :name)

В отличие от select, pluck непосредственно конвертирует результат запроса в массив Ruby, без создания объектов ActiveRecord. Это может означать лучшую производительность для больших или часто используемых запросов. Однако, любые переопределения методов в модели будут недоступны. Например:

class Client < ApplicationRecord
  def name
    "I am #{super}"
  end
end

Client.select(:name).map &:name
# => ["I am David", "I am Jeremy", "I am Jose"]

Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]

Более того, в отличие от select и других скоупов Relation, pluck вызывает немедленный запрос, и поэтому не может быть соединен с любыми последующими скоупами, хотя он может работать со скоупами, подключенными ранее:

Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

Client.limit(1).pluck(:name)
# => ["David"]

ids

ids может быть использован для сбора всех ID для relation, используя первичный ключ таблицы.

Person.ids
# SELECT id FROM people
class Person < ApplicationRecord
  self.primary_key = "person_id"
end

Person.ids
# SELECT person_id FROM people

Существование объектов

Если вы просто хотите проверить существование объекта, есть метод, называемый exists?. Этот метод запрашивает базу данных, используя тот же запрос, что и find, но вместо возврата объекта или коллекции объектов, он возвращает или true, или false.

Client.exists?(1)

Метод exists? также принимает несколько значений, при этом возвращает true, если хотя бы одна из этих записей существует.

Client.exists?(id: [1,2,3])
# или
Client.exists?(name: ['John', 'Sergei'])

Даже возможно использовать exists? без аргументов на модели или relation:

Client.where(first_name: 'Ryan').exists?

Пример выше вернет true, если есть хотя бы один клиент с first_name 'Ryan', и false в противном случае.

Client.exists?

Это возвратит false, если таблица clients пустая, и true в противном случае.

Для проверки на существование также можно использовать any? и many? на модели или relation.

# на модели
Article.any?
Article.many?

# на именованном скоупе
Article.recent.any?
Article.recent.many?

# на relation
Article.where(published: true).any?
Article.where(published: true).many?

# на связи
Article.first.categories.any?
Article.first.categories.many?

Вычисления

Этот раздел использует count для примера в этой преамбуле, но описанные опции применяются ко всем подразделам.

Все методы вычисления работают прямо на модели:

Client.count
# SELECT count(*) AS count_all FROM clients

Или на relation:

Client.where(first_name: 'Ryan').count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')

Можно также использовать различные методы поиска на relation для выполнения сложных вычислений:

Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count

Что выполнит:

SELECT count(DISTINCT clients.id) AS count_all FROM clients
  LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE
  (clients.first_name = 'Ryan' AND orders.status = 'received')

Количество

Если хотите увидеть, сколько записей есть в таблице модели, можете вызвать Client.count, и он возвратит число. Если хотите быть более определенным и найти всех клиентов с присутствующим в базе данных возрастом, используйте Client.count(:age).

Про опции смотрите выше в разделе Вычисления.

Среднее

Если хотите увидеть среднее значение определенного показателя в одной из ваших таблиц, можно вызвать метод average для класса, относящегося к таблице. Вызов этого метода выглядит так:

Client.average("orders_count")

Это возвратит число (возможно, с плавающей запятой, такое как 3.14159265), представляющее среднее значение поля.

Про опции смотрите выше в разделе Вычисления.

Минимум

Если хотите найти минимальное значение поля в таблице, можете вызвать метод minimum для класса, относящегося к таблице. Вызов этого метода выглядит так:

Client.minimum("age")

Про опции смотрите выше в разделе Вычисления.

Максимум

Если хотите найти максимальное значение поля в таблице, можете вызвать метод maximum для класса, относящегося к таблице. Вызов этого метода выглядит так:

Client.maximum("age")

Про опции смотрите выше в разделе Вычисления.

Сумма

Если хотите найти сумму полей для всех записей в таблице, можете вызвать метод sum для класса, относящегося к таблице. Вызов этого метода выглядит так:

Client.sum("orders_count")

Про опции смотрите выше в разделе Вычисления.

Запуск EXPLAIN

Можно запустить EXPLAIN на запросах, вызываемых в relations. Например,

User.where(id: 1).joins(:articles).explain

может выдать

EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+----------+-------+---------------+
| id | select_type | table    | type  | possible_keys |
+----+-------------+----------+-------+---------------+
|  1 | SIMPLE      | users    | const | PRIMARY       |
|  1 | SIMPLE      | articles | ALL   | NULL          |
+----+-------------+----------+-------+---------------+
+---------+---------+-------+------+-------------+
| key     | key_len | ref   | rows | Extra       |
+---------+---------+-------+------+-------------+
| PRIMARY | 4       | const |    1 |             |
| NULL    | NULL    | NULL  |    1 | Using where |
+---------+---------+-------+------+-------------+

2 rows in set (0.00 sec)

для MySQL и MariaDB.

Active Record применяет красивое форматирование, эмулирующее работу соответствующей оболочки базы данных. Таким образом, запуск того же запроса с адаптером PostgreSQL выдаст вместо этого

EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1
                                  QUERY PLAN
------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=0.00..37.24 rows=8 width=0)
   Join Filter: (articles.user_id = users.id)
   ->  Index Scan using users_pkey on users  (cost=0.00..8.27 rows=1 width=4)
         Index Cond: (id = 1)
   ->  Seq Scan on articles  (cost=0.00..28.88 rows=8 width=4)
         Filter: (articles.user_id = 1)
(6 rows)

Нетерпеливая загрузка может вызвать более одного запроса за раз, и некоторым запросам могут потребоваться результаты предыдущих. Поэтому explain фактически выполняет запрос, а затем запрашивает планы запросов. Например,

User.where(id: 1).includes(:articles).explain

выдаст

EXPLAIN for: SELECT `users`.* FROM `users`  WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+
| id | select_type | table | type  | possible_keys |
+----+-------------+-------+-------+---------------+
|  1 | SIMPLE      | users | const | PRIMARY       |
+----+-------------+-------+-------+---------------+
+---------+---------+-------+------+-------+
| key     | key_len | ref   | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4       | const |    1 |       |
+---------+---------+-------+------+-------+

1 row in set (0.00 sec)

EXPLAIN for: SELECT `articles`.* FROM `articles`  WHERE `articles`.`user_id` IN (1)
+----+-------------+----------+------+---------------+
| id | select_type | table    | type | possible_keys |
+----+-------------+----------+------+---------------+
|  1 | SIMPLE      | articles | ALL  | NULL          |
+----+-------------+----------+------+---------------+
+------+---------+------+------+-------------+
| key  | key_len | ref  | rows | Extra       |
+------+---------+------+------+-------------+
| NULL | NULL    | NULL |    1 | Using where |
+------+---------+------+------+-------------+


1 row in set (0.00 sec)

для MySQL и MariaDB.

Интерпретация EXPLAIN

Интерпретация результатов EXPLAIN находится за рамками этого руководства. Может быть полезной следующая информация:

results matching ""

    No results matching ""