http://habrahabr.ru/post/242225/
Хочу рассказать Вам о наболевшем: о работе с
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 почему-то часто забывают. Эти метод пропускают и валидации, и колбэки и пишут прямо в базу без всяких предварительных ласк.
Рецепт номер два
В комментариях напомнили про замечательный метод
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 штук за раз и это просто прекрасно!
Совет про 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 к скорости, а инструмент, который нужно применять в правильных местах, а не махать им над головой и засовывать в каждую дырку. Вот
тут можно почитать про типы индексов, а
тут можно узнать зачем они вообще нужны.
За сим все
Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Так же буду рад, если поделитесь своим «наболевшим» и своими опытом о том, что надо помнить и чем лучше пользоваться в разных ситуациях при разработке.