Статьи

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

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

1
Mission.last.agents.where(name: ‘James Bond’)

Если вы новичок в запросах Active Record и SQL, я рекомендую вам взглянуть на мои предыдущие две статьи, прежде чем продолжить. Это может быть трудно проглотить без знания того, что я до сих пор строил. Конечно, до вас. С другой стороны, эта статья не будет такой же длинной, как другие, если вы просто захотите взглянуть на эти слегка продвинутые варианты использования. Давайте копаться в!

  • Области применения и ассоциации
  • Стройнее присоединяется
  • Объединить
  • имеет много
  • Пользовательские соединения

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class Agent < ActiveRecord::Base
 
  belongs_to :mission
 
  scope :find_bond, -> { where(name: ‘James Bond’) }
  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
  scope :womanizer, -> { where(womanizer: true) }
  scope :gambler, -> { where(gambler: true) }
end
 
# => Agent.find_bond
# => Agent.licenced_to_kill
# => Agent.womanizer
# => Agent.gambler
 
# => Mission.last.agents.find_bond
# => Mission.last.agents.licenced_to_kill
# => Mission.last.agents.womanizer
# => Mission.last.agents.gambler
 
# => Agent.licenced_to_kill.womanizer.gambler
# => Mission.last.agents.womanizer.gambler.licenced_to_kill

Таким образом, вы также можете упаковать их в свои собственные методы класса и покончить с этим. Я думаю, что прицелы не сомнительны или что-то в этом роде — хотя люди называют их немного волшебными здесь и там — но поскольку методы класса достигают одного и того же, я бы выбрал это.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Agent < ActiveRecord::Base
 
  belongs_to :mission
 
  def self.find_bond
    where(name: ‘James Bond’)
  end
 
  def self.licenced_to_kill
    where(licence_to_kill: true)
  end
 
  def self.womanizer
    where(womanizer: true)
  end
 
  def self.gambler
    where(gambler: true)
  end
end
 
# => Agent.find_bond
# => Agent.licenced_to_kill
# => Agent.womanizer
# => Agent.gambler
 
# => Mission.last.agents.find_bond
# => Mission.last.agents.licenced_to_kill
# => Mission.last.agents.womanizer
# => Mission.last.agents.gambler
 
# => Agent.licenced_to_kill.womanizer.gambler
# => Mission.last.agents.womanizer.gambler.licenced_to_kill

Эти методы класса читают то же самое, и вам не нужно наносить удар лямбда-сообщения кому-либо. Все, что работает лучше для вас или вашей команды; Вам решать, какой API вы хотите использовать. Только не смешивайте и не сочетайте их — придерживайтесь одного выбора! Обе версии позволяют легко связать эти методы внутри другого метода класса, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class Agent < ActiveRecord::Base
 
  belongs_to :mission
 
  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
  scope :womanizer, -> { where(womanizer: true) }
 
  def self.find_licenced_to_kill_womanizer
    womanizer.licenced_to_kill
  end
end
 
# => Agent.find_licenced_to_kill_womanizer
# => Mission.last.agents.find_licenced_to_kill_womanizer
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Agent < ActiveRecord::Base
 
  belongs_to :mission
 
  def self.licenced_to_kill
    where(licence_to_kill: true)
  end
 
  def self.womanizer
    where(womanizer: true)
  end
 
  def self.find_licenced_to_kill_womanizer
    womanizer.licenced_to_kill
  end
end
 
# => Agent.find_licenced_to_kill_womanizer
# => Mission.last.agents.find_licenced_to_kill_womanizer

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

Это довольно круто и легко читаемо с использованием более коротких цепочек методов. Однако остерегайтесь слишком тесного соединения этих моделей.

1
2
3
4
5
6
7
8
class Mission < ActiveRecord::Base
 
  has_many :double_o_agents,
    -> { where(licence_to_kill: true) },
    class_name: «Agent»
end
 
# => Mission.double_o_agents

Скажи мне, что это не круто как-то! Это не для повседневного использования, но дурак, чтобы я думаю. Так что здесь Mission может «запросить» только агентов, которые имеют лицензию на убийство.

Несколько слов о синтаксисе, поскольку мы отклонились от соглашений об именах и использовали нечто более выразительное, например double_o_agents . Нам нужно упомянуть имя класса, чтобы не перепутать Rails, который в противном случае может ожидать поиск класса DoubleOAgent . Вы можете, конечно, иметь как Agent ассоциации, так и обычные, и Rails не будет жаловаться.

