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

54f8e65ee08e4628a2078a497aafbf0c.png Хочу рассказать Вам о наболевшем: о работе с 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. Подумайте, какие её параметры вы знаете и какими активно пользуетесь? Перечислите их в уме, посчитайте…, а теперь вопрос: знаете ли вы сколько их на самом деле? Ответ 24 штуки в Rails3 и 12 в Rails4. Разницу в 12шт составляют методы типа where, group и тд, а так же методы для работы с чистым SQL, которые в Rails4 передаются в блоке, а не в хэше.

Зачем я это спросил? Да чтобы очень приблизительно оценить Ваш уровень и сказать, что ежели большую часть опций Вы знаете, то и нижеизложенное вряд ли принесёт Вам новые знания. Оценка эта очень условная, поэтому, уважаемый Читатель, не гневайся сильно, ежели она показалась Тебе нелепой/несостоятельной/странной/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 к скорости, а инструмент, который нужно применять в правильных местах, а не махать им над головой и засовывать в каждую дырку. Вот тут можно почитать про типы индексов, а тут можно узнать зачем они вообще нужны.За сим все Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Так же буду рад, если поделитесь своим «наболевшим» и своими опытом о том, что надо помнить и чем лучше пользоваться в разных ситуациях при разработке.

© Habrahabr.ru