Сегодня я поделюсь своим набором не всегда очевидных функций и возможностей Active Record, с которыми я столкнулся в процессе разработки Ruby on Rails приложений или нашел в чужих блогах.
Обход валидации при использовании update_attributes
Стандартный метод update_attributes не имеет ключа, позволяющему обойти валидацию, поэтому приходится прибегать к assign_attributes с последующим save
@user = User.find(params[:id])
@user.assign_attributes(:name, "")
@user.save(validate: false)
Разумеется – лучше не прибегать к этому способу очень часто :)
Разделение на 2 непересекающихся коллекции
Иногда возникает задача разделения выборки объектов на 2 непересекающиеся коллекции. Сделать это можно с помощью такого использования scope.
Article < ActiveRecord::Base
scope :unchecked, where(:checked => false)
#or this, apologies for somewhat unefficient, but you already seem to have several queries
scope :unchecked2, lambda { |checked| where(["id not in (?)", checked.pluck(:id)]) }
end
Ну и соответственно доступ к обеим коллекциям можн ополучить с помощью
Article.unchecked
Article.unchecked2(@unchecked_articles)
pluck
В предыдущем примере я использовал метод pluck. Наверняка каждый из вас использовал что-то типа
Article.all.select(:title).map(&:title)
или даже
Article.all.map(&:title)
Так вот – pluck позволяет сделать это проще
Article.all.pluck(:title)
Доступ к базовому классу
В процессе работы над одним проектом я столкнулся с большой вложенностью классов моделей и необходимостью добраться до корневого класса. Классы выглядели примерно так:
class Art < ActiveRecord::Base
end
class Picture < Art
end
class PlainPicture < Picture
end
Для того, чтобы добраться из PlainPicture до Art можно использовать метод becomes
@plain_pictures = PlainPicture.all
@plain_pictures.map { |i| if i.class < Art then i.becomes(Art) else i end }.each do |pp|
#do something with Art
end
first_or_create и first_or_initialize
Еще один замечательный метод – first_or_create. Из названия ясно что он делает, а мы давайте посмотрим как его можно использовать
Art.where(name: "Black square").first_or_create
Также мы его можем использовать в блочной конструкции
Art.where(name: "Black square").first_or_create do |art|
art.author = "Malevich"
end
А если вы не хотите сохранять – можно использовать first_or_initialize например таким образом
@art = Art.where(name: "Black square").first_or_initialize
scoped и none
Обратите внимание на еще 2 замечательных метода – scoped и none. Как они работают – покажу на примере, при этом хочу отметить, что надо разделять их поведение в rails3 и rails4, так как оно различается.
def filter(filter_name)
case filter_name
when :all
scoped
when :published
where(:published => true)
when :unpublished
where(:published => false)
else
none
end
end
Как поведет себя метод в случае передачи в него :published и :unpublished я надеюсь вам понятно, различия в версиях rails тут нет.
Использование scoped в нашем примере в случае rails3 позволяет создать анонимный скоп, который может использоваться для сложных составных запросов. Если попытаться его применить в rails4, то можно увидеть сообщение, что метод стал deprecated и вместо него предлагается использовать Model.all. В случае же rails3 Model.all возвращает не ожидаемый нами ActiveRecord::Relation, а Array.
Ситуация с none похожа на scoped с точностью до наоборот :) Этот метод возвращает пустой ActiveRecord::Relation, но работает он только в rails4. Нужен он в том случае, если нужно вернуть нулевые результаты, Для использования в rails3 есть такой workaround:
scope :none, where(:id => nil).where("id IS NOT ?", nil)
или даже такой (например в initializer)
class ActiveRecord::Base
def self.none
where(arel_table[:id].eq(nil).and(arel_table[:id].not_eq(nil)))
end
end
find_each
Метод find_each очень удобен для того, чтобы обработать большое количество записей из базы данных. Можно было бы конечно сделать выборку типа
Article.where(published: true).each do |article|
#do something
end
Но в таком случае нам придется хранить в памяти всю выборку целиком, что в случае большого объема данных очень нерентабельно. В этом случае правильнее будет использовать такой подход
Article.where(published: true).find_each do |article|
#do something
end
который небольшими выборками (по 1000 объектов за раз по умолчанию) обрабатывает данные.
to_sql и explain
Два метода, которые помогут вам разобраться как работает ваш запрос.
Art.joins(:user).to_sql
вернет вам sql-запрос, который приложение составит для завпроса в базу данных, а
Art.joins(:user).explain
покажет техническую информацию по запросу – примерное количество времени, объем выборки и другие данные.
scoping
Этот метод позволяет сделать выборку внутри выборки, например
Article.where(published: true).scoping do
Article.first
end
осуществит запрос типа
SELECT * FROM articles WHERE published = true LIMIT 1
merge
Еще один интересный метод, который позволяет пересечь несколько выборок. Например
class Account < ActiveRecord::Base
# ...
# Returns all the accounts that have unread messages.
def self.with_unread_messages
joins(:messages).merge( Message.unread )
end
end
позволяет сделать выборку из всех аккаунтов, в которых есть непрочитанные собщения.
Автор: poimtsev