#Запросы Active Record

##Введение

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

Active Record предоставляет гораздо больше возможностей для работы с отдельными записями, чем просто действия CRUD (англ. create, read, update, delete — «создание, чтение, обновление, удаление»). Он предоставляет вам сочетание Ruby-подобного интерфейса и возможности делать почти все, что можно сделать с помощью "голого" SQL. Вы можете избирательно подходить к выбору отдельных групп записей в соответствии с конкретными критериями и упорядочивать их так, как будет угодно. Вы можете объединять (join) таблицы вручную или делать запрос с использованием ассоциаций associations, предоставляемых Rails. Вы можете возвращать списки записей или производить над ними базовые математические операции - такие, как использование счетчиков или вычисление средних.

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

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

Предстоит много прочитать и изучить, но все это следует общей идее - "все, что можно сделать в SQL, можно сделать и с помощью Active Record". Кроме того, терминология остается той же, Active Record просто расширяет функциональность, предоставляя комплект универсальных методов (и концепций, таких как Relations), чтобы заодно сделаться еще более дружелюбным.

##Вы должны понимать:

  1. Что такое ActiveRecord::Relation?
  2. Что значит "ленивые (отложенные) вычисления" (lazy evaluation)?
  3. Как отразить отношение в виде массива?.
  4. Как проверить, существует ли уже определенная запись в базе данных?
  5. Чем полезно #find_by и как его использовать?
  6. Какая разница между результатами, возвращаемыми с использованием запросов #where и #find?
  7. Как объединять (join) таблицы в Rails?
  8. Когда можно использовать символы / хэши, а когда необходимо использовать строковые переменные в явном виде (explicit strings) в качестве параметров запроса?
  9. Что такое Скоупы (Scopes) и для чего они нужны?
  10. Что должно произойти с методом класса, чтобы он действовал как скоуп (scope)?

##Отношения и отложенные вычисления (Relations and Lazy Evaluation)

Использование User.find(1) вернет совершенно определенный объект - пользователя с ID=1 в виде объекта Ruby. Однако, такое поведение не является обычным. Большинство запросов, на самом деле, возвращают не объект Ruby, а только его имитацию. Например:

User.where(id: 1)

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

