ActiveRecord Hacks

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


позволяет сделать выборку из всех аккаунтов, в которых есть непрочитанные собщения.
Поделиться публикацией

Комментарии 18

    0
    Здорово, что собрали всё в одном месте. Нашел немного нового для себя.
      0
      Алексей, спасибо, с none и becomes не доводилось столкнуться. Возьму на карандаш. Хотя сейчас не вижу где можно было бы попробовать. Если честно, то не ожидал, что бы будите постить статьи тех характера. Успехов!
        0
        Дык я стараюсь :) Буду и в своем блоге alec-c4.com что-то иногда писать :)
          0
          Например(про none):
          каминари получает nil и дохнет
          = paginate @objects получает nil и дохнет
          проще один раз проверить и отдать пустой ActiveRelation чем во всех мыслимых мистах писать
          — if @objects.presence
          = paginate @objects
          –1
          Вы конечно простите, я понимаю, что это пример, но за такое нужно пальцы ломать выговор делать.

          @plain_pictures = PlainPicture.all
          
          @plain_pictures.map ...
          


          Для этого есть

          #find_each
          
            0
            в этом куске речь шла про becomes если что :)
              0
              Я то понял :) Но ведь кто-то же может взять это в оборот.
            0
            find_each, кстати, добавит/заменит у вашего запроса order by, что естественно. а так же естественно что уберёт limit, если таковой есть.

              –1
              Боюсь, что если возникнет надобность использовать find_each вместе с order и limit, то это в 99.9% будет код, за который можно смело руки вырвать.
              +3
              «Hacks» подразумевает под собой использование неких скрытых возможностей или расширение оных. В вашем случае это скорее cheetsheet.
                0
                *cheat
                0
                Код, использующий pluck, можно упростить до

                Article.pluck(:title)
                
                  0
                  согласен, да и как напомнил мне один из хабравчан — в 3й рельсе Article.all.pluck(:id) работать не будет так как all возвращает Array, а не AR Relation
                  0
                  Раз уж примеры для rails 4

                  where(["id not in (?)", checked.pluck(:id)])

                  упрощается до

                  where.not(id: checked.ids)
                    0
                    Для использования в rails3 есть такой workaround:

                    scope :none, where(:id => nil).where("id IS NOT ?", nil)

                    разве нельзя просто
                    scope :none, where('1=2')
                    ?
                      0
                      Хотя, если есть джойны, наверное, не выйдет…
                      0
                      А есть ли способ элегантно объединять скоупы?
                      Например у пользователя есть скоупы received_messages, sent_messages. Можно ли сделать all_messages без непосредственного использования SQL-строки?
                        0
                        Может лучше вместо:
                        if i.class < Art then i.becomes(Art) else i end

                        Писать так:
                        i.class < Art ? i.becomes(Art) : i

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое