Примечание. Этот пост будет наиболее доступен для разработчиков, имеющих некоторый опыт работы с реляционными базами данных, а также некоторый опыт работы с SQL и ActiveRecord.

Распространенная головоломка с базой данных

Допустим, у меня есть сайт электронной коммерции, и в моей базе данных есть таблица товаров и таблица заказов. Заказы и товары имеют отношения «многие ко многим» через таблицу соединения order_items. То есть в заказе может быть много позиций, и одна позиция может быть связана со многими заказами. Таблица order_items содержит внешние ключи идентификаторов товаров и заказов.

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

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

Учитывая этот псевдокод, может возникнуть соблазн написать что-то подобное на Ruby:

items_with_count = Hash.new(0)

 Order.all.each do |order|
    order.items.each do |item|
      items_with_count[item.id] += 1
    end
  end

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

Развитие мечты

Что, если бы у нас было по-другому? Занимаясь некоторыми DDD, как мы больше всего хотели бы взаимодействовать с этой информацией? Нам может понравиться что-то более простое, например вызов item.count и получение количества раз, когда этот товар был заказан.

Конечно, у items нет метода #count, так как в базе данных нет столбца «count». Но с помощью ActiveRecord мы можем сделать возможным вызов этого метода. Более того, мы можем сделать это всего за один запрос к базе данных.

Использование #Select для доступа к дополнительным атрибутам

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

Настройка

Предположим, что в нашей базе данных есть такая информация:

beanie_baby  = Item.create(name: "Beanie Baby")
gorilla_suit = Item.create(name: "Gorilla Suit")
Order.create(items: [beanie_baby, gorilla_suit])
Order.create(items: [beanie_baby, beanie_baby, gorilla_suit])
Order.create(items: [gorilla_suit, beanie_baby, beanie_baby])

Шаг 1. Получите доступ к связи между товаром и заказом

Мы можем использовать этот AR-запрос:

joined = Item.joins(:orders) 

Это даст нам отношение элементов ActiveRecord, где каждый элемент появляется столько раз, сколько раз он связан с заказом. Для нашего примера данных наше отношение ActiveRecord содержит 5 записей для beanie_baby и 3 записи для gorilla_suit. (Потому что, давайте будем честными, костюмы горилл, хотя и классные, гораздо менее удобны для перевозки и хранения, чем детские шапочки.)

Шаг 2. Получите доступ к количеству каждого элемента

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

selected = 
joined.select("items.id, count(order_items.item_id) AS count")

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

Наконец, последний оператор, начинающийся с AS, позволяет нам переименовать этот второй столбец, чтобы мы могли получить к нему доступ по желаемому имени, то есть «count».

Шаг 3. Сгруппируйте по идентификаторам элементов

Теперь, когда мы выбрали идентификаторы элементов и количество идентификаторов, отображаемых в таблице order_items, нам нужно добавить еще один метод, чтобы убедиться, что AR знает, как сгруппировать подсчитанные идентификаторы:

grouped = selected.group("items.id")

Шаг 4. Заказывайте записи

Мы можем наложить еще один метод, чтобы убедиться, что элементы отображаются в порядке их количества:

ordered = grouped.order("count DESC")

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

Дополнительная фильтрация

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

most_ordered_item = ordered.limit(1)
# OR
most_ordered_item = ordered.first 

Учитывая то, как мы организовали информацию с помощью ActiveRecord, эта дополнительная фильтрация проста.

В итоге

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

Наш окончательный запрос ActiveRecord для наиболее упорядоченного элемента выглядит следующим образом:

Item.
joins(:orders).
select("items.id, count(order_items.item_id) AS count").
group("items.id").
order("count DESC").
limit(1)

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