ActiveRecord Hacks

в 16:14, , рубрики: activerecord, ruby, ruby on rails, метки: , ,

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js