01
02
03
04
05
06
07
08
09
10
11
class Mission < ActiveRecord::Base
 
  has_many :agents
   
  has_many :double_o__agents,
    -> { where(licence_to_kill: true) },
    class_name: «Agent»
end
 
# => Mission.agents
# => Mission.double_o_agents

Когда вы запрашиваете в базе данных записи и вам не нужны все данные, вы можете указать, что именно вы хотите вернуть. Почему? Поскольку данные, возвращаемые в Active Record, в конечном итоге будут встроены в новые объекты Ruby. Давайте рассмотрим одну простую стратегию, чтобы избежать раздувания памяти в вашем приложении Rails:

1
2
3
4
5
6
7
class Mission < ActiveRecord::Base
  has_many :agents
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
end
1
Agent.all.joins(:mission)
1
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id»

Таким образом, этот запрос возвращает список агентов с заданием из базы данных в Active Record, которая, в свою очередь, намеревается создать из нее объекты Ruby. Данные mission доступны, поскольку данные из этих строк объединены в строки данных агента. Это означает, что присоединенные данные доступны во время запроса, но не возвращаются в Active Record. Таким образом, у вас будут эти данные для выполнения расчетов, например.

Это особенно круто, потому что вы можете использовать данные, которые также не отправляются обратно в ваше приложение. Меньшее количество атрибутов, которые необходимо встроить в объекты Ruby, которые занимают память, может стать большой победой. В общем, подумайте об отправке только тех необходимых строк и столбцов, которые вам нужны. Таким образом, вы можете избежать вздутия.

1
Agent.all.joins(:mission).where(missions: { objective: «Saving the world» })

Просто немного о синтаксисе здесь: поскольку мы не запрашиваем таблицу Agent через where , а объединяем таблицу :mission , нам нужно указать, что мы ищем конкретные missions в нашем WHERE .

1
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»objective» = ?

Использование include в этом случае также вернет миссии в Active Record для активной загрузки и использования памяти для создания объектов Ruby.

merge оказывается полезным, например, когда мы хотим объединить запрос к агентам и связанным с ними миссиям, которые имеют определенную область действия, определенную вами. Мы можем взять два объекта ActiveRecord::Relation и объединить их условия. Конечно, не важно, но merge полезно, если вы хотите использовать определенную область при использовании ассоциации.

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

01
02
03
04
05
06
07
08
09
10
11
12
class Mission < ActiveRecord::Base
     
  has_many :agents
 
  def self.dangerous
    where(enemy: «Ernst Stavro Blofeld»)
  end
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
end
1
Agent.joins(:mission).merge(Mission.dangerous)
1
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»enemy» = ?

Когда мы заключаем в капсулу то, что представляет собой dangerous миссия в модели Mission , мы можем привязать ее к join посредством merge таким образом. Таким образом, перемещение логики таких условий в соответствующую модель, к которой она принадлежит, с одной стороны, является хорошим способом достижения более слабой связи — мы не хотим, чтобы наши модели Active Record знали много деталей друг о друге, а с другой — рука, это дает вам хороший API в ваших объединениях, не взрываясь на вашем лице. Пример ниже без слияния не будет работать без ошибки:

1
Agent.all.merge(Mission.dangerous)
1
SELECT «agents».* FROM «agents» WHERE «missions».»enemy» = ?

Когда мы теперь объединяем объект ActiveRecord::Relation для наших миссий с нашими агентами, база данных не знает, о каких миссиях мы говорим. Нам нужно уточнить, какая связь нам нужна, и сначала присоединить данные миссии — или SQL запутается. Последняя вишня сверху. Мы можем инкапсулировать это еще лучше, включив также агентов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Mission < ActiveRecord::Base
 
  has_many :agents
 
  def self.dangerous
    where(enemy: «Ernst Stavro Blofeld»)
  end
end
 
class Agent < ActiveRecord::Base
     
  belongs_to :mission
 
  def self.double_o_engagements
    joins(:mission).merge(Mission.dangerous)
  end
end
1
Agent.double_o_engagements
1
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»enemy» = ?

Это черешня в моей книге. Инкапсуляция, правильный ООП и отличная читаемость. Джек-пот!

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

