Pull to refresh

Создание мульти-модельных форм

Ruby on Rails *
Translation
Original author: Ryan
Иногда требуется создать форму, данные которой связаны с несколькими таблицами. К примеру, у вас имеется две модели: Owner и Car. При добавлении нового Owner'a хотелось бы, чтобы была возможность сразу добавить машину. С появлением Rails 2.3 это стало проще.

# Старый вариант (приблизительный)
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. Посмотрев на него, вы поймете как работает целая система моделей и связей между ними.
Tags:
Hubs:
Total votes 64: ↑54 and ↓10 +44
Views 18K
Comments Comments 16