Иногда требуется создать форму, данные которой связаны с несколькими таблицами. К примеру, у вас имеется две модели: Owner и Car. При добавлении нового Owner'a хотелось бы, чтобы была возможность сразу добавить машину. С появлением Rails 2.3 это стало проще.
А магия в волшебном параметре accepts_nested_attributes_for, который прописывается в модели и при создании объекта передает дополнительные параметры. Создается все это одной строкой:
Тем самым мы создаем сразу две записи: об Owner'e и о машине.
Рассмотрим другой пример, более подробно: модель Person должна быть связана сама с собой — при добавлении персоны мы сделаем возможность «добавлять детей», которые тоже обращаются к этой модели.
Первое, что надо сделать для ассоциации, это добавить строку accepts_nested_attributes_for
После этого вы сможете напрямую создавать, редактировать и удалять дочерние связи с объектом:
Для поддержки создания и редактирования объектов, нам следует использовать массив хешей при ассоциации один-ко-многим, или просто хеш при ассоциации один-к-одному. Если параметр хеша
Для удаления уже существующего связанного объекта, используйте такой способ:
В Rails 2.3.5 произошло переименование функции _destroy — ранее она называлась _delete. Не забывайте об этом, если работаете с устаревшей версией.
Возможно, все это выглядит небольшим хаком, но уже скоро мы убедимся, что работа с мульти-модельными формами на самом деле упростилась.
В view просто добавляем fields_for, и в ней пишем поля для этой модели:
Код создаст форму со всеми необходимыми полями, которая пойдет на RESTful-контроллер, а оттуда будут незаметно переданы параметры children_attributes на модель. Если при создании «детей» возникнут ошибки, то они добавятся к person.errors — в этом случае объект не сохранится в базе.
Несколько полезных заметок:
Третий шаг, наверное самый простой, потому что мы делаем все, не нарушая REST. Красота этого решения в том, что наши контроллеры не захламляются лишним кодом, которому место в модели. Просто посмотрите на эти методы создания и обновления:
Как видите, все что мы прописали в модели и в форме просто работает, без лишних обработок в контроллере.
Довольно часто требуется, чтобы вложенные поля отображались сразу. Например, если пользователь хочет создать новую персону и одновременно с этим добавить детей.
Из-за того что создаваемый обьект person создан только что, поля child_form не будут отображены. Есть два способа решения этой проблемы:
— Построить новый объект в контроллере:
— Можно добавить хелпер, который делает практически то же самое, но располагается в другом месте:
После этого надо поменять form_for person на немного другую:
Какой из этих способов использовать, каждый решает сам. Лично мне больше нравится первый, автору оригинальной статьи — второй.
Если вы создаете форму, в которой по умолчанию есть вложенные поля (Person → children), то кто-нибудь рано или поздно попытается отправить форму с пустыми параметрами (без детей). Можно сделать, чтобы пользователь получал ошибку — если дети обязательны для заполнения, и возвращался назад. Или просто создать объект без детей.
Для этого есть опция
Эта опция также будет полезна, если у вас есть boolean-поля в модели, и checkbox на форме. Если его не отметить и не написать имя ребенка, на контроллер передастся лишь параметр '0', и
Если вы строите сложную форму, в которой находятся большое количество моделей, да еще и вложенных в несколько уровней, то можно просто сделать несколько вложенных форм видимыми по умолчанию. Хотя это и не совсем рационально. Более привлекательный вариант — это динамически добавлять новые поля с помощью JavaScript'a по запросу пользователя.
Отличное приложение-пример можно посмотреть на GitHub, автором которого является Eloy. Посмотрев на него, вы поймете как работает целая система моделей и связей между ними.
# Старый вариант (приблизительный)
def create
@owner = Owner.new(params[:owner])
...
if @owner.save
@car = Car.new(params[:car])
if @car.save
...
end
# Новый вариант, Rails 2.3+
def create
@owner = Owner.new(params[:owner])
...
end
А магия в волшебном параметре accepts_nested_attributes_for, который прописывается в модели и при создании объекта передает дополнительные параметры. Создается все это одной строкой:
Owner.create(:name => "Шумахер", :age => 40, :car_attributes =>
{:model => "Formula 1", :color => "red"})
Тем самым мы создаем сразу две записи: об Owner'e и о машине.
Рассмотрим другой пример, более подробно: модель Person должна быть связана сама с собой — при добавлении персоны мы сделаем возможность «добавлять детей», которые тоже обращаются к этой модели.
Шаг 1: Сообщаем модели об использовании nested-атрибутов
Первое, что надо сделать для ассоциации, это добавить строку accepts_nested_attributes_for
сlass Person < ActiveRecord::Base
validates_presence_of :name
has_many :children, :class_name => 'Person'
accepts_nested_attributes_for :children, :allow_destroy => true
# также можно использовать с has_one и другими ассоциациями
end
После этого вы сможете напрямую создавать, редактировать и удалять дочерние связи с объектом:
# К персоне можно добавить чадо:
@person.children_attributes = [ { :name => 'Son' } ]
@person.children #=> [ <#Person: name: 'Son'> ]
@person.children.clear
# А теперь добавим сразу двух:
@person.children_attributes =
[ { :name => 'Son' }, { :name => 'Daughter' } ]
@person.save
@person.children #=> [ <#Person: name: 'Son'>, <#Person: name: 'Daughter'> ]
# Редактирование сына (считая что его id == 1)
@person.children_attributes = [ { :id => 1, :name => 'Lad' } ]
@person.save
#=> Теперь сына зовут 'Lad'
# Добавим дочери имя (id == 2) и заодно создадим нового отпрыска:
@person.children_attributes =
[ { :id => 2, :name => 'Lassie' }, { :name => 'Pat' } ]
@person.save
#=> Теперь дочку зовут 'Lassie', и появился некто 'Pat'
# Удалим Pat'a (id = 3), нам он не нравится
@person.children_attributes = [ :id => 3, '_destroy' => '1' } ]
@person.save
#=> Pat теперь удален
Для поддержки создания и редактирования объектов, нам следует использовать массив хешей при ассоциации один-ко-многим, или просто хеш при ассоциации один-к-одному. Если параметр хеша
:id
не задан, то будет создан новый объект. Для удаления уже существующего связанного объекта, используйте такой способ:
[{ :id => pk, '_destroy' => '1' }]
, где параметр '_destroy' должен принимать любое значение true. Не забудьте установить опцию :allow_destroy
в модели, по умолчанию она выключена.В Rails 2.3.5 произошло переименование функции _destroy — ранее она называлась _delete. Не забывайте об этом, если работаете с устаревшей версией.
Возможно, все это выглядит небольшим хаком, но уже скоро мы убедимся, что работа с мульти-модельными формами на самом деле упростилась.
Шаг 2. Создаем форму со вложенной моделью
В view просто добавляем fields_for, и в ней пишем поля для этой модели:
<% form_for @person do |person_form| %>
<%= person_form.label :name %>
<%= person_form.text_field :name %>
<% person_form.fields_for :children do |child_form| %>
<%= child_form.label :name %>
<%= child_form.text_field :name %>
<% unless child_form.object.new_record? %>
<%= child_form.check_box '_destroy' %>
<%= child_form.label '_destroy', 'Remove' %>
<% end %>
<% end %>
<%= submit_tag %>
<% end %>
Код создаст форму со всеми необходимыми полями, которая пойдет на RESTful-контроллер, а оттуда будут незаметно переданы параметры children_attributes на модель. Если при создании «детей» возникнут ошибки, то они добавятся к person.errors — в этом случае объект не сохранится в базе.
Несколько полезных заметок:
- При использовании fields_for со связью
:has_many
можно стать жертвой рекурсии, неосмотрительно добавив несколько вложенных моделей друг в друга, которые связаны ассоциациями - Если вам нужно изменить что-то в объекте вложенной модели, вы можете получить к нему доступ, используя child_form.object. В примере выше мы использовали child_form.object.new_record? чтобы определить, следует ли показывать галочку «Удалить» (для новых записей она не нужна)
Шаг 3. Что прописывать в контроллере?.. Ничего
Третий шаг, наверное самый простой, потому что мы делаем все, не нарушая REST. Красота этого решения в том, что наши контроллеры не захламляются лишним кодом, которому место в модели. Просто посмотрите на эти методы создания и обновления:
class PersonController < ApplicationController
def create
@person = Person.new(params[:person])
@person.save ? redirect_to(person_path(@person)) : render(:action => :new)
end
def update
@person = Person.find(params[:id])
@person.update_attributes(params[:person]) ?
redirect_to(person_path(@person)) : render(:action => :edit)
end
end
Как видите, все что мы прописали в модели и в форме просто работает, без лишних обработок в контроллере.
Дополнительно
Показываем все вложенные поля
Довольно часто требуется, чтобы вложенные поля отображались сразу. Например, если пользователь хочет создать новую персону и одновременно с этим добавить детей.
Из-за того что создаваемый обьект person создан только что, поля child_form не будут отображены. Есть два способа решения этой проблемы:
— Построить новый объект в контроллере:
def new
@person = Person.new
@person.children.build
# ...
end
— Можно добавить хелпер, который делает практически то же самое, но располагается в другом месте:
module ApplicationHelper
def setup_person(person)
returning(person) do |p|
p.children.build if p.children.empty?
end
end
end
После этого надо поменять form_for person на немного другую:
<% form_for setup_person(@person) do |person_form| %>
<!-- ... -->
<% end %>
Какой из этих способов использовать, каждый решает сам. Лично мне больше нравится первый, автору оригинальной статьи — второй.
Указываем, когда нам нужны вложенные модели
Если вы создаете форму, в которой по умолчанию есть вложенные поля (Person → children), то кто-нибудь рано или поздно попытается отправить форму с пустыми параметрами (без детей). Можно сделать, чтобы пользователь получал ошибку — если дети обязательны для заполнения, и возвращался назад. Или просто создать объект без детей.
Для этого есть опция
:reject_if
:class Person < ActiveRecord::Base
validates_presence_of :name
has_many :children, :class_name => 'Person'
# Запрещаем создание детей, у которых оба поля не заполнены
accepts_nested_attributes_for :children,
:reject_if => proc { |attrs| attrs.all? { |k, v| v.blank? } }
# Тот же метод, но более изящный
accepts_nested_attributes_for :children, :reject_if => :all_blank
end
# пустые параметры не создадут новую запись
@person.children_attributes = [ { :name => '' } ]
@person.save
@person.children.count #=> 0
Эта опция также будет полезна, если у вас есть boolean-поля в модели, и checkbox на форме. Если его не отметить и не написать имя ребенка, на контроллер передастся лишь параметр '0', и
:all_blank
уже не поможетclass Person < ActiveRecord::Base
validates_presence_of :name, :bad
has_many :children, :class_name => 'Person'
# Это предотвратит добавление пользователей, у которых пустое имя, и чекбокс не был отмечен
accepts_nested_attributes_for :children,
:reject_if => proc { |attrs| attrs['bad'] == '0' && attrs['name'].blank? }
# Можно было обойтись :reject_if => proc { |attrs| attrs['name'].blank? }
# чтобы запретить создавать детей без имени, отметив чекбокс (прим. перев.)
end
@person.children_attributes = [ { :name => '', :bad => '0' } ]
@person.save
@person.children.count #=> 0
Динамически загружаемые поля
Если вы строите сложную форму, в которой находятся большое количество моделей, да еще и вложенных в несколько уровней, то можно просто сделать несколько вложенных форм видимыми по умолчанию. Хотя это и не совсем рационально. Более привлекательный вариант — это динамически добавлять новые поля с помощью JavaScript'a по запросу пользователя.
Отличное приложение-пример можно посмотреть на GitHub, автором которого является Eloy. Посмотрев на него, вы поймете как работает целая система моделей и связей между ними.