01
02
03
04
05
06
07
08
09
10
11
12
class Section < ActiveRecord::Base
  has_many :agents
end
 
class Mission < ActiveRecord::Base
  has_many :agents
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
  belongs_to :section
end

Таким образом, в этом сценарии агенты будут иметь не только mission_id но и section_id . Все идет нормально. Давайте найдем все разделы с агентами с определенной миссией — так что разделы, у которых есть какое-то задание, выполняются.

1
Section.joins(:agents)
1
SELECT «sections».* FROM «sections» INNER JOIN «agents» ON «agents».»section_id» = «sections.»id»

Вы что-то заметили? Маленькая деталь отличается. Внешние ключи перевернуты. Здесь мы запрашиваем список разделов, но используем внешние ключи, например: "agents"."section_id" = "sections."id" . Другими словами, мы ищем внешний ключ из таблицы, к которой мы присоединяемся.

1
Agent.joins(:mission)
1
SELECT «agents».* FROM «agents» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id»

Раньше наши объединения через ассоциацию belongs_to выглядели так: внешние ключи были зеркально отображены ( "missions"."id" = "agents"."mission_id" ) и искали внешний ключ из таблицы, которую мы "missions"."id" = "agents"."mission_id" .

Возвращаясь к вашему сценарию has_many , мы теперь получили бы список разделов, которые повторяются, потому что, конечно, у них есть несколько агентов в каждом разделе. Таким образом, для каждого столбца агента, к которому присоединяется, мы получаем строку для этого раздела или section_id — короче говоря, мы в основном дублируем строки. Чтобы сделать это еще более головокружительным, давайте добавим миссии в микс.

1
Section.joins(agents: :mission)
1
SELECT «sections».* FROM «sections» INNER JOIN «agents» ON «agents».»section_id» = «sections».»id» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id»

Проверьте две части INNER JOIN . Все еще со мной? Через агентов мы «достигаем» их миссий из секции агентов. Да, для забавных головных болей, я знаю. То, что мы получаем, — это миссии, которые косвенно связаны с определенным разделом.

В результате мы получаем новые столбцы, но количество строк остается тем же, что возвращается этим запросом. То, что отправляется обратно в Active Record, что приводит к созданию новых объектов Ruby, также остается списком разделов. Поэтому, когда у нас несколько миссий с несколькими агентами, мы снова получим дублированные строки для нашего раздела. Давайте отфильтруем это еще немного:

1
Section.joins(agents: :mission).where(missions: { enemy: «Ernst Stavro Blofeld» })
1
SELECT «sections».* FROM «sections» INNER JOIN «agents» ON «agents».»section_id» = «sections».»id» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»enemy» = ‘Ernst Stavro Blofeld’

Теперь нам возвращаются только те участки, которые задействованы в миссиях, где Эрнст Ставро Блофельд является вовлеченным противником. Космополитические, как некоторые суперзлодеи могут думать о себе, они могут работать в более чем одном разделе — скажем, в разделах А и С, США и Канаде соответственно.

Если у нас есть несколько агентов в данном разделе, которые работают над одной и той же миссией, чтобы остановить Blofeld или что-то еще, мы бы снова вернули нам повторяющиеся строки в Active Record. Давайте быть немного более четким об этом:

1
Section.joins(agents: :mission).where(missions: { enemy: «Ernst Stavro Blofeld» }).distinct
1
SELECT DISTINCT «sections».* FROM «sections» INNER JOIN «agents» ON «agents».»section_id» = «sections».»id» INNER JOIN «missions» ON «missions».»id» = «agents».»mission_id» WHERE «missions».»enemy» = ‘Ernst Stavro Blofeld’

Это дает нам количество секций, из которых работает Блофельд, — которые известны, — в которых есть агенты, активные в миссиях с ним как с врагом. В качестве последнего шага давайте снова проведем рефакторинг. Мы извлекаем это в хороший «маленький» метод class Section :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Section < ActiveRecord::Base
 
  has_many :agents
 
  def self.critical
    joins(agents: :mission).where(missions: { enemy: «Ernst Stavro Blofeld» }).distinct
  end
end
 
class Mission < ActiveRecord::Base
  has_many :agents
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
  belongs_to :section
end

Вы можете реорганизовать это еще больше и разделить обязанности, чтобы добиться более слабой связи, но давайте двигаться дальше.

