ActiveRecord немного про грабли, Relations и индексы

    Хочу рассказать Вам о наболевшем: о работе с AR в целом и с Relation в частности; предостеречь от стандартных садовых изделий, которые легко могут испортить жизнь и сделать код медленным и прожорливым. Повествование будет основываться на Rails 3.2 и ActiveRecord того же разлива. В Rails 4, конечно же, много чего нового и полезного, но на него ещё перейти нужно, да и фундамент в любом случае один и тот же.

    Сей материал, по большей части, предназначен для начинающих, ибо автору очень больно смотреть на извлечение содержимого целых таблиц в память в виде ActiveRecord объектов и на прочие отстрелы конечностей при использовании AR. Разработчикам, познавшим дзен, топик вряд ли принесёт пользу, они могут лишь Помочь, дополнив его своими примерами и назиданиями.



    Уж сколько раз твердили миру…


    Если вы начали работать с Relation (да и с любым ActiveRecord объектом вообще), то нужно чётко представлять одну вещь: в какой момент мы «овеществляем» выборку, то есть в какой момент мы перестаём конструировать SQL-запрос. Иначе говоря: когда происходит выборка данных и мы переходим к из обработке в памяти. Почему это важно? Да потому что неловкое:

    Product.all.find{|p| p.id == 42}
    

    Может повесить сервер, забрать всю оперативку и сделать ещё много пакостей. А то же самое, но иными словами:

    Product.find(42)
    

    отработает быстро и без последствий. Таки образом find и find — это совсем не одно и то же! Почему? Да потому что в первом случае мы сказали Product.all и выстрелили себе в ногу, так как это означает извлечь всё содержимое таблицы products и для каждой строки построить AR-объект, создать из них массив и уж по нему пройтись find, который является методом класса Array (вообще говоря, find из Enumerable, но это уже детали). Во втором случае всё гораздо лучше: find — это метод AR и предназначен для поиска по pk. То есть мы генерируем запрос

    SELECT * FROM products WHERE products.id = 42;
    

    Выполняем его, получаем одну строку и всё.

    Что такое хорошо и что такое плохо


    Теперь, разобравшись почему работа с AR — это большая ответственность, разберёмся с тем, как же не выстрелить себе в ногу. Сие довольно просто: надо пользоваться методами, которые предоставляет нам AR. Вот они: where, select, pluck, includes, joins, scoped, unscoped, find_each и ещё несколько, о которых можно узнать в документации или в соседнем хабе. А вот чем лучше не пользоваться перечислить будет очень сложно и, в то же время, очень просто: нежелательно пользоваться всем остальным, так как почти все оставшееся многообразие методов превращает Relation в Array со всеми вытекающими последствиями.

    Простые рецепты


    Теперь, приведу несколько стандартных и не очень конструкций, которые облегчают жизнь, но о которых очень часто забывают. Но перед задам вопрос читателю: вспомните функцию has_many. Подумайте, какие её параметры вы знаете и какими активно пользуетесь? Перечислите их в уме, посчитайте… а теперь вопрос: знаете ли вы сколько их на самом деле?

    Ответ
    24 штуки в Rails3 и 12 в Rails4. Разницу в 12шт составляют методы типа where, group и тд, а так же методы для работы с чистым SQL, которые в Rails4 передаются в блоке, а не в хэше.

    Зачем я это спросил? Да чтобы очень приблизительно оценить Ваш уровень и сказать, что ежели большую часть опций Вы знаете, то и нижеизложенное вряд ли принесёт Вам новые знания. Оценка эта очень условная, поэтому, уважаемый Читатель, не гневайся сильно, ежели она показалась Тебе нелепой/несостоятельной/странной/etc (нужное подчеркнуть).

    Рецепт номер раз


    Итак, теперь пойдём по-порядку. Про update_attributes и update_attribute знают все (или не все?). Первый — массово обновляет поля с вызовом валидаций и колбэков. Ничего интереного. Второй — пропускает все валидации, запускает колбэки, но может обновить значение только одного выбранного поля(кому-то больше по душе save(validate: false)). А вот про update_column и update_all почему-то часто забывают. Эти метод пропускают и валидации, и колбэки и пишут прямо в базу без всяких предварительных ласк.

    Рецепт номер два


    В комментариях напомнили про замечательный метод touch. О нём тоже частенько забывают и пишут что-то вроде

    @product.updated_at = DateTime.now
    @product.save
    

    или

    @product.update_attribute(:updated_at, DateTime.now)
    

    Хотя, по хорошему, для таких целей проще сделать вот так:

    @product.touch(:updated_at)  # в данном случае параметр можно опустить
    

    К тому же для touch есть собственный колбэк after_touch, а так же опция :touch присутствует у метода belongs_to.

    Как правильно итерировать


    В хабе уже говорили про find_each, но я не могу не упомянуть его ещё раз, ибо конструкции

    product.documents.map{…}
    

    и им изоморфные, встречаются чуть более чем везде. Проблема в обычных итераторах, применённых на Relation только одна: они вытаскивают из БД сразу всё. И это ужасно. В противоположность им find_each, по умолчанию, таскает по 1000 штук за раз и это просто прекрасно!

    UPD: как уже отмечали в комментариях, все методы, которые однозначно не проецируются на raw-sql делегируются в to_a из-за чего и происходит выборка всего запроса в память и работа с ним уже не на стороне БД, а на стороне Ruby.

    Совет про default_scope


    Оборачивайте содержимое default_scope в блок. Пример:

    default_scope where(nullified: false)  # плохо!
    default_scope { where(nullified: false) }  # хорошо
    

    В чём разница? В том, что первый вариант выполняется прямо при запуске сервера и если поля nullified в БД не оказалось, то и сервер не взлетит. То жесамое относится и к миграциям — они не пройдут из-за отсутствия поля, которое, скорее всего, мы как раз хотим добавить. Во втором случае, в силу того, что Ruby ленив, блок выполнится только в момент обращения к модели и миграции выполнятся штатно.

    Has_many through


    Ещё один часто встречающийся пациент это

    product.documents.collect(&:lines).flatten
    

    здесь продукт имеет много документов, которые имеют много строк. Часто бывает, что хочется получить все строки всех документов, относящихся к продукту. И в таком случае творят вышеописанную конструкцию. В данном случае можно вспомнить про опцию through для реляций и сделать для продукта следующее:

    has_many :lines, through: documents
    

    и затем выполнить

    product.lines
    

    Получается и нагляднее и эффективнее.

    Немного про JOIN


    В продолжение темы джоинов вспомним про includes. Что в нём особенного? Да то, что это LEFT JOIN. Довольно часто вижу, что левый/правый джоин пишут явно

    joins("LEFT OUTER JOIN wikis ON wiki_pages.wiki_id=wikis.id")
    

    это конечно тоже работает, но чистый SQL в RoR всегда был не в почёте.

    Так же, не отходя от кассы, надо напомнить про разницу значений в joins и where при совместном использовании. Допустим у нас есть таблица users, а разные сущности, например products имеют поле author_id и реляцию author, кояя имеет под собой таблицу users.

    has_one :author,
    	     class: 'User',
    	     foreign_key: 'author_id'   # не обязательно, но для наглядности
    

    Следующий код для такого случая работать не будет

    products.joins(:author).where(author: {id: 42})
    

    Почему? Потому что в joins указывается имя реляции, которую джоиним, а в where накладывается условие на таблицу и надо говорить

    where(users: {id: 42})
    

    Избежать такого можно явным указанием ‘AS author’ в джоине, но это снова будет чистый SQL.

    Далее посмотрим на джоины с другого ракурса. Что бы мы не джоинили, в итоге мы получаем объекты класса, с которого всё начиналось:

    Product.joins(:documents, :files, :etc).first
    

    В данном случае получаем продукт вне зависимости от количества джоинов. Некоторых это поведение огорчает, так как им хотелось бы получить поля из приджойненных таблиц. И они начинают делать этот же запрос с другой стороны: брать документы, джоинить их с продуктами, писать чистый SQL для связи с другими сущностями, вобщем изобретают велосипед, когда правильный и логичный код был написан в самом начале. Поэтому напомню самую основу:

    Product.joins(:documents, :files, :etc).where(...).pluck('documents.type')
    

    Здесь мы получаем массив с нужным полем из БД. Плюсы: минимум запросов, не создаётся AR-объектов. Минусы: в Rails 3 pluck принимает только 1(один) параметр и вот такое

    pluck('documents.type', 'files.filename', 'files.path')
    

    можно будет сделать только в Rails 4.

    Build реляций


    Теперь обратимся к рассмотрению работы с build-ом реляций. В общем случае всё довольно просто:

    product.documencts.build(type: 'article', etc: 'etc').lines.build(content: '...')
    

    После вызова product.save у нас будет происходить сохранение всех ассоциаций вместе с валидациями, преферансом и куртизанками. Во всём этом радостном действе есть один нюанс: всё это хорошо, когда product не readonly и/или нет иных ограничений на сохранение. В таких случаях многие устраивают огород, аналогичный огороду с joins в примере выше. То есть создают document, привязывают его к product и build-ят строки для документа. Получается кривова-то и дефолтное поведение, которое, обычно, завязано на обработку ошибок product не работает. Поэтому в довесок всё это сразу же обставляют костылями, пробрасывающими ошибки и получается довольно мерзко. Что делать в таком случае? Надо вспомнить про autosave и понять как он работает. Не вдаваясь в детали скажу, что работает он на callback-ах. Поэтому способ сохранить реляции для вышеописанного продукта есть:

    product.autosave_associated_records_for_documents
    

    В этом случае случится сохранение документа, вызовутся его колбэки для сохранения строк и т.д.

    Несколько слов об индексах


    На последок нужно сказать про индексы, ибо многие бились головой об твёрдые предметы из-за проблем на почве индексов. Сразу прошу прощения что мешаю в кучу ActiveRecord и возможности БД, но по личному убеждению: нельзя хорошо работать с AR, не осознавая что происходит в этот момент на стороне БД.

    Проблема первая


    Почему-то многие уверены что order на Relation не зависит от того, по какому столбцу мы сортируем. Разновидностью этого заблуждения является отсутствие понимания разницы между order Relation и order Array. Из-за этого можно встретить default_scope с ордером по VARCHAR полю и вопросы в духе: «А почему это у вас так медленно страница загружается? Там же всего пара записей извлекается из БД!». Проблема здесь в том, что дефолтная сортировка — это чертовски дорого, если у нас нет индекса на этом столбце. По умолчанию AR сортирует по pk. Это происходит когда мы делаем

    Products.first
    

    Но у pk есть индекс практически всегда и проблем нет. А вот когда мы говорим, что будет делать order(:name) при любом обращении к модели — начинаются проблемы.
    Для справки: если объяснять «на пальцах», то при сортировке по индексированному столбцу реальной сортировки не происходит, она уже присутствует в базе и данные сразу отдаются в правильном порядке.

    Проблема вторая


    Составные индексы. Не все о них знают и ещё меньший круг лиц знает зачем они нужны. Если коротко, то составной индекс — это индекс на основе двух и более полей БД. Где он может пригодиться? Два частых места его использования:
    • polymorphic ассоциации
    • промежуточная таблица связей «много ко многим».
    Про полиморфные связи было рассказано здесь. Для них, очень часто, удобно создавать составной индекс. Вот немного дополненный пример из офф.манула:

    class CreatePictures < ActiveRecord::Migration
      def change
        create_table :pictures do |t|
          t.string  :name
          t.integer :imageable_id
          t.string  :imageable_type
          t.timestamps
        end
        
        add_index :pictures, [:imageable_id, :imageable_type] # вот он составной индекс
      end
    end
    

    Вот несколько слов про разницу обычного и составного индекса. Далее в подробности вдаваться не буду, ибо тема для отдельного хаба. К тому же, до меня уже всё расписали.
    Теперь про промежуточную таблицу связей. Всем известный HBTM. Здесь, в некоторых случаях, уместно повесить составной индекс на assemblies_parts (см. ссылку на HBTM). Но надо помнить о том, что последовательность полей в составном индексе имеет знаение. Подробности тут.

    Проблема третья


    «Индексы нужны везде!». Встречается не так часто, но вызывает страшные тормоза всего и вся. Нужно помнить, что индекс — это не панацея и гарантированный х10-х100 к скорости, а инструмент, который нужно применять в правильных местах, а не махать им над головой и засовывать в каждую дырку. Вот тут можно почитать про типы индексов, а тут можно узнать зачем они вообще нужны.

    За сим все


    Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Так же буду рад, если поделитесь своим «наболевшим» и своими опытом о том, что надо помнить и чем лучше пользоваться в разных ситуациях при разработке.

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

    Что бы Вы больше всего хотели увидеть в следующем хабе?
    Поделиться публикацией

    Похожие публикации

    Комментарии 41
      0
      Может как туториал пометить?
        0
        Думал об этом, но как-то так и не решил туторил ли это или просто сборник полезных советов. Если ещё кто-нибудь выскажется за туторил, то сразу помечу.
        0
        Вот здесь вызов select не нужен, он просто лишний:
        Product.joins(:documents, :files, :etc).where(...).select('documents.type').pluck('documents.type')
        

          0
          Согласен, не уследил, сейчас поправлю.
            +2
            Спасибо за статью, но хотелось бы больше, например, если бы вы ещё рассказали:

            1. Вы много рассказали о joins и ничего — про includes. А в 4.0 появился ещё и references, а ещё есть eager_load. И какая между ними разница — знают немногие, почитать можно тут и тут
            2. О классном методе merge, который позволяет объединять условия из двух Relation'ов.
            3. О том, что в where можно передавать Relation'ы и AR будет строить подзапросы (4.0+)
            4. О том, что не стоит избегать SQLя, потому что следующую конструкцию понять впоследствии будет решительно непросто (но тут я просто захотел проверить, можно ли такой ужас на AR сделать):
              # Constructs very scary SQL query to fetch latest conversations
              # Select max message ids from latest sent and received messages, grouped by users.
              sub_tmpl = Message.unscoped.select('MAX(messages.id) AS id')
              latest_sent = sub_tmpl.select('messages.sent_messageable_id AS user_id')
                                    .group(:sent_messageable_id).where(
                                        received_messageable: current_user,
                                        sent_messageable_type: 'User',
                                    )
              latest_rcvd = sub_tmpl.select('messages.received_messageable_id AS user_id')
                                    .group(:received_messageable_id).where(
                                      sent_messageable: current_user,
                                      received_messageable_type: 'User',
                                    )
              sent_and_received = latest_sent.union(latest_rcvd)
              latest_ids = Message.unscoped.select('MAX(last_message_ids.id)')
                                           .from("(#{sent_and_received.to_sql}) last_message_ids")
                                           .group('user_id')
              @messages = Message.where(id: latest_ids).page(params[:page])
              0
              Про includes хорошо написано в офф.документашке, конечно можно про «N + 1 проблему» написать и как к ней includes относится, да и ещё много чего можно, но целью хаба было описание самых ляповых моментов и того, что уж надоело рассказывать каждому встречному.

              Вы делаете упор на Rails 4, там умопомрачительное количество новых фич и я мечтаю перейти на него, и, быть может написать про всё это мнообразие: о том что нового, как пользоваться и какие финты ушами можно делать. Но пока проект на Redmine и Rails 3, большую часть времени пишу на нём и, думаю, ещё многие им пользуются, посему и рассказ про Rails 3.

              Что касается SQL в коде… это сложный вопрос. По-хорошему его там быть не должно, но столько разных случаев… Честно говоря, целую статью, а может и цикл статей, можно написать про то, что на рельсах сделать невозможно или очень сложно, если не прибегать к SQL. Видал я и огромные выборки в моделях, и логику в БД, и вызов процедур, и модели на pipeline-функциях, и собственные конструкторы SQL-запросов чуть ли не с самописными биндами… но это довольно узкоспециализированная тематика. В таких темах человек либо уже разбирается и статья ему не нужна, либо он недостаточно опытен и не поймёт что же за магия творится в коде. Так что Ваш случай довольно мил и прост и SQL там совсем не страшный.
                0
                Сочувствую вам, переходите на 4-е рельсы как можно скорее, особенно это касается 4-го Active Record'а. У нас тоже есть один проект ещё на 3.2 и делать что либо в нём после 4-х рельсов — сущая пытка. Подзапросы не умеет, pluck с несколькими колонками — тоже. И т.д. и т.п. А уж сколько любви разработчики вложили в PostgreSQL — словами не передать, а в 4.2 они это количество любви ещё и удвоили.
                  0
                  Rails 4 в плане работы с БД — это шикарно. Сплю и вижу pluck, none scope, not и многое многое другое, в том числе то, о чём пока даже не подозреваю. Но перейти пока физически невозможно, ибо проект на Redmine, который сам по себе ещё не переехал на четвёрку. Поэтому будем тянеть лямку третьих до победного.

                  Для успокоения души недавно заглядывал в исходники под Rails 2. Очень удивлялся, как люди в таких спартанских условиях разрабатывали. После такого и Ralis 3 очень неплох.

                  И да, раз с PostgreSQL всё так радужно, может они что-то для MySQL и Oracle сделали?
            0
            product.lines

            Получается и нагляднее и эффективнее.

            В чём «эффективность» заключается? Что делать, если нам не нужны остальные методы зависимости, а нужен только один?
              0
              Простите, не до конца понял вопрос, поправьте если не а то отвечаю.

              В данном случае я хотел во-первых подчеркнуть экономию в плане обращений к базе, ибо collect мы имеет N+1 обращений, где N — количество документов. Если же реляция чем-то не нравится, то scope уж точно решит проблему. То есть в модели Product сделать:

              scope :lines, ->{Line.where(product_id: id)}
              

              Но это, в подавляющем большинстве случаев, тупиковый путь, ибо раз Вам зачем-то понадобилась связь, то она непременно понадобится вновь, потребуются джоины и прочее, и тут уж от реляции никуда не деться. А экономия на методах, которые создаёт has_many и прочие функции для ассоциаций — это как-то очень странно. Если приведёте пример, когда это критично, буду очень благодарен.
                0
                Понял, спасибо.
                Про критичность — не знаю) просто интересно отношение других разработчиков к перенасыщению методами. Я пока не знаю, на что это влияет.
                  0
                  В общем случае в Rails, особенно в первое время, очень пугает количество методов. Например, всякие acts_as_something (например acts_as_list) или state_machine, которые добавляют несколько реляций и десяток методов, хотя, казалось бы, пара-тройка строк кода в модели. Но ничего страшного в этом нет и в своей жизни ни разу не видел борьбы за минимум методов. Зато часто видел модели по 600+ строк — вот это плохо. Существует много способов разнести методы так, чтобы было много файлов с небольшим содержимым. Каждый файл отвечает за маленький кусочек логики. А все вместе они образовывают большую модель с глубокой структурой и связями. Тут уж надо упомянуть ActiveSupport::Concern. В Rails 4 для этого даже папку специальную сделали 'app/models/concerns'. Ну и привести пример: по моему мнению active_record/base.rb — очень хороший представитель того, как много методов не мешают друг другу и что много require, include и extend — это хорошо и правильно.

                  И вообще, про структуру проекта, выделение модулей и их связь тоже можно много писать.
              +1
              Говоря о AR, я бы ещё упомянул всяческие touch и inc.

              И что касается find_each, вы как-то слишком трепетно к нему относитесь. Инструмент хороший, когда вам нужно сделать рассылку по таблице пользователей из нескольких десятков тысяч записей. И совершенно неуместный, когда вам нужно вывести на странице 20 записей.
                0
                Про touch постараюсь добавить упоминание, нужная штуковина.

                Трепетно отношусь по субъективным причинам. Часть занимаюсь rake-задачами по обработке больших объёмов данных, да и back-end люблю гораздо больше, чем front-end. Плюс периодически перелопачиваю старый код, в котором создатель знать не знал о работе с БД и уместная вставка find_each даёт феерическую оптимизацию с 1+час до 5мин. А так да, если вы не заняты рабобой с большими объёмами, а кругом пагинаторы с ~10 записями, то find_each довольно бесполезен. Хотя всё равно приведу пример, хоть и кривоватенький: при генерации в контроллере

                @products = Product.last(10)
                

                и последующей обработке во вьюхе

                - @products.each do |product|   # find_each тут получше будет
                  %td 
                    = product.name
                  %td 
                    = product.type
                
                  0
                  find_each штука хорошая, но больше нравится find_in_batches, при этом каждую пачку оборачиваю в транзакцию если делаю много изменений.
                  На тему последнего примера, для себя взял за правило по возможности выводимые списки/таблицы выносить во фрагменты. Список = _list, элемент = _item, перечислимый объект передаю снаружи. Таким образом повышается реюзабельность, можно передать любой подходящий массив или отношение. Плюс если использовать кэширование в добавок.
                  Так что ИМХО, для пагинации и мелких списков find_each не лучший вариант.
                    0
                    Я и не утверждал что find_each лучше. Как Вы и сказали, вьюхи не должны думать о типах данных и о их структуре, они должны отображать. Но жизнь такая штука… много уж велосипедов написано, да и «лучшее — враг хорошего», поэтому всегда будут простор для рефакторинга.

                    За find_in_batches спасибо. Помнится пару лет назад свой велосипед писали, один к одному его функционал.
                    0
                    вот именно в приведённом примере не вижу вообще никаких причин использовать find_each.
                    а вообще, в 4.2 обещают какую-то адскую оптимизацию AR, надо на днях поэкспериментировать.
                      0
                      Да, с примером я неправ здесь. Но ошибку понял несколько позже, когда delegate обсудили. А про AR расскажите, если не затруднит, думаю многим интересно будет.
                        0
                        Речь видимо идёт об Adequate Record — Active Record теперь умеет кэшировать некоторые виды генерённых SQL-запросов для повторного использования (поскольку процедура получения сырого SQL из ActiveRecord::Relation всё же затратна). Если в каком-то месте к базе нужно делать много однотипных запросов — это даёт ощутимый прирост. Ну, по крайней мере так говорят.
                  0
                  Замечание насчет индексов: вот это
                  реальной сортировки не происходит

                  , конечно же, неверно.
                  Сортировка, естественно, происходит, но не во время выборки, а во время вставки/обновления данных. Под каждый индекс создается отдельное дерево, соответственно, при вставке новой записи нужно сделать вставку в каждое такое дерево.
                  Ближе к делу: если у вас таблица умеренных размеров (не гигабайты), 99% операций с ней — это select-ы, и всего 1% запись, но вы навесили на таблицу 100 индексов, то, вероятно, вы тормознули систему (так как индексы разбросаны по диску и их нужно флашить при каждом обновлении, и это почти наверняка дольше, чем прочитать таблицу целиком и сделать ей full scan в памяти).
                    0
                    Сортировки при выборке(при чтении) не происходит, именно это я и имел в виду. И да, очень о многих вещах умолчал и умолчал умышленно, оставив ссылки для дальнейшего ознакомления. Не хотел писать много много о том, о чём и так книги написаны. Хотелось дать понять что индексы — это круто, что ими надо уметь пользоваться и отправить изучать их дальше, чтобы тот, кто ещё не в курсе сам дошёл до озвученных Вами вещей.
                    0
                    Раз хаб оказался востребованным у Сообщества, добавляю опрос с темой для следующего рассказа.
                      0
                      Свое:
                      Про Ruby магию, метапрограммирование, в частности можно применительно к AR.
                        0
                        Спасибо. Как говорится «слона-то я и не приметил».
                      0
                      >> Product.all.find{|p| p.id == 42}

                      А еще говорят, говнокодеры только в php бывают
                        +1
                        Просто много народу приходит в Rails из PHP :-)
                        0
                        В Rails 4 all≡scoped и прострелить конечность так просто не получится, разве что вместо all вызвать to_a, но это совсем тяжёлый случай.


                        Вопрос на засыпку для автора статьи (для Rails 4): в Вашем же примере
                        Product.all.find{|p| p.id == 42}
                        метод #find делегируется к какому классу?? Ага. А как Вы думаете, каким образом к этому классу будет преобразовано relation Product.all? ;-)
                          0
                          Делегироваться #find и там и там к Enumerable будет. Тут подразумевается тяжесть содеянного. В Rails 3 all≡to_a и выборка всей таблицы в память неизбежна, в Rails 4 all≡scoped и элемент с id=42 должен найтись побыстрее, хоть и при помощи множества выборок с 'LIMIT 1'. А вообще да, в обоих случаях фигня получается.
                            0
                            С какого перепугу в Rails 4 будет «множество выборок с 'LIMIT 1'»?? = ) При вызове #find будет предварительно вызван метод #to_a для relation Product.all, что неизбежно приведёт к «SELECT * FROM products».
                              0
                              У меня было чёткое осознание того, что явного to_a не происходит, иначе бы перед любым другим итератором (each, collect, map etc) случался бы тот же to_a и адские выборки, но их нет. Основываясь на описании N + 1 queries problem подозреваю, что и для scoped выборки всего не происходит. Хотя окончательно смогу убедиться в этом только завтра, сейчас под рукой нету RoR проекта.
                                  0
                                  Интересно девки пляшут… Завтра поиграюсь с этим. Спасибо.
                                    +1
                                    Опередил. )) Я чуть ниже подробнее написал — наверняка кто-то из читателей (не автор статьи) ещё не очень хорошо умеет читать код Rails.
                                    +4
                                    Чёткое осознание подвело. Выборки происходят — можно включить log level :info (0) и убедиться. А можно почитать код.

                                    ActiveRecord::Relation:
                                    # ...
                                    include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
                                    # ...
                                    def to_a
                                      load
                                      @records
                                    end
                                    


                                    Очевидно, #to_a осуществляет овеществление relation'а. Смотрим дальше модуль Delegation.

                                    ActiveRecord::Delegation:
                                    delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
                                    


                                    Уже всё понятно, наверное (насчёт #map, #each, #collect и по аналогии далее)… Тем не менее, на всякий случай найдём прямые доказательства для #find из примера.

                                    def array_delegable?(method)
                                      Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
                                    end
                                    
                                    def method_missing(method, *args, &block)
                                      if @klass.respond_to?(method)
                                        scoping { @klass.public_send(method, *args, &block) }
                                      elsif array_delegable?(method)
                                        to_a.public_send(method, *args, &block)
                                      elsif arel.respond_to?(method)
                                        arel.public_send(method, *args, &block)
                                      else
                                        super
                                      end
                                    end
                                    


                                    Исходя из предположения, что метод Product::find не определён ("klass.respond_to?(method)" ложно), попадаем в ветку «elsif array_delegable?(method)». Метод #find определён для Array и не входит в BLACKLISTED_ARRAY_METHODS:

                                    BLACKLISTED_ARRAY_METHODS = [
                                      :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
                                      :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
                                      :keep_if, :pop, :shift, :delete_at, :compact, :select!
                                    ].to_set
                                    


                                    В результате вызов #to_a, который для Product.all будет осуществлён через запрос «SELECT 'products'.* FROM 'products'».

                                    П.С. Призываю всех не верить мне на слово, не верить моей интерпретации кода ActiveRecord::Relation и ActiveRecord::Delegation, а проверить самостоятельно и убедиться на практике. :-)
                                      0
                                      Покопаюсь завтра в исходниках и брэкпоинтами побалуюсь, ежели всё так — текст поправлю.
                                        0
                                        Да, Ваша правда, всё преобразуется в Array и никаких чудес. Буду лучше изучать исходнки AR.
                                    0
                                    Иными словами, пример будет работать одинаково и в Rails 3, и в Rails 4, и «Примечание» действительности не соотвутствует. Методы Enumerable потребуют овеществления relation в любом случае.
                                  0
                                  Совет про default_scope

                                  Лучший совет про default_scope — это не использовать default_scope
                                    0
                                    Что правда, то правда. Кажущееся удобство с лихвой компенсируется кучей случаев некорректного поведения, с которыми никто не знает, что делать. Например #8217
                                    0
                                    product.documents.map{…}

                                    Проблема в обычных итераторах, применённых на Relation только одна: они вытаскивают записи из БД поштучно.


                                    Ошибка. Уже писал про это выше. «Обычные» итераторы, как вы их называете, будут вызваны на self.to_a (будут загружены все записи, соответствующие relation, а не «поштучно»). Хотите поштучно (just for lulz) — используйте find_each(batch_size: 1).
                                      0
                                      Да, спасибо, поправил. Подискутировали об этом хорошо, а ошибку исправить забыл.

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

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