[#<User id: 1, email: "foo@bar.com">]

Но попробуйте выполнить User.where(:id => 1).class и увидите, что никакого массива (Array) нет, а есть экземпляр ActiveRecord::Relation. Отношения (relations) действительно очень похожи на массив, но представляют собой нечто большее.

Запросы Active Record возвращают отложенные (ленивые) отношения (Relations). Собственно, нет никаких причин требовать от базы данных выполнить запрос до самой последней минуты. А что если вам, вообще, никогда не придется использовать этот запрос? Что если вы захотите усложнить его перед тем, как выполнить? Relations дают вам эту гибкость и позволяют более эффективно использовать драгоценное время вашей базы данных.

Relations выполняются только тогда, когда становится совершенно необходимым узнать, что они содержат. Таким образом, если ваш контроллер запрашивает 5 постов в блоге, используя @posts = Post.limit(5), вашему взгляду предстает отношение (relation). И только когда код в шаблоне представления (вьюхе) вызывает метод @posts (например, @posts.first.title), запрос будет выполнен и отношение сохранено в памяти как реальный Ruby объект.

Такое поведение сложно заметить, если вы пользуетесь для тестирования чем-то вроде Rails Console ($ rails console), потому что запросы будут запускаться прямо в консоли, т.к. она неявно запускает метод .inspect для отношения (relation), который в свою очередь требует выполнения запроса. Но попробуйте посоздавать запросы так, как мы делали выше, и попроверять их с помощью #class... Чаще всего возвращаться будет ActiveRecord::Relation.

##Цепочки запросов (Chaining Queries)

Отношения (Relations) созданы для повышения не только скорости, но и гибкости. Допустим, вы хотите получить первые 5 постов в виде упорядоченного в возрастающем порядке списка (Post.limit(5).order(:created_at => :desc)). Т.к. #limit возвращает отношение, #order берет это отношение и применяет к нему свои собственные критерии. Таким же образом вы можете соединить в цепочку десятки методов, и в итоге, когда приходит время для вычислений, ActiveRecord и SQL (если это то, что вы используете для своей базы данных) определяют оптимальный путь построения и выполнения запроса, чтобы получить желаемый результат.

Это именно то поведение, которое вы ожидаете, и отношения это то, что позволяет ему осуществиться.

##Почему это важно?

Вам следует помнить, что Active Record обычно возвращают отношения (Relations), хотя бы потому, что вам придется часто с этим сталкиваться при написании и отладке кода. Это знание поможет вам почувствовать себя увереннее, создавая цепочки методов, чтобы получить сложные запросы.

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

И еще пара общих методов, которые НЕ возвращают Отношения и могут принудить к вычислению отношения: #all (возвращает массив объектов) и #find (возвращает одиночный объект).

##За рамками базовых запросов

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

###Проверка существования

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

#exists? (существует?) вернет true/false. #any? вернет true, если хотя бы одна запись соответствует заданному критерию, а #many? вернет true, если несколько записей отвечают конкретному критерию. Вы можете применить любой из них как непосредственно к модели, так и к Отношению, ассоциации или скоупу (это мы изучим позднее). В общем, где бы вы ни захотели применить их, скорей всего они будут работать.

# с использованием модели
Post.any?
Post.many?

# с использованием отношения
Post.where(:published => true).any?
Post.where(:published => true).many?

# с использованием ассоциации
Post.first.categories.any?
Post.first.categories.many?

###Аргументы

Существует много способов указывать аргументы для большинства методов Rails запросов. Как правило, вы можете использовать символы, строковые переменные (strings) или и то, и другое вместе. Я предпочитаю иметь дело с символами и хэшами (hashes), где только это возможно. Вы также можете использовать параметры ? как в обычном SQL.
Когда не возникает неопределенности (например, если вы не работаете с несколькими таблицами), вы можете выбрать - указывать имя таблицы или нет (см. #5). Все нижеследующее - одно и то же:

  1. User.where(:email => "foo@bar.com")
  2. User.where("email" => "foo@bar.com")
  3. User.where("email = 'foo@bar.com'")
  4. User.where("email = ?", "foo@bar.com")
  5. User.where("users.email" => "foo@bar.com")

###Еще немного разнообразной информации о запросах

Очень большие запросы, вообще-то, могут быть разделены на большое количество подзапросов, так что они не будут съедать кучу ресурсов. #find_each этим и занимается. Базовый принцип работы заключается в том, что он делит запрос на части, загружает первую часть и вычисляет ее до того, как перейти к следующей. Это может пригодиться при оптимизации запросов, но это совсем не то, о чем стоит беспокоиться заранее.

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

Стоит отметить, что #find возвращает непосредственно запись, тогда как #where возвращает ActiveRecord::Relation, которое обычно ведет себя как массив. Таким образом, если вы используете #where для поиска отдельной записи, нужно помнить о необходимости перейти в этот "массив" и взять оттуда первую запись User.where(:email => "foo@bar.com")[0] или User.where(:email => "foo@bar.com").first.

#find_by действительно изящный метод, который, по сути, позволяет вам создавать ваши собственные методы поиска. Это альтернатива использованию #where (к которому необходимо добавить #take или #first, чтобы извлечь результат из возвращенного массива). Если вы хотите осуществить поиск по электронной почте пользователя, напишите User.find_by(:email => 'foo@bar.com').

Смысл #select должен быть достаточно очевидным для таких SQL-ниндзей, как вы, - он позволяет определить, какие колонки выбрать из таблицы/таблиц, так же, как и в SQL. Например, выбрать только колонку ID для всех пользователей - User.select(:id). Вы так же можете использовать псевдонимы (alias), как и в SQL, но должны использовать кавычки вместо символов, т.е. @users = User.select("users.id AS user_id") создаст новый атрибут user_id, таким образом предоставляя вам доступ к @users.first.user_id.

##Агрегации (Aggregations)

Так же, как и в SQL, часто хочется сгруппировать некоторые поля (или "свернуть" (roll up) значения под одним заголовком). Например, группировка постов блога, написанных в определенный день. Это особенно полезно, когда необходимо применить к ним какую-то математическую операцию вроде #count или #max. Примером (чуть более сложным, потому что он включает в себя объединение двух таблиц) может служить подсчет количества всех постов блога, сгруппированных по тегам. Я могу написать что-то вроде этого:

Post.joins(:tags).group("tags.name").count
# => {"tag1" => 4, "tag2" => 2, "tag3" => 5}

#having - это что-то вроде условия #where для сгруппированных запросов.

##Соединения (Joins)

Во время работы со множеством таблиц часто возникает необходимость их объединения. Ассоциации Rails часто выполняют всю тяжелую работу по настройке соединений, если вы работаете с экземплярами объекта, так что вам не приходится явно использовать #join.

Однако, при выполнении таких запросов, как были показаны в примере выше (количество сгруппированных по тэгам постов), возникает необходимость использовать соединения, чтобы свести вместе требуемые таблицы. Нужно быть более внимательным с тем, как выбирать данные при использовании соединений - если вам нужна колонка :id, ID колонка какой таблицы имеется в виду? Придется использовать более точные строковые переменные при соединении, как в примере выше (скопирован ниже), где мы задаем атрибут name таблицы tags

Post.joins(:tags).group("tags.name").count
# => {"tag1" => 4, "tag2" => 2, "tag3" => 5}

##Ваше задание

  1. Прочитайте первые 5 секций гайдов Rails по запросам Active Records, чтобы более основательно рассмотреть функции запросов. Можно особо не беспокоиться о пакетах (batching) и #find_each.
  2. Прочитайте секцию 18 тех же самых гайдов Rails, чтобы получить краткое представление о exists?, any? и many?.
  3. Прочитайте секции 6, 7 и 19 все тех же гайдов Rails для понимания функций агрегирования и вычислений, которые вы можете осуществить с их помощью.
  4. Просмотрите секции 8-11 все тех же гайдов Rails.
  5. Прочитайте секции 12 все тех же гайдов Rails, чтобы увидеть, как Rails позволяет вам играть с соединением таблиц.
  6. Прочитайте секцию 16 все тех же гайдов Rails, чтобы ознакомиться с полезными методами find_or_create_by

##Запросы N+1 и нетерпеливая загрузка (Eager Loading)

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

Можно запрашивать ОДНУ И ТУ ЖЕ информацию множество раз... Rails в любом случае кэширует первый результат, так что это совершенно не влияет на производительность. Однако, существуют ситуации, когда вы сразу же вычисляете ActiveRecord::Relation, возвращенное запросом, а затем пытаетесь выполнить запрос на каждом из членов этой совокупности данных. А это уже такое количество запросов, что вы сразу замедлите свое приложение до скорости улитки.

Проблема N + 1 запроса является классическим образцом этого явления - вы захватываете все записи ваших пользователей (User.all), потом запускаете цикл, который проходит по каждому из пользователей и вызывает его ассоциацию, например, город, в котором живет пользователь (user.city). В этом примере мы предполагаем, что существует такая ассоциация между пользователем и городом, что пользователь принадлежит (belongs_to) городу. Это может выглядить так:

User.all.each do |user|
  puts user.city
end

Это приведет к тому, что один запрос будут направлен на получение всех пользователей, второй - получение для каждого пользователя города при помощи ассоциаций... итого N дополнительных запросов, где N - общее число пользователей. Отсюда следуют проблемы "N + 1". Обратите внимание, что абсолютно нормально просто захватывать (grab) обычный атрибут пользователя (User), такой как user.name... Это из-за того, что вы получаете это через ассоциацию с Городом (City), для которого приходится выполнять еще один полный запрос.

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

Rails знает об этой проблеме и предлагает простое решение - "нетерпеливую загрузку". Когда вы захватываете (grab) список всех пользователей, вы можете так же сказать Rails захватить еще и города (при помощи всего одного дополнительного запроса) и сохранить их в памяти до того времени, пока вы не захотите к ним обратиться. Затем user.city обрабатывается тем же способом, что и user.name... не требуя выполнения еще одного запроса. Весь фокус в методе #includes.

В сущности, #includes берет имя одной или более ассоциаций, которые вы хотите загрузить одновременно как исходный объект, и доставляет их в память. Вы можете соединить его в цепочку с другими методами, такими как операторы #where или #order.

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

Почти так же полезен метод #pluck, который описан в Rails Guide. #pluck позволяет пропустить некоторые шаги в процессе извлечения набора записей, сохранения их в памяти, а потом захвата определенной колонки и размещения ее в массиве. #pluck просто сразу же дает вам этот результирующий массив.

User.pluck(:name)
# => ["Foo", "Bar", "Baz", "Jimmy-Bob"]

Это еще один способ ускорить ваше приложение, если были обнаружены какие-то проблемные места. Впрочем, начните с избавления от N+1 запросов.

##Скоупы (Scopes)

Скоупы - недооцененные, впечатляющие и очень простые. Скоуп - это, в сущности, специальная цепочка методов ActiveRecords, которую вы можете присоединить к существующему отношению, вызвав её имя, как если бы оно было обычным методом. Легче увидеть это на примере.

Допустим, вы позволяете пользователям фильтровать только те посты блога, которые помечены отметкой "важно" ("important").

# app/models/post.rb
...
scope :important, -> { where(:is_important => true) }
...

# app/controllers/posts_controller.rb
...
def index
  if params[:important] == true
    @posts = Post.important.all
  else
    @posts = Post.all
  end
end

Это достаточно простой пример. Вместо того, чтобы постоянно переписывать эту цепочку методов ActiveRecord, когда это необходимо, вы создаете скоуп (scope) с чудесным названием, в котором содержится вся логика компонентов. Вы уменьшаете повторения и делаете свой код более читаемым. А самым прекрасным является то, что скоуп (scope) возвращает Отношения (Relations)... То есть вы можете соединять их в цепочки так, как вам угодно.

Возможно, вы сейчас думаете - зачем использовать скоуп (scope), когда можно написать метод класса, который будет делать то же самое? Вы можете сделать это при условии, что метод класса возвращает Отношение (Relation) (что добавляет работы мысли для пограничных случаев). На самом деле, использование метода класса зачастую является лучшим решением, если ваши логические цепочки достаточно сложны. Пример, приведенный выше, может быть также решен с помощью метода класса.

# app/models/post.rb
...
def self.important
  self.where(:is_important => true)
end
...

Для более глубокого изучения того, как пользоваться обоими способами, см. дополнительные ресурсы.

Сколько вам нужно знать, чтобы понимать и использовать скоупы? Возможно, в начале вам не захочется вникать в них или смотреть, зачем их использовать. Но помните о них, когда начнете работать над немного более сложными проектами, где они могут пригодиться.

##"Голый" SQL (Bare-Metal SQL)

Иногда не получается с помощью Active Record получить то, что нужно. В таком случае он предоставляет интерфейс для работы с "голым" SQL, который вы можете просто вписать в ваш запрос, как пожелаете. Лучше использовать это только в крайнем случае - обычно это утяжеляет код вашего приложения. Используйте для этого метод #find_by_sql.

##Ваше задание

  1. Прочитайте главу 14 гайдов Rails по запросам Active Records, чтобы познакомиться со скоупами (Scopes). И снова - не нужно запоминать детали, нужно понять саму концепцию и когда она может пригодиться.
  2. Прочитайте главу 17 тех же самых гайдов Rails, чтобы посмотреть, как использовать SQL напрямую.

##Заключение

Было пройдено много материала, но вы должны получить крепкое понимание основ того, что можете сделать с помощью ActiveRecord. Однако, уже даже на самом фундаментальном уровне вы можете делать с помощью запросов ActiveRecord достаточно многое из того, что делается в SQL. У вас будет возможность применить новоприобретенные методы запросов в будущих проектах, а остальное придет, когда вы будете работать самостоятельно.

##Дополнительные ресурсы

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

Поделиться уроком: