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 ответ
Это не ошибка или странность, после некоторого исследования я обнаружил, что он разработан специально.
Прежде всего,
scope
возвращаетActiveRecord::Relation
Если существует ноль записей, он запрограммирован на возврат всех записей, что снова
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/