В этой последней части мы собираемся немного углубиться в запросы и поиграть с несколькими более сложными сценариями. В этой статье мы подробнее рассмотрим отношения моделей 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)
|
SQL
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
.
SQL
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)
|
SQL
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)
|
SQL
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
|
SQL
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)
|
SQL
1
|
SELECT «sections».* FROM «sections» INNER JOIN «agents» ON «agents».»section_id» = «sections.»id»
|
Вы что-то заметили? Маленькая деталь отличается. Внешние ключи перевернуты. Здесь мы запрашиваем список разделов, но используем внешние ключи, например: "agents"."section_id" = "sections."id"
. Другими словами, мы ищем внешний ключ из таблицы, к которой мы присоединяемся.
Рельсы
1
|
Agent.joins(:mission)
|
SQL
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)
|
SQL
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» })
|
SQL
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
|
SQL
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)
|
SQL
1
|
SELECT «agents».* FROM «agents» INNER JOIN «gadgets» ON «gadgets».»agent_id» = «agents».»id»
|
Мы, с другой стороны, ищем агентов чмо, которые остро нуждаются в любви от квартирмейстера. Ваше первое предположение могло бы выглядеть следующим образом:
Рельсы
1
|
Agent.joins(:gadgets).where(gadgets: {agent_id: nil})
|
SQL
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})
|
SQL
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 })
|
SQL
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, чтобы вывести его из вашей системы — раз и навсегда!