Хочу рассказать Вам о наболевшем: о работе с 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;
выполняем его, получаем одну строку и всё.
Примечание: справедливости ради стоит отметить, что этот пример работает в Rails 3; в Rails 4 all≡scoped и прострелить конечность так просто не получится, разве что вместо all вызвать to_a, но это совсем тяжёлый случай.
Что такое хорошо и что такое плохо
Теперь, разобравшись почему работа с AR — это большая ответственность, разберёмся с тем, как же не выстрелить себе в ногу. Сие довольно просто: надо пользоваться методами, которые предоставляет нам AR. Вот они: where, select, pluck, includes, joins, scoped, unscoped, find_each и ещё несколько, о которых можно узнать в документации или в соседнем хабе. А вот чем лучше не пользоваться перечислить будет очень сложно и, в то же время, очень просто: нежелательно пользоваться всем остальным, так как почти все оставшееся многообразие методов превращает Relation в Array со всеми вытекающими последствиями.
Простые рецепты
Теперь, приведу несколько стандартных и не очень конструкций, которые облегчают жизнь, но о которых очень часто забывают. Но перед задам вопрос читателю: вспомните функцию has_many. Подумайте, какие её параметры вы знаете и какими активно пользуетесь? Перечислите их в уме, посчитайте… а теперь вопрос: знаете ли вы сколько их на самом деле?
Зачем я это спросил? Да чтобы очень приблизительно оценить Ваш уровень и сказать, что ежели большую часть опций Вы знаете, то и нижеизложенное вряд ли принесёт Вам новые знания. Оценка эта очень условная, поэтому, уважаемый Читатель, не гневайся сильно, ежели она показалась Тебе нелепой/несостоятельной/странной/etc (нужное подчеркнуть).
Рецепт номер раз
Итак, теперь пойдём по-порядку. Про update_attributes и update_attribute знают все (или не все?). Первый — массово обновляет поля с вызовом валидаций и колбэков. Ничего интереного. Второй — пропускает все валидации, запускает колбэки, но может обновить значение только одного выбранного поля(кому-то больше по душе save(validate: false)). А вот про update_column и update_all почему-то часто забывают. Эти метод пропускают и валидации, и колбэки и пишут прямо в базу без всяких предварительных ласк.
Как правильно итерировать
В хабе уже говорили про find_each, но я не могу не упомянуть его ещё раз, ибо конструкции
product.documents.map{…}
и им изоморфные, встречаются чуть более чем везде. Проблема в обычных итераторах, применённых на Relation только одна: они вытаскивают записи из БД поштучно. И это ужасно. В противоположность им find_each, по умолчанию, таскает по 1000 штук за раз и это просто прекрасно!
Совет про 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(...).select('documents.type').pluck('documents.type')
Здесь мы получаем массив с нужным полем из БД. Плюсы: минимум запросов, не создаётся AR-объектов. Минусы: в Rails 3 pluck принимает только 1(один) параметр и вот такое
select('documents.type', 'files.filename', 'files.filename').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 к скорости, а инструмент, который нужно применять в правильных местах, а не махать им над головой и засовывать в каждую дырку. Вот тут можно почитать про типы индексов, а тут можно узнать зачем они вообще нужны.
За сим все
Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Так же буду рад, если поделитесь своим «наболевшим» и своими опытом о том, что надо помнить и чем лучше пользоваться в разных ситуациях при разработке.
Автор: Loriowar