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

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

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

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

  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])
Про includes хорошо написано в офф.документашке, конечно можно про «N + 1 проблему» написать и как к ней includes относится, да и ещё много чего можно, но целью хаба было описание самых ляповых моментов и того, что уж надоело рассказывать каждому встречному.

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

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

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

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

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

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

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

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

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

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

И что касается find_each, вы как-то слишком трепетно к нему относитесь. Инструмент хороший, когда вам нужно сделать рассылку по таблице пользователей из нескольких десятков тысяч записей. И совершенно неуместный, когда вам нужно вывести на странице 20 записей.
Про 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
find_each штука хорошая, но больше нравится find_in_batches, при этом каждую пачку оборачиваю в транзакцию если делаю много изменений.
На тему последнего примера, для себя взял за правило по возможности выводимые списки/таблицы выносить во фрагменты. Список = _list, элемент = _item, перечислимый объект передаю снаружи. Таким образом повышается реюзабельность, можно передать любой подходящий массив или отношение. Плюс если использовать кэширование в добавок.
Так что ИМХО, для пагинации и мелких списков find_each не лучший вариант.
Я и не утверждал что find_each лучше. Как Вы и сказали, вьюхи не должны думать о типах данных и о их структуре, они должны отображать. Но жизнь такая штука… много уж велосипедов написано, да и «лучшее — враг хорошего», поэтому всегда будут простор для рефакторинга.

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

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

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


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

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

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


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

Публикации

Истории