ActiveRecord Pluck. Что это за фигня?

ActiveRecord Pluck. Что это за фигня?

Содержание
  1. Что такое pluck и как он работает?
  2. Так почему же он быстрее?
  3. Извлечение полей jsonb в Rails
  4. Превосходящая сила Pluck
  5. Выщипывание, где
  6. Когда стоит дважды подумать, прежде чем использовать pluck?

ActiveRecord - это отличная ORM, в которой есть масса интересных мелочей, облегчающих вам жизнь при работе с запросами к БД. Одним из таких методов является pluck. Я использую pluck столько, сколько себя помню, и даже моя вторая по возрасту статья посвящена ему!

Vm2iq2P.png
Vm2iq2P.png

Руководство по pluck в Rails довольно интересное, но все же есть некоторые моменты, которые вы можете упустить, поэтому я постараюсь изложить все здесь. Если вы только начинаете или могли пропустить документацию, я настоятельно рекомендую ознакомиться с ней. Однако если вы уже знакомы с AR/pluck, то можете смело переходить к заключительным разделам.

Что такое pluck и как он работает?

Pluck - это метод AR-запроса, который выбирает один или несколько атрибутов (или столбцов) и возвращает массив, не загружая соответствующие записи. Он использует select для внутренних целей и запускает запрос на получение значений.

Customer.pluck(:id) # SELECT customers.id FROM customers => [1,2,3]
# or
Customer.pluck(:id, :first_name) # SELECT customers.id, customers.first_name FROM customers => [[1, "David"], [2, "Fran"], [3, "Jose"]]

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

Customer.pluck(:id) # или Customer.pluck(:id, :first_name)

чтобы стать чище и эффективнее.

Так почему же он быстрее?

pluck напрямую преобразует данные в массив Ruby, минуя создание AR-объектов, что дает ему преимущество в производительности. Вот статья с бенчмарком, сравнивающим pluck с select с collect/map.

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

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

Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")

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

Customer.pluck(:first_name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

Customer.limit(1).pluck(:first_name)
# => ["David"]

pluck активирует ускоренную загрузку, если объект отношения содержит включаемые значения, даже если ускоренная загрузка не нужна для запроса. Как видно из примера, нам не нужно соединение для идентификаторов клиентов, но pluck все равно выполнит его! Так что будьте осторожны, когда у вас есть ассоциации, загружаемые с нетерпением!

irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"

# To avoid this, you can do:
assoc.unscope(:includes).pluck(:id)

Теперь мы увидели, что есть в руководствах, давайте посмотрим на некоторые случаи, которые могут поставить вас в тупик!

Извлечение полей jsonb в Rails

Если у вас есть jsonb-поле в объекте AR и вы хотите получить вложенное значение, вы все равно можете использовать pluck!

user.metadata = {
    "location": {
        "city": "Нью-Йорк",  # ... дополнительные данные
    },
    # ... другие атрибуты метаданных
}

# Использование 'pluck' для получения значений 'city' из ключа 'location' в 'metadata'
cities = user.pluck("metadata -> 'location' ->> 'city'")
=> ["New York"]

Превосходящая сила Pluck

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

# Сценарий: Вы хотите получить отличительные города из таблицы пользователей
# Шаг 1: Использование 'select' с 'distinct' для получения уникальных городов
distinct_cities_query = User.select(:city).distinct

# Шаг 2: Попытка использовать 'pluck' для получения названий городов
cities = distinct_cities_query.pluck(:city)

# Города могут содержать дублирующиеся названия городов

Выщипывание, где

Еще одна распространенная проблема может возникнуть при использовании pluck с where (помните, что pluck выполняет немедленный запрос). Следовательно, он может выполнить несколько дополнительных запросов там, где это не требуется. К счастью, существует рубокоп PluckInWhere, который можно использовать для предупреждения.

# bad # вызовет 2 запроса
Post.where(user_id: User.active.pluck(:id)) # SELECT id FROM users WHERE /* условия для активных пользователей */;
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...); -- Массив идентификаторов из первого запроса

# good # срабатывает только 1 запрос
Post.where(user_id: User.active.select(:id)) # SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE /* условия для активных пользователей */);

Когда стоит дважды подумать, прежде чем использовать pluck?

Если вы уже загрузили объекты AR в память, нет необходимости использовать pluck, так как это вызовет еще один запрос. Лучше воспользуйтесь map или collect.

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

# Используйте find_in_batches для пакетной обработки
User.find_in_batches(batch_size: 1000) do |users|
  users.each do |user|
    email = user.email
    # Обработка email
  end
end

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