Advertisement
  1. Code
  2. Ruby on Rails
Code

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

by
Length:LongLanguages:

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

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

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

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

Темы

  • Includes и ранняя загрузка
  • Соединение таблиц
  • Предварительная загрузка
  • Скоупы
  • Агрегации
  • Динамические фаиндеры
  • Конкретные поля
  • Кастомный SQL

Includes и предварительная загрузка

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

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

Rails

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

SQL

У этого одного агента в списке есть два обработчика, и когда мы теперь запрашиваем объект-агент для его обработчиков, никаких дополнительных запросов к базе данных не нужно. Разумеется, мы можем шагнуть еще дальше и загрузить несколько связанных записей таблицы. Если нам нужна была загрузка не только обработчиков, но и связанных с ними миссий агента по любой причине, мы могли бы использовать includes следующим образом.

Rails

Просто! Просто будьте осторожны с использованием уникальных и множественных версий для includes. Они зависят от ваших ассоциаций. У ассоциации has_many используется множественное число, в то время как belongs_to или has_one нуждаются в особой версии. Если вам нужно, вы также можете использовать выражение where для указания дополнительных условий, но предпочтительным способом указания условий для связанных таблиц, которые загружаются, является использование вместо них joins.

Одна вещь, о которой нужно помнить, - это то, что данные, которые будут добавлены, будут полностью отправлены в Active Record, которая, в свою очередь, строит объекты Ruby, включая эти атрибуты. Это контрастирует с «просто» объединением данных, где вы получите виртуальный результат, который вы можете использовать для вычислений, например, и будет меньше утечки памяти.

Соединение таблиц

Объединение таблиц - еще один инструмент, который позволяет избежать отправки слишком большого количества ненужных запросов. Общим сценарием является объединение двух таблиц с одним запросом, который возвращает какую-то комбинированную запись. joins - это еще один метод поиска Active Record, который позволяет вам использовать SQL-термины - JOIN таблиц. Эти запросы могут возвращать записи, объединенные из нескольких таблиц, и вы получаете виртуальную таблицу, которая объединяет записи из этих таблиц. Это довольно удобно, когда вы сравниваете это с выполнением кучи разных запросов для каждой таблицы. Существует несколько различных видов перекрытия данных, которые вы можете получить с помощью этого подхода.

Внутреннее соединение является операцией по умолчанию для joins. Это соответствует всем результатам, которые соответствуют определенному идентификатору и его представлению в качестве внешнего ключа из другого объекта или таблицы. В приведенном ниже примере просто укажите: дайте мне все миссии, где id миссии отображается как mission_id в таблице агента. "agents"."mission_id" = "missions"."id". Внутренние соединения исключают отношения, которые не существуют.

Rails

SQL

Таким образом, мы сопоставляем миссии и сопровождающих эти миссии агентов - в одном запросе! Конечно, мы могли бы сначала получить миссии, перебирать их один за другим и запросить их агентов. Но тогда мы вернемся к нашей ужасной «проблеме запроса N + 1». Нет уж, спасибо!

Что также приятно в таком подходе, так это то, что мы не получим ни одного nil в случае с внутренними соединениями; мы получаем только возвращаемые записи, которые соответствуют их идентификаторам для внешних ключей в связанных таблицах. Если нам нужно найти миссии, например, которые не имеют каких-либо агентов, нам потребуется внешнее соединение. Поскольку в настоящее время речь идет о написании собственного OUTER JOIN SQL, мы рассмотрим это в последней статье. Возвращаясь к стандартным соединениям, вы также можете соединить несколько связанных таблиц.

Rails

И вы можете добавить к ним некоторые, выражения where, чтобы сделать ваших фаиндеров еще более специфичными. Ниже мы рассматриваем только миссии, которые выполняются Джеймсом Бондом и только агенты, принадлежащие миссии «Moonraker» во втором примере.

SQL

Rails

SQL

При использовании joins вы также должны обратить внимание на исключительное и множественное использование ваших ассоциаций моделей. Поскольку мой класс Mission has_many: agents, мы можем использовать множественное число. С другой стороны, для класса Agent belongs_to: mission, единственная версия отлично работает. Важная небольшая деталь: часть where здесь проще. Поскольку вы просматриваете несколько строк в таблице, которые выполняют определенное условие, множественная форма всегда имеет смысл.

Скоупы

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

Здесь вас ограничивает только фантазия - joinsincludes и where, все что угодно! Поскольку скоупы также возвращают объекты ActiveRecord::Relations, вы можете сделать цепочку вызовов и без колебаний вызвать другие скоупы. Извлечение таких скоупов и объединение их в более сложные запросы очень удобно и делает более длинные из них все более читаемыми. Скоупы определяются с помощью синтаксиса «прочный лямбда»:

Rails

SQL

