Как стать автором
Обновить

ActiveRecord Hacks

Время на прочтение4 мин
Количество просмотров10K
Сегодня я поделюсь своим набором не всегда очевидных функций и возможностей 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


позволяет сделать выборку из всех аккаунтов, в которых есть непрочитанные собщения.
Теги:
Хабы:
+26
Комментарии18

Публикации

Истории

Работа

Ruby on Rails
10 вакансий
Программист Ruby
8 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн