Примечание. Этот пост будет наиболее доступен для разработчиков, имеющих некоторый опыт работы с реляционными базами данных, а также некоторый опыт работы с 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. Довольно мило, если вы спросите меня.