Статьи

Запросы в Rails, часть 2

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

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

Большинство из них действительно просты, но синтаксис немного странный, если вы только начали программировать, особенно в Ruby. Держись, это не ракетостроение!

  • Включает и нетерпеливо загружается
  • Столы
  • Нетерпеливая загрузка
  • Области применения
  • Скопления
  • Динамические Искатели
  • Конкретные поля
  • Пользовательский SQL

Эти запросы включают в себя более одной таблицы базы данных для работы и могут быть наиболее важными из этой статьи. Это сводится к следующему: вместо того, чтобы делать несколько запросов информации, которая распределена по нескольким таблицам, includes попытки свести их к минимуму. Ключевая концепция, стоящая за этим, называется «готовая загрузка» и означает, что мы загружаем связанные объекты, когда делаем поиск.

Если бы мы сделали это, перебирая коллекцию объектов и пытаясь получить доступ к связанным с ней записям из другой таблицы, мы столкнулись бы с проблемой, которая называется «проблема запроса N + 1». Например, для каждого agent.handler в коллекции агентов мы agent.handler отдельные запросы как для агентов, так и для их обработчиков. Это то, что мы должны избегать, поскольку это не масштабируется вообще. Вместо этого мы делаем следующее:

1
agents = Agent.includes(:handlers)

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

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)
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’ })
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’ })
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
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
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
1
SELECT COUNT(*) FROM «missions»
  • average
1
2
3
Agent.average(:number_of_gadgets).to_f
 
# => 3.5
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
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)
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)
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)
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)
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 }
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)
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)
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»>, …]>
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’>, … ]>
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 любом случае будет предоставлен вам автоматически, вы можете запросить его, не выбирая его.

Наконец, что не менее 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»)

Неудивительно, что это приводит к:

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 значительно возрастет, если вы будете работать над своими первыми реальными проектами и вам нужно будет создавать свои собственные пользовательские запросы. Если вы все еще немного стесняетесь этой темы, я бы сказал, просто повеселиться с ней — это действительно не ракетостроение!