Активный запись что такое
Active Record Pattern
Хочу рассказать о применении шаблона Active Record для C# на практике. Такой класс реализует извлечение и запись структуры в базу данных. Бизнес логика выносится на следующие уровни абстракции, где с таким объектом можно работать уже как с обычной структурой.
Центральный случай, который я буду рассматривать для примера — это работа со справочником Country из базы данных, который часто читается, но очень редко меняется.
Использование active record объекта в коде бизнес логики выглядит вот так:
Country не имеет публичного конструктора, и получение объектов возможно только через обращение к методу Dictionary All.
Теперь поподробнее о том, как устроен этот класс изнутри.
Конструктор записи
Благодаря приватности конструктора мы можем надеяться на корректное применение конструктора. И только внутри класса.
Например вот так:
sqlSelect – приватная константа, для чтения всех записей с нужным для конструктора порядком полей.
DBWrapper – самописный класс, инкапсулирующий работу с базой данных. Благодаря ему на этом уровне нам приходится работать только с интерфейсами, без указания конкретной реализации.
Add – добавление новой записи в общий реестр, скрыта для лаконичности кода в статье.
Словарь All
Тут тоже ничего сложного:
В итоге мы имеем отложенную загрузку справочника по первому обращению.
Приватная переменная _all использует только в этом куске кода. К сожалению C# не позволяет ограничить ее применение меньше чем на весь класс. Остается опасность ее применения например в публичных методах. Что станет настоящей проблемой при работе в нескольких потоках.
Это первый вопрос, который я хочу обсудить: как сильнее ограничить видимость переменной _all?
Синхронизация многопоточности
Такой способ отложенной загрузки пока не пригоден для работы в многопоточности, поэтому я добавил класс LoadStatus.
private static readonly LoadStatus statusCountryList = new LoadStatus(“country”);
Название для статуса нужно для его идентификации в списке статусов всех справочников. Но об этом позже.
Много макарон, зато теперь у нас есть многопоточность и отчет о здоровье нашего справочника, в качестве бонуса от архитектуры.
LoadStatus прячет в себе синхронизацию и сбор данных о здоровье справочника.
Кроме того, через обнуление LoadStatus появляется возможность перегрузить справочник на лету.
Именно ради этой возможности я отказался от readonly для _all.
Class Generic
Решение получилось настолько удобным и изящным, что я использую его в десятках справочников в всех проектах. И возникает огромное желания превратить этот код в generic class.
Однако синтаксис C# не позволяет этого сделать.
Что вы думаете о этом решении?
Какие могут быть способы для превращения этого решения в generic?
Активный запись что такое
Вы здесь, вероятно, для того, чтобы изучать веб-разработку (а иначе… вы, видимо, ошиблись с выбором места…). Независимо от того, в чем ваша цель – создать собственный сайт или начать карьеру веб-разработчика, самым важным навыком, который вы в итоге получите, является возможность мыслить логически и разбивать проблему на составляющие компоненты. После этого вы сможете обращаться к этим кусочкам по отдельности в разное время. В этом и заключается сущность проектирования и разработки.
Так как данные – это наиболее важная часть веб-приложения, вам должно быть очень интересно и то, как Rails взаимодействует с данными. К счастью, это одна из наиболее значимых вещей, которые Rails существенно улучшил по сравнению с опциями, доступными всего несколько лет назад. Active Record – это интерфейс, предоставляемый Rails для взаимодействия между базой данных и вашим приложением. Он позволяет вам структурировать модели таких данных, как пользователи, сообщения в блогах, комментарии, последователи и др., причем осуществляется это очень логичным путем, очень похожим на простой английский язык. Если он покажется вам сложным (что иногда бывает), просто представьте себе жизнь до Active Record.
Твердое понимание Active Record поможет вам сравнительно легко освоить оставшийся Rails. Вспомните, как несколько занятий назад Model в MVC была именно той частью, которая выполняет всю тяжелую работу. В этом уроке мы изучим основы работы с моделями от их установки до построения простых ассоциаций между ними. Как обычно, мы подразумеваем, что этот материал представляет собой высокоуровневый обзор, а материалы к прочтению дадут более глубокое понимание. Более продвинутые темы будут изучены в последующих уроках.
##Пункты для размышления##
Постарайтесь ответить на предложенные вопросы. После выполнения задания попробуйте ответить на них ещё раз
ORM расшифровывается как Object-Relational-Mapping (объектно-реляционное отображение). В сущности это обозначает, что Active Record берет данные, хранящиеся в строках и столбцах таблицы базы данных, которые должны быть модифицированы или возвращены с помощью SQL инструкций (если вы используете базу данных SQL), и позволяет вам взаимодействовать с этими данными так, будто это обычный Ruby объект.
Еще более впечатляющим является то, что на самом деле не имеет никакого значения, каким типом базы данных вы пользуетесь (пока вы правильно задаете файл config/database.yml ), Active Record сглаживает все различия между базами данных так, что вам уже не надо об этом думать. Вы сосредотачиваетесь на написании кода приложения, а Active Record будет думать о мельчайших подробностях того, как соединить его с вашей базой данных. Это также обозначает, что если вы переключаетесь с одной базы данных на другую, вам не нужно менять код любого крупного приложения, только некоторые конфигурационные файлы. Звучит логично, не так ли?
###30 секунд о работе между моделями###
Это сэкономит вам немного времени, но, как вы увидите позже, иногда вам захочется разделить их в своем приложении.
Это все было всего лишь вступлением на тему, что может делать Active Record. Дальше вы научитесь взаимодействовать с Active Record в ваших моделях.
[*] если только вы не передали Rails генератору названия необходимых колонок, в этом случае они появятся автоматически в полях миграции. Генераторы позволяют передавать им очень многое в качестве аргументов.
Итак, что такое миграция? Миграции – это в сущности скрипт, который сообщает Rails, как вы хотите настроить или изменить базу данных. Это другая сторона магии Active Record, которая позволяет вам избежать написания SQL-кода вручную, чтобы создать таблицу базы данных. Вы просто задаете определенный Ruby метод (как нельзя лучше названный create_table ) и его параметры и получаете все готовым к использованию.
Почему это полезно? Очевидно, что это позволяет вам настроить базу данных, используя дружелюбный код Ruby вместо SQL, но это еще не все. Через какое-то время вы построите целую кучу таких файлов миграции. Если однажды вы решите, что хотите выбросить свою базу данных и создать новую с нуля, вы легко сможете это сделать и затем перезапустить свои миграции. Если вы решите развернуть проект в сети, вы запустите те же самые миграции и итоговая база данных уже будет ждать вас… даже если база данных другого типа! И еще раз, Active Records делает за вас всю тяжелую работу, а вам остается только сфокусироваться на построении сайта.
###Сколько я должен знать о базах данных?###
Миграции не требуют написания SQL, однако вам необходимо понимать достаточно о базах данных, чтобы знать, как вы хотите структурировать свою базу. Какие колонки вам нужны? Какие должны быть индексированы (и почему)? Нужно ли задавать значения по умолчанию? Какой тип данных будет храниться в ваших колонках – строка (string) или текст (text)?
Это серьезные вопросы, и нужно чувствовать себя комфортно, задавая их, даже если вы не слишком уверены в ответах. Если вы не понимаете, о чем я, то вам нужно вернуться назад и прочитать об основах баз данных в предыдущем уроке.
##Базовые валидации (Basic Validations)##
Представьте, что у вас есть работающая база данных и вы хотите проверить, являются ли данные, пересылаемые другими людьми в вашу базу, хорошими. Например, чтобы создать аккаунт на сайте, пользователю нужно ввести имя пользователя (username) и адрес электронной почты. Как обеспечить соблюдение этого?
Существуют три уровня валидации, чье выполнение вы можете обеспечить, каждый последующий более строгий и безопасный, чем предыдущий. На верхнем уровне, вы можете писать код с использованием Javascript в вашем браузере и выяснить, правильно ли заполнена форма, потребовать ее верного заполнения до того, как двигаться дальше. Мы изучим это более подробно в курсе о Javascript. Преимущество здесь в том, что это практически мгновенно, а значит хорошо для пользователя. А проблема здесь в том, что Javascript легко перехитрить и пользователь легко может отправить вредоносный или дефектный запрос.
Второй уровень обеспечения валидации данных пользователя (которым вы никогда не должны слепо доверять) – это валидация на уровне сервера. Это означает написание кода в вашем Rails приложении (особенно в модели, чей экземпляр вы пытаетесь сохранить, например, User), который будет проверять входные данные пользователя, сверять их с ограничениями, заданными вами, и возвращать ошибки, если они имеют место быть.
Это более безопасно, чем javascript, но обладает таким недостатком, как требования полного двустороннего HTTP запроса для проверки. Валидации моделей обычно достаточно эффективны и это именно то, на чем мы заострим внимание.
Другая проблема возникает, когда ваше приложение разрастается до того, что на множестве серверов запущено сразу несколько его экземпляров, и все эти серверы соединены с одной и той же центральной базой данных. Допустим, вы хотите проверить, является ли имя пользователя уникальным… Что происходит, когда два пользователя одновременно отправляют одинаковые имена пользователей, и получают их отдельные параллельно работающие экземпляры вашего приложения? Когда каждый из экземпляров приложения сверяется с базой данных, уникально ли имя пользователя, оба раза все выглядит прекрасно, оба продолжают работу, сохраняют модель и… упс! Звучит маловероятно, но что если подумать об оперативно возникающих автоматизированных транзакциях? Такой «режим соперничества» становится очень правдоподобным.
##Базовые ассоциации / связи (Basic associations)##
Ключевое отличие здесь в том, что мы говорим не о том, сколько в данный момент сообщений у пользователя или сколько объектов местного франчайзинга в городе прямо сейчас, мы пытаемся смоделировать, сколько всего они МОГЛИ БЫ ИМЕТЬ в течение всего жизненного цикла вашего приложения. Конечно, если сегодня у вашего Пользователя только одно сообщение, вы можете вписать ID этого сообщения в вашу таблицу пользователей. Потом еще одно сообщение (Post) и еще один столбец с ID. И еще один. И еще. Но в этом нет смысла, и именно поэтому мы говорим, что Пользователь (User) has_many :posts и оставляем таблицу сообщений (Post table) жестко закрепленной за ID пользователя (User ID), в то время как каждое сообщение в отдельности будет ассоциировано только с одним Пользователем (допустим, у нас есть только один автор).
Если вы нормальный человек, то вы сейчас, вероятно, находитесь где-то между «чего?» и «ненавижу тебя, перестань учить меня всякой ерунде!». Не бросайте это, суть в том, чтобы приучить вас думать, как смоделировать отношения и дать вам представление о них. Проект даст вам возможность на практике построить то, что вы изучаете.
Active Record является одной из мощнейших частей Rails, но в то же время и одной из самых запутанных для освоения. Вам понадобится переносить реальный мир в таблицы баз данных, а на то, чтобы с этим познакомиться поближе, требуется какое-то время. Наиболее сложной концепцией для новичков обычно являются ассоциации.
Проще начинать думать о конкретных отношениях в реальном мире, а потом уже превращать их в ассоциации Active Record. Когда вы почувствуете себя уверено в том, какая модель обаладает ( has_many ) какими другими моделями и кто из них кому принадлежит ( belongs_to ), вы можете начинать моделировать более абстрактные концепции, скажем, приглашения на какое-то мероприятие (event invitations) или принятые приглашения (invitation acceptances), которые не так очевидны как ребенок и его мраморные шарики (пример, который мы использовали раньше).
Это все придет с практикой, таким образом, начиная с этого момента проекты будут требовать от вас продумывания модели организации данных до того, как вы начнете. Несколько минут на предварительные размышления об отношениях между данными играют ключевую роль в выборе правильного направления, когда вы начинаете писать свой код.
Этот раздел содержит полезные ссылки на дополнительные материалы. Они не обязательны, так что расценивайте их как нечто полезное, если вы хотите поглубже погрузиться в тему
Active Record против Data Mapper-а для сохранения данных
Эти 2 шаблона проектирования описаны в книге Мартина Фаулера «Шаблоны корпоративных приложений» и представляют собой способы работы с сохранением данных в объектно-ориентированном программировании.
Пример шаблона Active Record
В этом упрощенном примере, дескриптор базы данных вводится в конструкторе Foo (Использование инъекции зависимостей здесь позволяет тестировать объект без использования реальной базы данных), и Foo использует его, чтобы сохранять свои данные. Do_something — просто метод-заглушка, заменяющий бизнес логику.
Преимущества Active Record
Недостатки Active Record
Пример Data Mapper-а
В данном случае, класс Foo намного проще и должен беспокоиться только о своей бизнес-логике. Он не только не должен сохранять собственные данные, он даже не знает и не заботится о том, все ли его данные были сохранены.
Преимущества Data Mapper-а
Недостатки Data Mapper-а
Сервис-объекты
При использовании шаблона проектирования Data Mapper, вызывающий код должен выбрать Mapper и бизнес-объект и связать их вместе. Если это код вызова в контроллере, то в конечном счете ваша модель «утекает» в контроллер, что может вызвать большие проблемы при поддержке и юнит-тестировании. Эта проблема может быть решена путем введения объекта-сервиса. Сервис является воротами между контроллером и моделью и связывает доменный объект с Mapper-ом по мере необходимости.
Следует помнить, что M в MVC, представляет собой слой абстракции модели, а не объект модели. Так может быть несколько типов объектов в одной модели (в приведенном выше примере, у вас может быть объект сервиса, доменный объект и объект Mapper-а, выступающие в роли единой модели). С другой стороны, если вы используете модели Active Record, ваша модель может быть представлена лишь одним объектом.
Варианты использования
Объекты Active Record исторически были очень популярны из-за того, что они проще, легче в понимании и быстрее в написании, поэтому многие фреймворки и ORM используют Active Record по умолчанию.
Если вы уверены, что вам никогда не понадобиться менять слой сохранения данных (если вы имеете дело с объектом, который представляет из себя INI-файл, например), или вы имеете дело с очень простыми объектами, в которых не так много бизнес-логики, или просто предпочитаете держать все в небольшом количестве классов, тогда шаблон Active Record это то, что вам нужно.
Использование Data Mapper-а хотя и ведет к более чистому, простому в тестировании и поддержке коду, и обеспечивает большую гибкость, — цена этому, — повышение сложности. Если вы еще не пробовали его использовать, то дайте ему шанс, — вам должно понравиться.
Прикручиваем ActiveRecord к сайту
Введение
Как работает ActiveRecord?
Компонент представляет из себя набор основных классов, необходимых для работы(Model,Config, ConnectionManager и др.), набор адаптеров для подключения к конкретной СУБД и точки входа, файла инициализации ActiveRecord.php который содержит функцию автозагрузки классов наших моделей проекта. Все классы определенны в пространстве имен ActiveRecord, наш проект скорее всего будет находится в другом пространстве или в глобальном, поэтому, чтобы при наследовании классов каждый раз не писать конструкции вроде extends \ActiveRecord\Model или использовать директиву use ActiveRecord, имеет смысл создать собственную обертку над ActiveRecord. Это также позволит расширить возможности нашей ORM не затрагивая компонент AR.
Итак, чтобы воспользоваться всеми методами AR, нам необходимо подключить файл инициализации ActiveRecord.php к проекту, создать для каждой таблицы в БД класс-модель и унаследовать его от \ActiveRecord\Model(например class Book extends \ActiveRecord\Model <> ), инициализировать подключение к БД с помощью конструкции:
После этого мы можем обращаться к нашим моделям и вызывать необходимые методы, например Book::first() — вернет первую строку из таблицы определенной в модели Book.
Создание обертки AR
В проекте возможно потребуется обращение к БД из разных файлов, да и конфигурация обычно храниться в отдельном файле, стандартных возможностей AR не всегда хватает и сама форма записи через пространство имен \ActiveRecord не очень красиво. Эта тема тянет на несколько статьей, поэтому здесь я постараюсь изложить суть вопроса.
В простом случае нам потребуется создать всего 2 класса, один мы наследуем от \ActiveRecord\Model и другой будет основным, в котором мы будем проводить инициализацию и конфигурацию AR. Создадим 2 файла-класса:
От класса Model мы будем наследовать все модели существующих таблиц. Также предположим, что вся конфигурация приложения хранится в отдельном файле Configuration.php:
В конструкторе класса Orm(этот код взят из ActiveRecord.php) подключаем необходимые классы и регестрируем автозагрузчик, в самом конце инициализируем подключение к БД.
Особое внимание стоит уделить формату времени, если его оставить по дефолту, то во время операций записей данных в БД поля типа datetime будут генерировать ошибку, т.к. AR генерирует строки в формате 2000-02-03 16:23:27 MSK, т.е. указывает индекс часового пояса. Изменить конфиг не достаточно, не знаю почему, но разработчики AR используют в других классах формат даты и времени не из конфига, а явно указывают его в требуемых методах, поэтому придется внести еще измения в следующие файлы:
/lib/Column.php метод cast
Аналогично в файлах /lib/Connection.php методы datetime_to_string() string_to_datetime(), и /lib/Model.php метод assign_attribute().
Теперь приведу пример как можно всем этим пользоваться. Сначала нам нужно создать переменную в которой мы будем хранить объект нашего класса Orm, эта переменная должна быть доступна в любом нужном нам месте любого скрипта, поэтому ее лучше объявлять как статическую главного Контроллера или глобальную. После создания объекта необходимо в массив _models поместить массив всех моделей используемых в проекте, формат массива можно узнать в комментарии в коде. Вот возможный пример реализации всего сказанного:
Конечно, данный способ требует еще доработки, например можно сделать статические методы у Orm класса, тогда при запуске проекта нам нужно будет инициализировать его, а дальше везде использовать конструкцию вроде Orm::getModel(‘Имя модели’);
AR довольно мощный и гибкий инструмент, в нем поддерживаются кроме стандартных операций CRUD, также и связи между таблицами(включая сложные связи через — through), имеется SQLBuilder для построения SQL запросов, валидация, конвертация и др.
Официальная документация на английском и в ней освещены элементарные вопросы, есть также форум, на котором можно найти большинство ответов по работе с AR, но я так и не смог нагуглить более мене нормального источника с информацией о внедрении AR в собственный фреймворк или простой движек сайта.
По ходу своей работы мне пришелось в плотную сталкнуться с данной библиотекой, и если эта тема интересна, то я продолжу данный цикл статьей по ActiveRecord.
ActiveRecord немного про грабли, Relations и индексы
Хочу рассказать Вам о наболевшем: о работе с AR в целом и с Relation в частности; предостеречь от стандартных садовых изделий, которые легко могут испортить жизнь и сделать код медленным и прожорливым. Повествование будет основываться на Rails 3.2 и ActiveRecord того же разлива. В Rails 4, конечно же, много чего нового и полезного, но на него ещё перейти нужно, да и фундамент в любом случае один и тот же.
Уж сколько раз твердили миру…
Если вы начали работать с Relation (да и с любым ActiveRecord объектом вообще), то нужно чётко представлять одну вещь: в какой момент мы «овеществляем» выборку, то есть в какой момент мы перестаём конструировать SQL-запрос. Иначе говоря: когда происходит выборка данных и мы переходим к из обработке в памяти. Почему это важно? Да потому что неловкое:
Выполняем его, получаем одну строку и всё.
Что такое хорошо и что такое плохо
Теперь, разобравшись почему работа с 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 почему-то часто забывают. Эти метод пропускают и валидации, и колбэки и пишут прямо в базу без всяких предварительных ласк.
Рецепт номер два
В комментариях напомнили про замечательный метод touch. О нём тоже частенько забывают и пишут что-то вроде
Хотя, по хорошему, для таких целей проще сделать вот так:
К тому же для touch есть собственный колбэк after_touch, а так же опция :touch присутствует у метода belongs_to.
Как правильно итерировать
В хабе уже говорили про find_each, но я не могу не упомянуть его ещё раз, ибо конструкции
и им изоморфные, встречаются чуть более чем везде. Проблема в обычных итераторах, применённых на Relation только одна: они вытаскивают из БД сразу всё. И это ужасно. В противоположность им find_each, по умолчанию, таскает по 1000 штук за раз и это просто прекрасно!
UPD: как уже отмечали в комментариях, все методы, которые однозначно не проецируются на raw-sql делегируются в to_a из-за чего и происходит выборка всего запроса в память и работа с ним уже не на стороне БД, а на стороне Ruby.
Совет про default_scope
Оборачивайте содержимое default_scope в блок. Пример:
В чём разница? В том, что первый вариант выполняется прямо при запуске сервера и если поля nullified в БД не оказалось, то и сервер не взлетит. То жесамое относится и к миграциям — они не пройдут из-за отсутствия поля, которое, скорее всего, мы как раз хотим добавить. Во втором случае, в силу того, что Ruby ленив, блок выполнится только в момент обращения к модели и миграции выполнятся штатно.
Has_many through
Ещё один часто встречающийся пациент это
здесь продукт имеет много документов, которые имеют много строк. Часто бывает, что хочется получить все строки всех документов, относящихся к продукту. И в таком случае творят вышеописанную конструкцию. В данном случае можно вспомнить про опцию through для реляций и сделать для продукта следующее:
Получается и нагляднее и эффективнее.
Немного про JOIN
В продолжение темы джоинов вспомним про includes. Что в нём особенного? Да то, что это LEFT JOIN. Довольно часто вижу, что левый/правый джоин пишут явно
это конечно тоже работает, но чистый SQL в RoR всегда был не в почёте.
Так же, не отходя от кассы, надо напомнить про разницу значений в joins и where при совместном использовании. Допустим у нас есть таблица users, а разные сущности, например products имеют поле author_id и реляцию author, кояя имеет под собой таблицу users.
Следующий код для такого случая работать не будет
Почему? Потому что в joins указывается имя реляции, которую джоиним, а в where накладывается условие на таблицу и надо говорить
Избежать такого можно явным указанием ‘AS author’ в джоине, но это снова будет чистый SQL.
Далее посмотрим на джоины с другого ракурса. Что бы мы не джоинили, в итоге мы получаем объекты класса, с которого всё начиналось:
В данном случае получаем продукт вне зависимости от количества джоинов. Некоторых это поведение огорчает, так как им хотелось бы получить поля из приджойненных таблиц. И они начинают делать этот же запрос с другой стороны: брать документы, джоинить их с продуктами, писать чистый SQL для связи с другими сущностями, вобщем изобретают велосипед, когда правильный и логичный код был написан в самом начале. Поэтому напомню самую основу:
Здесь мы получаем массив с нужным полем из БД. Плюсы: минимум запросов, не создаётся AR-объектов. Минусы: в Rails 3 pluck принимает только 1(один) параметр и вот такое
можно будет сделать только в Rails 4.
Build реляций
Теперь обратимся к рассмотрению работы с build-ом реляций. В общем случае всё довольно просто:
После вызова product.save у нас будет происходить сохранение всех ассоциаций вместе с валидациями, преферансом и куртизанками. Во всём этом радостном действе есть один нюанс: всё это хорошо, когда product не readonly и/или нет иных ограничений на сохранение. В таких случаях многие устраивают огород, аналогичный огороду с joins в примере выше. То есть создают document, привязывают его к product и build-ят строки для документа. Получается кривова-то и дефолтное поведение, которое, обычно, завязано на обработку ошибок product не работает. Поэтому в довесок всё это сразу же обставляют костылями, пробрасывающими ошибки и получается довольно мерзко. Что делать в таком случае? Надо вспомнить про autosave и понять как он работает. Не вдаваясь в детали скажу, что работает он на callback-ах. Поэтому способ сохранить реляции для вышеописанного продукта есть:
В этом случае случится сохранение документа, вызовутся его колбэки для сохранения строк и т.д.
Несколько слов об индексах
На последок нужно сказать про индексы, ибо многие бились головой об твёрдые предметы из-за проблем на почве индексов. Сразу прошу прощения что мешаю в кучу ActiveRecord и возможности БД, но по личному убеждению: нельзя хорошо работать с AR, не осознавая что происходит в этот момент на стороне БД.
Проблема первая
Проблема вторая
Проблема третья
«Индексы нужны везде!». Встречается не так часто, но вызывает страшные тормоза всего и вся. Нужно помнить, что индекс — это не панацея и гарантированный х10-х100 к скорости, а инструмент, который нужно применять в правильных местах, а не махать им над головой и засовывать в каждую дырку. Вот тут можно почитать про типы индексов, а тут можно узнать зачем они вообще нужны.