Сегодня я поделюсь своим набором не всегда очевидных функций и возможностей Active Record, с которыми я столкнулся в процессе разработки Ruby on Rails приложений или нашел в чужих блогах.
Обход валидации при использовании update_attributes
Стандартный метод update_attributes не имеет ключа, позволяющему обойти валидацию, поэтому приходится прибегать к assign_attributes с последующим save
Разумеется – лучше не прибегать к этому способу очень часто :)
Разделение на 2 непересекающихся коллекции
Иногда возникает задача разделения выборки объектов на 2 непересекающиеся коллекции. Сделать это можно с помощью такого использования scope.
Ну и соответственно доступ к обеим коллекциям можн ополучить с помощью
pluck
В предыдущем примере я использовал метод pluck. Наверняка каждый из вас использовал что-то типа
или даже
Так вот – pluck позволяет сделать это проще
Доступ к базовому классу
В процессе работы над одним проектом я столкнулся с большой вложенностью классов моделей и необходимостью добраться до корневого класса. Классы выглядели примерно так:
Для того, чтобы добраться из PlainPicture до Art можно использовать метод becomes
first_or_create и first_or_initialize
Еще один замечательный метод – first_or_create. Из названия ясно что он делает, а мы давайте посмотрим как его можно использовать
Также мы его можем использовать в блочной конструкции
А если вы не хотите сохранять – можно использовать first_or_initialize например таким образом
scoped и none
Обратите внимание на еще 2 замечательных метода – scoped и none. Как они работают – покажу на примере, при этом хочу отметить, что надо разделять их поведение в rails3 и rails4, так как оно различается.
Как поведет себя метод в случае передачи в него :published и :unpublished я надеюсь вам понятно, различия в версиях rails тут нет.
Использование scoped в нашем примере в случае rails3 позволяет создать анонимный скоп, который может использоваться для сложных составных запросов. Если попытаться его применить в rails4, то можно увидеть сообщение, что метод стал deprecated и вместо него предлагается использовать Model.all. В случае же rails3 Model.all возвращает не ожидаемый нами ActiveRecord::Relation, а Array.
Ситуация с none похожа на scoped с точностью до наоборот :) Этот метод возвращает пустой ActiveRecord::Relation, но работает он только в rails4. Нужен он в том случае, если нужно вернуть нулевые результаты, Для использования в rails3 есть такой workaround:
или даже такой (например в initializer)
find_each
Метод find_each очень удобен для того, чтобы обработать большое количество записей из базы данных. Можно было бы конечно сделать выборку типа
Но в таком случае нам придется хранить в памяти всю выборку целиком, что в случае большого объема данных очень нерентабельно. В этом случае правильнее будет использовать такой подход
который небольшими выборками (по 1000 объектов за раз по умолчанию) обрабатывает данные.
to_sql и explain
Два метода, которые помогут вам разобраться как работает ваш запрос.
вернет вам sql-запрос, который приложение составит для завпроса в базу данных, а
покажет техническую информацию по запросу – примерное количество времени, объем выборки и другие данные.
scoping
Этот метод позволяет сделать выборку внутри выборки, например
осуществит запрос типа
merge
Еще один интересный метод, который позволяет пересечь несколько выборок. Например
позволяет сделать выборку из всех аккаунтов, в которых есть непрочитанные собщения.
Обход валидации при использовании 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
позволяет сделать выборку из всех аккаунтов, в которых есть непрочитанные собщения.