Rails Scope возвращает все вместо nil

Я сталкиваюсь со странной проблемой создания области и использования first Искатель. Кажется, как будто с помощью first как часть запроса в области действия, он будет возвращать все результаты, если результаты не найдены. Если какие-либо результаты будут найдены, он вернет первый результат.

Я настроил очень простой тест, чтобы продемонстрировать это:

class Activity::MediaGroup < ActiveRecord::Base
  scope :test_fail, -> { where('1 = 0').first }
  scope :test_pass, -> { where('1 = 1').first }
end

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

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

irb(main):001:0> Activity::MediaGroup.test_fail
  Activity::MediaGroup Load (0.0ms)  SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1
  Activity::MediaGroup Load (0.0ms)  SELECT "activity_media_groups".* FROM "activity_media_groups"
=> #<ActiveRecord::Relation [#<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1>, #<Activity::MediaGroup id: 2, created_at: "2014-01-06 01:11:06", updated_at: "2014-01-06 01:11:06", user_id: 1>, #<Activity::MediaGroup id: 3, created_at: "2014-01-06 01:26:41", updated_at: "2014-01-06 01:26:41", user_id: 1>, #<Activity::MediaGroup id: 4, created_at: "2014-01-06 01:28:58", updated_at: "2014-01-06 01:28:58", user_id: 1>]>

Другая область работает как ожидалось:

irb(main):002:0> Activity::MediaGroup.test_pass
  Activity::MediaGroup Load (1.0ms)  SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 1) ORDER BY "activity_media_groups"."id" ASC LIMIT 1
=> #<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1>

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

irb(main):003:0> Activity::MediaGroup.where('1=0').first
  Activity::MediaGroup Load (0.0ms)  SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1=0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1
=> nil

Я что-то здесь упускаю? Мне кажется, что это ошибка в Rails/ActiveRecord/Scopes, если нет каких-то неизвестных поведенческих ожиданий, о которых я не знаю.

1 ответ

Решение

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

Прежде всего,

  1. scope возвращает ActiveRecord::Relation

  2. Если существует ноль записей, он запрограммирован на возврат всех записей, что снова ActiveRecord::Relation вместо nil

Идея заключается в том, чтобы сделать области видимости цепными (то есть) одним из ключевых различий между scope а также class methods

Пример:

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

class Post < ActiveRecord::Base
  scope :by_status, -> status { where(status: status) }
  scope :recent, -> { order("posts.updated_at DESC") }
end

И мы можем назвать их свободно так:

Post.by_status('published').recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
#   ORDER BY posts.updated_at DESC

Или с предоставленным пользователем параметром:

Post.by_status(params[:status]).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
#   ORDER BY posts.updated_at DESC

Все идет нормально. Теперь давайте переместим их в методы класса, просто для сравнения:

class Post < ActiveRecord::Base
  def self.by_status(status)
    where(status: status)
  end

  def self.recent
    order("posts.updated_at DESC")
  end
end

Помимо использования нескольких дополнительных строк, никаких больших улучшений. Но что теперь происходит, если параметр: status равен nil или пуст?

Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL 
#   ORDER BY posts.updated_at DESC

Post.by_status('').recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = '' 
#   ORDER BY posts.updated_at DESC

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

scope :by_status, -> status { where(status: status) if status.present? }

Там мы идем:

Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC

Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC

Потрясающие. Теперь давайте попробуем сделать то же самое с нашим любимым методом класса:

class Post < ActiveRecord::Base
  def self.by_status(status)
    where(status: status) if status.present?
  end
end

Запуск этого:

Post.by_status('').recent
NoMethodError: undefined method `recent' for nil:NilClass

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

def self.by_status(status)
  if status.present?
    where(status: status)
  else
    all
  end
end

Обратите внимание, что я возвращаю все для случая nil/blank, который в Rails 4 возвращает отношение (ранее он возвращал массив элементов из базы данных). В Rails 3.2.x вы должны вместо этого использовать область видимости. И там мы идем:

Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC

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

Короче:

Независимо от того, что области предназначены для возвращения ActiveRecord::Relation чтобы сделать его цепным. Если вы ожидаете first, last или же find результаты, которые вы должны использовать class methods

Источник: http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/

Вы можете использовать limit вместо того first потому как -

Когда данные не найдены, тогда first возвращается nil или first(<number>) возвращает массив, который не может быть связан с цепочкой.

В то время как, limit возвращается ActiveRecord::Relation объект.

Подробнее в этом посте -https://sagarjunnarkar.github.io/blogs/2019/09/15/activerecord-scope/

Другие вопросы по тегам