Большую часть времени вы можете положиться на Active Record, который пишет SQL-код, который вам нужен. Это означает, что вы остаетесь на земле Ruby и вам не нужно слишком беспокоиться о деталях базы данных. Но иногда вам нужно пробиться в землю SQL и заниматься своими делами. Например, если вам нужно использовать LEFT соединение и выйти из обычного поведения Active Record — выполнить INNER соединение по умолчанию. joins — небольшое окно для написания собственного пользовательского SQL, если это необходимо. Вы открываете его, подключаете свой собственный код запроса, закрываете «окно» и можете продолжать добавлять методы запросов Active Record.

Давайте продемонстрируем это на примере с гаджетами. Скажем, у типичного агента обычно есть гаджеты, и мы хотим найти агентов, которые не оснащены какими-либо модными гаджетами, чтобы помочь им в этой области. Обычное объединение не дало бы хороших результатов, так как мы на самом деле заинтересованы в значениях этих шпионских игрушек, равных nil или null в языке SQL.

01
02
03
04
05
06
07
08
09
10
11
12
class Mission < ActiveRecord::Base
  has_many :agents
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
  has_many :gadgets
end
 
class Gadget < ActiveRecord::Base
  belongs_to :agent
end

Когда мы выполняем операцию joins , мы получим только тех агентов, которые уже оснащены гаджетами, потому что agent_id в этих гаджетах не равен nil. Это ожидаемое поведение внутреннего соединения по умолчанию. Внутреннее соединение строится на совпадении с обеих сторон и возвращает только те строки данных, которые соответствуют этому условию. Несуществующий гаджет со значением nil для агента, который не имеет гаджета, не соответствует этому критерию.

1
Agent.joins(:gadgets)
1
SELECT «agents».* FROM «agents» INNER JOIN «gadgets» ON «gadgets».»agent_id» = «agents».»id»

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

1
Agent.joins(:gadgets).where(gadgets: {agent_id: nil})
1
SELECT «agents».* FROM «agents» INNER JOIN «gadgets» ON «gadgets».»agent_id» = «agents».»id» WHERE «gadgets».»agent_id» IS NULL

Неплохо, но, как вы можете видеть из выходных данных SQL, он не подходит и все еще настаивает на INNER JOIN по умолчанию. Это сценарий, в котором нам нужно соединение OUTER , потому что, так сказать, отсутствует одна сторона нашего «уравнения». Мы ищем результаты для гаджетов, которых нет, точнее, для агентов без гаджетов.

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

1
Agent.joins(«LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id»).where(gadgets: {agent_id: nil})
1
SELECT «agents».* FROM «agents» LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id WHERE «gadgets».»agent_id» IS NULL

Или, если вам интересны ленивые агенты без миссий — возможно, висящие на Барбадосе или где-то еще — наше пользовательское объединение будет выглядеть так:

1
Agent.joins(«LEFT OUTER JOIN missions ON missions.id = agents.mission_id»).where(missions: { id: nil })
1
SELECT «agents».* FROM «agents» LEFT OUTER JOIN missions ON missions.id = agents.mission_id WHERE «missions».»id» IS NULL

Внешнее объединение является более инклюзивной версией объединения, поскольку оно будет соответствовать всем записям из объединенных таблиц, даже если некоторые из этих отношений еще не существуют. Поскольку этот подход не так исключителен, как внутренние объединения, вы получите кучу нулей здесь и там. Конечно, в некоторых случаях это может быть информативным, но, тем не менее, обычно мы ищем внутренние объединения. Rails 5 позволит нам использовать специализированный метод left_outer_joins вместо этого для таких случаев. В заключение!

Одна небольшая вещь для дороги: держите эти заглядывающие дыры в земле SQL как можно меньше. Вы сделаете всем, в том числе и будущему себе, огромную услугу.

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

Да, SQL может быть скучным, утомительным для чтения и не элегантным на вид, но не забывайте, что Rails оборачивает Active Record вокруг SQL, и вы не должны пренебрегать пониманием этого жизненно важного элемента технологии — просто потому, что Rails очень легко не заботиться о большинстве времени. Эффективность имеет решающее значение для запросов к базе данных, особенно если вы создаете что-то для большой аудитории с большим трафиком.

Теперь зайдите в интернет и найдите еще материал по SQL, чтобы вывести его из вашей системы — раз и навсегда!