Active Records Queries Tricks (перевод)

September 12, 2017    Ruby Rails ActiveRecord SQL

Оригинал статьи: https://medium.com/@apneadiving/active-records-queries-tricks-2546181a98dd

Запрос с условием в связанных таблицах

У вас есть таблица Users связанная с таблицей Profile. Если вам нужно получить пользователей с активным профилем, то в конечном итоге остаётся сделать следующее:

# User model
scope :activated, ->{
  joins(:profile).where(profiles: { activated: true })
}

Это неправильное решение, логика модели Profile проникает в модель User, а это противоречит объектно-ориентированным принципам инкапсуляции. Я бы предложил следующее решение:

# Profile model
scope :activated, ->{ where(activated: true) }
# User model
scope :activated, ->{ joins(:profile).merge(Profile.activated) }

В этом решении мы разделяем отношения и логику.


Вложенные joins

Будьте осторожнее при использовании joins в ActiveRecord. Рассмотрим пример: у пользователя (User), есть профиль (модель Profile), а у профиля есть много Skills. Для этой задачи используют INNER JOIN, но …

User.joins(:profiles).merge(Profile.joins(:skills))
=> SELECT users.* FROM users
INNER JOIN profiles    ON profiles.user_id  = users.id
LEFT OUTER JOIN skills ON skills.profile_id = profiles.id

Рекомендуется использовать:

User.joins(profiles: :skills)
=> SELECT users.* FROM users
INNER JOIN profiles ON profiles.user_id  = users.id
INNER JOIN skills   ON skills.profile_id = profiles.id

Exist query. Запрос на проверку существования.

Вам нужны пользователи (User) без популярных публикаций (Post), в этом случае вы воспользуетесь запросом с использованием конструкции NOT EXISTS:

# Post
scope :famous, ->{ where("view_count > ?", 1_000) }
# User
scope :without_famous_post, ->{
 where(_not_exists(Post.where("posts.user_id = users.id").famous))
}
def self._not_exists(scope)
  "NOT #{_exists(scope)}"
end
def self._exists(scope)
  "EXISTS(#{scope.to_sql})"
end

Такую же схему можете использовать для конструкции EXISTS.


Подзапросы

Вам требуется найти все публикации, которые были сделаны определенными пользователями. И чаще всего делают следующее:

Post.where(user_id: User.created_last_month.pluck(:id))

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

Post.where(user_id: User.created_last_month)

ActiveRecord делает всё за вас.


К основам

Не забывайте ActiveRecord запросы можно перевести в обычный SQL запрос в виде текста, добавив к нему .to_sql, а также увидеть детали и оценить сложность, используя .explain.


Booleans

Конструкция User.where.not(tall: true) генерирует запрос (postgres) вида

SELECT users.* FROM users WHERE users.tall <> 't'

Вы получите пользователей у которых поле tall будет иметь значение false, но будут пропущены пользователи, у которых поле tall не содержит данных, т.е. NULL. Чтобы избежать такой ситуации делаем следующее

User.where("users.tall IS NOT TRUE")

или так

User.where(tall: [false, nil])


comments powered by Disqus