Как видно из приведенного выше примера, найти Джеймса Бонда намного лучше, если вы можете просто объединить скоупы. Таким образом, вы можете смешивать и сопоставлять различные запросы и одновременно оставаться DRY. Если вам нужны скоупы с помощью ассоциаций, они также в вашем распоряжении:

Вы также можете переопределить default_scope, когда вы используете что-то вроде Mission.all.

SQL

Агрегации

Этот раздел не настолько продвинут с точки зрения понимания, но вы будете нуждаться в них чаще, чем не в сценариях, которые можно считать более продвинутыми, чем ваш обычный поиск, похожий на .all, .first, .find_by_id или что-то еще. Например, фильтрация, основанная на базовых расчетах, скорее всего - то, с чем новички не сразу начинают работать. На что мы смотрим именно здесь?

  • sum
  • count
  • minimum
  • maximum
  • average

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

  • count

Rails

SQL

  • average

Rails

SQL

Поскольку мы теперь знаем, как мы можем использовать joins, мы можем сделать шаг дальше и спросить, например, среднее количество гаджетов, которые агенты берут на конкретную миссию.

Rails

SQL

Группировка этого среднего количества гаджетов по именам миссий становится теперь весьма тривиальной. Подробнее о группировках см. ниже:

Rails

SQL

  • sum

Rails

SQL

  • maximum

Rails

SQL

  • minimum

Rails

SQL

Внимание!

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

Rails

Сгруппированные

Если вы хотите, чтобы расчеты разбивались и сортировались в логические группы, вы должны использовать выражение GROUP, а не делать это в Ruby. Я имею в виду, что вам следует избегать итерации над группой, которая производит потенциально тонны запросов.

Rails

SQL

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

Динамические фаиндеры

Для каждого атрибута ваших моделей, например name, email_address, favorite_gadget и т.д., Active Record позволяет использовать очень читаемые методы поиска, которые динамически создаются для вас. Звучит загадочно, я знаю, но это не значит ничего, кроме find_by_id или find_by_favorite_gadget. Часть find_by является стандартной, и Active Record просто подставляет имя атрибута для вас. Вы даже можете добавить! если вы хотите, чтобы этот фаиндер выбрасывал ошибку, если ничего не найдено. Более того, вы можете даже объединить эти методы динамического поиска вместе. Именно так:

Rails

SQL

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

Rails

SQL

В этом примере, тем не менее, приятно видеть, как все это работает под капотом. Каждый новый _and_ добавляет оператор SQL AND для логического связывания атрибутов вместе. В целом, основное преимущество динамических фаиндеров - легкость чтения при слишком большом количестве динамических атрибутов, тем не менее, теряет преимущество. Я редко использую это, может быть, в основном, когда я играю в консоли, но, безусловно, хорошо знать, что Rails предлагает эту приятную небольшую хитрость.

Определенные поля

Active Record дает вам возможность возвращать объекты, которые немного более сфокусированы на атрибутах, которые они несут. Обычно, если не указано иначе, запрос будет запрашивать все поля в строке с помощью * (SELECT "agents".*), А затем Active Record строит объекты Ruby с полным набором атрибутов. Однако вы можете выбрать select только определенные поля, которые должны быть возвращены запросом, и ограничить количество атрибутов, которые ваши объекты Ruby должны «переносить».

Rails

SQL

Rails

SQL

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

Rails

Предупреждение: если вы попытаетесь получить доступ к атрибутам объекта, который не был выбран в ваших запросах, вы получите MissingAttributeError. Так как id будет автоматически предоставлен для вас в любом случае, вы можете запросить идентификатор, не выбирая его.

Пользовательский SQL

И последнее, но не менее важное: вы можете написать свой собственный SQL-запрос через find_by_sql. Если вы достаточно уверены в своем собственном SQL-Fu и нуждаетесь в некоторых пользовательских запросах к базе данных, этот метод может оказаться очень удобным. Но это уже совсем другая история. Просто не забудьте сначала проверить методы обертки Active Record чтобы избежать повторного изобретения колеса, где Rails пытается встретиться с вами более чем на полпути.

Rails

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

SQL

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

Rails

Мы можем написать методы класса, которые инкапсулируют SQL внутри документа Here. Это позволяет нам писать многострочные строки очень легко читаемым образом, а затем хранить эту строку SQL внутри переменной, которую мы можем повторно использовать и передать в find_by_sql. Таким образом, мы не замазываем тонны кода запроса внутри вызова метода. Если у вас есть несколько мест для использования этого запроса, это также следует принципу DRY.

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

Используйте если вам это нужно! Это может быть оказаться для вас спасением. Слово о синтаксисе здесь. Часть SQL является всего лишь идентификатором здесь, чтобы отметить начало и конец строки. Бьюсь об заклад, вы не будете нуждаться в этом методе так часто, будем надеяться! Это определенно имеет место быть, и Rails земля не будет такой без этой возможности - в редких случаях вы наверняка захотите максимально точно настроить свой собственный SQL.

Заключение

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

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

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.