Во второй статье мы углубимся в запросы Active Record в Rails. Если вы все еще не знакомы с SQL, я добавлю примеры, которые достаточно просты, чтобы вы могли пометить их и немного подобрать синтаксис.
При этом, безусловно, будет полезно, если вы пройдете краткое руководство по SQL, прежде чем вернуться к чтению. В противном случае не торопитесь, чтобы понять SQL-запросы, которые мы использовали, и я надеюсь, что к концу этой серии это больше не будет пугать.
Большинство из них действительно просты, но синтаксис немного странный, если вы только начали программировать, особенно в Ruby. Держись, это не ракетостроение!
темы
- Включает и нетерпеливо загружается
- Столы
- Нетерпеливая загрузка
- Области применения
- Скопления
- Динамические Искатели
- Конкретные поля
- Пользовательский SQL
Включает и нетерпеливо загружается
Эти запросы включают в себя более одной таблицы базы данных для работы и могут быть наиболее важными из этой статьи. Это сводится к следующему: вместо того, чтобы делать несколько запросов информации, которая распределена по нескольким таблицам, includes
попытки свести их к минимуму. Ключевая концепция, стоящая за этим, называется «готовая загрузка» и означает, что мы загружаем связанные объекты, когда делаем поиск.
Если бы мы сделали это, перебирая коллекцию объектов и пытаясь получить доступ к связанным с ней записям из другой таблицы, мы столкнулись бы с проблемой, которая называется «проблема запроса N + 1». Например, для каждого agent.handler
в коллекции агентов мы agent.handler
отдельные запросы как для агентов, так и для их обработчиков. Это то, что мы должны избегать, поскольку это не масштабируется вообще. Вместо этого мы делаем следующее:
Рельсы
1
|
agents = Agent.includes(:handlers)
|
Если теперь мы проведем итерацию по такому набору агентов (не считая того, что мы пока не ограничили число возвращаемых записей), мы получим два запроса вместо, возможно, gazillion.
SQL
1
2
|
SELECT «agents».* FROM «agents»
SELECT «handlers».* FROM «handlers» WHERE «handlers».»id» IN (1, 2)
|
Этот один агент в списке имеет два обработчика, и когда мы теперь запрашиваем объект агента для его обработчиков, не нужно запускать дополнительные запросы к базе данных. Конечно, мы можем пойти еще дальше и загрузить несколько связанных табличных записей. Если по какой-либо причине нам нужно было загружать не только обработчики, но и связанные с ним миссии агента, мы могли бы использовать такие включения.
Рельсы
1
|
agents = Agent.includes(:handlers, :mission)
|
Просто! Только будьте осторожны с использованием единственного и множественного числа версий для включений. Они зависят от ваших модельных ассоциаций. Ассоциация has_many
использует множественное число, в то время как для belongs_to
или has_one
требуется единственная версия. Если вам нужно, вы также можете воспользоваться предложением where
для указания дополнительных условий, но предпочтительным способом задания условий для связанных таблиц, которые активно загружаются, является использование вместо этого соединений.
Следует иметь в виду, что данные, которые будут добавлены, будут полностью возвращены в Active Record, которая, в свою очередь, создает объекты Ruby, включающие эти атрибуты. Это отличается от «простого» объединения данных, когда вы получите виртуальный результат, который вы можете использовать, например, для расчетов, и он будет занимать меньше памяти, чем включает.
Столы
Объединение таблиц — это еще один инструмент, позволяющий избежать отправки слишком большого количества ненужных запросов по конвейеру. Распространенный сценарий — объединение двух таблиц одним запросом, который возвращает какую-то комбинированную запись. joins
— это еще один метод поиска Active Record, который позволяет вам — в терминах SQL — объединять таблицы. Эти запросы могут возвращать записи, объединенные из нескольких таблиц, и вы получаете виртуальную таблицу, которая объединяет записи из этих таблиц. Это очень приятно, когда вы сравниваете это с запуском всевозможных запросов для каждой таблицы. Существует несколько различных видов перекрытия данных, которые вы можете получить с помощью этого подхода.
Внутреннее объединение — это способ действия по умолчанию для joins
. Это соответствует всем результатам, которые соответствуют определенному идентификатору и его представлению в качестве внешнего ключа от другого объекта или таблицы. В приведенном ниже примере просто: дайте мне все миссии, в которых id
миссии отображается как mission_id
в таблице агента. "agents"."mission_id" = "missions"."id"
. Внутренние объединения исключают отношения, которые не существуют.
Рельсы
1
|
Mission.joins(:agents)
|
SQL
1
|
SELECT «missions».* FROM «missions» INNER JOIN «agents» ON «agents».»mission_id» = «mission».»id»
|
Таким образом, мы сопоставляем миссии и сопровождающих их агентов — в одном запросе! Конечно, мы могли бы сначала получить задания, перебирать их по очереди и запрашивать их агентов. Но тогда мы вернемся к нашей ужасной «проблеме запросов N + 1». Нет, спасибо!
Что также хорошо в этом подходе, так это то, что мы не получим ни одного случая с внутренними объединениями; мы получаем только те записи, которые соответствуют их идентификаторам внешним ключам в связанных таблицах. Например, если нам нужно найти миссии, в которых нет агентов, нам потребуется внешнее соединение. Поскольку в настоящее время это требует написания собственного SQL-кода OUTER JOIN
, мы рассмотрим это в последней статье. Возвращаясь к стандартным объединениям, вы, конечно же, также можете объединять несколько связанных таблиц.
Рельсы
1
|
Mission.joins(:agents, :expenses, :handlers)
|
И вы можете добавить к ним некоторые условия where
чтобы указать ваши искатели еще больше. Ниже мы ищем только миссии, которые выполняются Джеймсом Бондом, и только агентов, которые принадлежат миссии «Лунный гонщик» во втором примере.
1
|
Mission.joins(:agents).where( agents: { name: ‘James Bond’ })
|
SQL
1
|
SELECT «missions».* FROM «missions» INNER JOIN «agents» ON «agents».»mission_id» = «missions».»id» WHERE «agents».»name» = ?
|
Рельсы
1
|
Agent.joins(:mission).where( missions: { mission_name: ‘Moonraker’ })
|
SQL
1
|
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»mission_name» = ?
|
С joins
вы также должны обращать внимание на единственное и множественное использование ваших модельных ассоциаций. Поскольку у моего класса Mission
has_many :agents
, мы можем использовать множественное число. С другой стороны, для класса Agent
belongs_to :mission
, только единственная версия работает без взрыва. Важная маленькая деталь: часть where
проще. Поскольку вы сканируете несколько строк в таблице, которые удовлетворяют определенному условию, форма множественного числа всегда имеет смысл.
Области применения
Области — это удобный способ для извлечения общих потребностей запросов в свои хорошо названные методы. Таким образом, их будет немного легче обойти, а также, возможно, их будет легче понять, если другие будут работать с вашим кодом или вам нужно будет вернуться к определенным запросам в будущем. Вы можете определить их для отдельных моделей, но также использовать их для их ассоциаций.
Небо это предел на самом деле — joins
, includes
, и where
все честные игры! Так как области видимости также возвращают объекты ActiveRecord::Relations
, их можно связывать и вызывать другие области поверх них без колебаний. Извлечение подобных областей и объединение их в цепочку для более сложных запросов очень удобно и делает более длинные из них более читабельными. Области определяются через синтаксис «стабильная лямбда»:
Рельсы
1
2
3
4
5
6
7
|
class Mission < ActiveRecord::Base
has_many: agents
scope :successful, -> { where(mission_complete: true) }
end
Mission.successful
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class Agent < ActiveRecord::Base
belongs_to :mission
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
scope :womanizer, -> { where(womanizer: true) }
scope :gambler, -> { where(gambler: true) }
end
# Agent.gambler
# Agent.womanizer
# Agent.licenced_to_kill
# Agent.womanizer.gambler
Agent.licenced_to_kill.womanizer.gambler
|
SQL
1
|
SELECT «agents».* FROM «agents» WHERE «agents».»licence_to_kill» = ?
|
Как вы можете видеть из приведенного выше примера, найти Джеймса Бонда гораздо приятнее, когда вы можете просто объединить прицелы. Таким образом, вы можете смешивать и сопоставлять различные запросы и оставаться сухим в то же время. Если вам нужны области через ассоциации, они также в вашем распоряжении:
1
|
Mission.last.agents.licenced_to_kill.womanizer.gambler
|
1
2
|
SELECT «missions».* FROM «missions» ORDER BY «missions».»id» DESC LIMIT 1
SELECT «agents».* FROM «agents» WHERE «agents».»mission_id» = ?
|
Вы также можете переопределить default_scope
для Mission.all
когда вы смотрите на что-то вроде Mission.all
.
1
2
3
4
5
|
class Mission < ActiveRecord::Base
default_scope { where status: «In progress» }
end
Mission.all
|
SQL
1
|
SELECT «missions».* FROM «missions» WHERE «missions».»status» = ?
|
Скопления
Этот раздел не настолько продвинут с точки зрения вовлеченного понимания, но они вам понадобятся чаще, чем не в сценариях, которые можно считать немного более продвинутыми, чем ваш средний искатель, например .all
, .first
, .find_by_id
или что-то еще. Например, фильтрация на основе базовых вычислений, скорее всего, новички не сразу связываются. Что мы смотрим именно здесь?
-
sum
-
count
-
minimum
-
maximum
-
average
Легко peasy, верно? Круто то, что вместо циклического повторения возвращенной коллекции объектов для выполнения этих вычислений мы можем позволить Active Record выполнять всю эту работу за нас и возвращать эти результаты с запросами — предпочтительно в одном запросе. Хорошо, а?
-
count
Рельсы
1
2
3
|
Mission.count
# => 24
|
SQL
1
|
SELECT COUNT(*) FROM «missions»
|
-
average
Рельсы
1
2
3
|
Agent.average(:number_of_gadgets).to_f
# => 3.5
|
SQL
1
|
SELECT AVG(«agents».»number_of_gadgets») FROM «agents»
|
Поскольку теперь мы знаем, как мы можем использовать joins
, мы можем сделать еще один шаг вперед и запросить, например, среднее количество гаджетов, которые имеют агенты в конкретной миссии.
Рельсы
1
2
3
|
Agent.joins(:mission).where(missions: {name: ‘Moonraker’}).average(:number_of_gadgets).to_f
# => 3.4
|
SQL
1
|
SELECT AVG(«agents».»number_of_gadgets») FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»name» = ?
|
Группировка этого среднего количества гаджетов по названиям миссий в этот момент становится тривиальной. Подробнее о группировке смотрите ниже:
Рельсы
1
|
Agent.joins(:mission).group(‘missions.name’).average(:number_of_gadgets)
|
SQL
1
|
SELECT AVG(«agents».»number_of_gadgets») AS average_number_of_gadgets, missions.name AS missions_name FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» GROUP BY missions.name
|
-
sum
Рельсы
1
2
3
4
5
|
Agent.sum(:number_of_gadgets)
Agent.where(licence_to_kill: true).sum(:number_of_gadgets)
Agent.where.not(licence_to_kill: true).sum(:number_of_gadgets)
|
SQL
1
2
3
4
5
|
SELECT SUM(«agents».»number_of_gadgets») FROM «agents»
SELECT SUM(«agents».»number_of_gadgets») FROM «agents» WHERE «agents».»licence_to_kill» = ?
SELECT SUM(«agents».»number_of_gadgets») FROM «agents» WHERE («agents».»licence_to_kill» != ?) [[«licence_to_kill», «t»]]
|
-
maximum
Рельсы
1
2
3
|
Agent.maximum(:number_of_gadgets)
Agent.where(licence_to_kill: true).maximum(:number_of_gadgets)
|
SQL
1
2
3
|
SELECT MAX(«agents».»number_of_gadgets») FROM «agents»
SELECT MAX(«agents».»number_of_gadgets») FROM «agents» WHERE «agents».»licence_to_kill» = ?
|
-
minimum
Рельсы
1
2
3
|
Agent.minimum(:iq)
Agent.where(licence_to_kill: true).minimum(:iq)
|
SQL
1
2
3
|
SELECT MIN(«agents».»iq») FROM «agents»
SELECT MIN(«agents».»iq») FROM «agents» WHERE «agents».»licence_to_kill» = ?
|
Внимание!
Все эти методы агрегирования не позволяют вам цепляться за другие вещи — они являются терминальными. Порядок важно делать расчеты. Мы не получаем объект ActiveRecord::Relation
от этих операций, что приводит к остановке музыки в этот момент — вместо этого мы получаем хеш или числа. Примеры ниже не будут работать:
Рельсы
1
2
3
4
5
|
Agent.maximum(:number_of_gadgets).where(licence_to_kill: true)
Agent.sum(:number_of_gadgets).where.not(licence_to_kill: true)
Agent.joins(:mission).average(:number_of_gadgets).group(‘missions.name’)
|
Сгруппированные
Если вы хотите, чтобы вычисления разбивались и сортировались на логические группы, вы должны использовать предложение GROUP
, а не делать это в Ruby. Под этим я подразумеваю, что вам следует избегать итерации по группе, которая потенциально генерирует тонны запросов.
Рельсы
1
2
3
|
Agent.joins(:mission).group(‘missions.name’).average(:number_of_gadgets)
# => { «Moonraker»=> 4.4, «Octopussy»=> 4.9 }
|
SQL
1
|
SELECT AVG(«agents».»number_of_gadgets») AS average_number_of_gadgets, missions.name AS missions_name FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» GROUP BY missions.name
|
В этом примере выполняется поиск всех агентов, сгруппированных в определенную миссию, и возвращает хэш с рассчитанным средним числом гаджетов в качестве значений — в одном запросе! Ага! То же самое относится и к другим расчетам, конечно. В этом случае действительно имеет смысл позволить SQL работать. Количество запросов, которые мы запускаем для этих агрегатов, слишком важно.
Динамические Искатели
Для каждого атрибута в ваших моделях, например, name
, email_address
, email_address
и т. Д., Active Record позволяет вам использовать очень удобочитаемые методы поиска, которые создаются для вас динамически. Звучит загадочно, я знаю, но это ничего не значит, кроме find_by_id
или find_by_favorite_gadget
. Часть find_by
является стандартной, и Active Record просто подбирает для вас имя атрибута. Вы даже можете добавить !
если вы хотите, чтобы этот искатель выдал ошибку, если ничего не может быть найдено. Беда в том, что вы можете даже объединить эти методы динамического поиска вместе. Именно так:
Рельсы
1
2
3
|
Agent.find_by_name(‘James Bond’)
Agent.find_by_name_and_licence_to_kill(‘James Bond’, true)
|
SQL
1
2
3
|
SELECT «agents».* FROM «agents» WHERE «agents».»name» = ?
SELECT «agents».* FROM «agents» WHERE «agents».»name» = ?
|
Конечно, вы можете сойти с ума с этим, но я думаю, что он теряет свое очарование и полезность, если вы выходите за рамки двух атрибутов:
Рельсы
1
|
Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets(‘James Bond’, true, true, true, 3)
|
SQL
1
|
SELECT «agents».* FROM «agents» WHERE «agents».»name» = ?
|
В этом примере, тем не менее, приятно видеть, как это работает под капотом. Каждый новый _and_
добавляет оператор SQL AND
чтобы логически связать атрибуты вместе. В целом, основным преимуществом динамических искателей является удобочитаемость — однако использование слишком большого количества динамических атрибутов быстро теряет это преимущество. Я редко использую это, может быть, в основном, когда я играю в консоли, но определенно приятно знать, что Rails предлагает этот изящный маленький обман.
Конкретные поля
Active Record дает вам возможность возвращать объекты, которые немного больше сфокусированы на атрибутах, которые они несут. Обычно, если не указано иное, запрос запрашивает все поля в строке с помощью *
( SELECT "agents".*
), А затем Active Record создает объекты Ruby с полным набором атрибутов. Однако вы можете select
только определенные поля, которые должны быть возвращены запросом, и ограничить количество атрибутов, которые ваши объекты Ruby должны «переносить».
Рельсы
1
2
3
|
Agent.select(«name»)
=> #<ActiveRecord::Relation [#<Agent 7: nil, name: «James Bond»>, #<Agent id: 8, name: «Q»>, …]>
|
SQL
1
|
SELECT «agents».»name» FROM «agents»
|
Рельсы
1
2
3
|
Agent.select(«number, favorite_gadget»)
=> #<ActiveRecord::Relation [#<Agent id: 7, number: ‘007’, favorite_gadget: ‘Walther PPK’>, #<Agent id: 8, name: «Q», favorite_gadget: ‘Broom Radio’>, … ]>
|
SQL
1
|
SELECT «agents».»number», «agents».»favorite_gadget» FROM «agents»
|
Как вы можете видеть, возвращенные объекты будут иметь только выбранные атрибуты и, конечно, их идентификаторы — это дано для любого объекта. Не имеет значения, используете ли вы строки, как указано выше, или символы — запрос будет таким же.
Рельсы
1
2
3
|
Agent.select(:number_of_kills)
Agent.select(:name, :licence_to_kill)
|
Предупреждение: если вы попытаетесь получить доступ к атрибутам объекта, который вы не выбрали в своих запросах, вы получите MissingAttributeError
. Поскольку id
любом случае будет предоставлен вам автоматически, вы можете запросить его, не выбирая его.
Пользовательский SQL
Наконец, что не менее find_by_sql
, вы можете написать свой собственный SQL через find_by_sql
. Если вы достаточно уверены в своем собственном SQL-Fu и нуждаетесь в некоторых пользовательских вызовах базы данных, этот метод может иногда оказаться очень полезным. Но это другая история. Только не забудьте сначала проверить методы-обертки Active Record и не изобретать велосипед, на котором Rails пытается встретить вас более чем на полпути.
Рельсы
1
2
3
|
Agent.find_by_sql(«SELECT * FROM agents»)
Agent.find_by_sql(«SELECT name, licence_to_kill FROM agents»)
|
Неудивительно, что это приводит к:
SQL
1
2
3
|
SELECT * FROM agents
SELECT name, licence_to_kill FROM agents
|
Так как области видимости и ваши собственные методы класса могут использоваться взаимозаменяемо для ваших собственных нужд поиска, мы можем сделать еще один шаг для более сложных запросов SQL.
Рельсы
01
02
03
04
05
06
07
08
09
10
11
12
|
class Agent < ActiveRecord::Base
…
def self.find_agent_names
query = <<-SQL
SELECT name
FROM agents
SQL
self.find_by_sql(query)
end
end
|
Мы можем написать методы класса, которые инкапсулируют SQL в документе Here. Это позволяет нам писать многострочные строки в очень удобочитаемом виде, а затем сохранять эту строку SQL внутри переменной, которую мы можем использовать повторно и передать в find_by_sql
. Таким образом, мы не добавляем тонны кода запроса в вызов метода. Если у вас есть несколько мест для использования этого запроса, это также СУХОЙ.
Так как предполагается, что это удобно для новичков, а не учебник по SQL, я сохранил пример очень минималистично по причине. Тем не менее, техника для более сложных запросов совершенно одинакова. Легко представить себе собственный SQL-запрос, который выходит за рамки десяти строк кода.
Сходи с ума, сколько нужно — разумно! Это может быть спасение жизни. Несколько слов о синтаксисе здесь. Часть SQL
— это просто идентификатор, обозначающий начало и конец строки. Бьюсь об заклад, вам не понадобится этот метод слишком много — будем надеяться! Он определенно имеет свое место, и земля Rails не была бы такой же без него — в тех редких случаях, когда вы абсолютно точно захотите настроить свой собственный SQL с ним.
Последние мысли
Надеюсь, вам стало удобнее писать запросы и читать страшный старый сырой SQL. Большинство тем, которые мы рассмотрели в этой статье, имеют важное значение для написания запросов, касающихся более сложной бизнес-логики. Не торопитесь, чтобы понять это, и немного поэкспериментируйте с запросами в консоли.
Я уверен, что когда вы оставите учебные материалы позади, рано или поздно ваш кредит Rails значительно возрастет, если вы будете работать над своими первыми реальными проектами и вам нужно будет создавать свои собственные пользовательские запросы. Если вы все еще немного стесняетесь этой темы, я бы сказал, просто повеселиться с ней — это действительно не ракетостроение!