fields_for 사용기

여러개의 모델 객체를 하나의 form_for 헬퍼메소드에서 사용하는 것이 그렇게 쉽게 되지 않는 것 같습니다.

여기에는 약간의 꽁수가 필요하더군요. 설명을 위해서 리소스를 아래와 같이 가정하겠습니다.

class Person  :destroy 
  attr_accessible :age, :name, :sex, :contact_attribute 

  def contact_attribute=(attribute) 
    build_contact(attribute) 
  end 
end 

class Contact < ActiveRecord::Base 
  belongs_to :person 
  attr_accessible :person, :address, :telephone 
end 

config/routes.rb 파일에서 리소스 정의는 다음과 같습니다.

Hasone::Application.routes.draw do 
  root :to => "people#index" 
  resources :people do 
    resource :contact 
  end 
end 

여기서 주의해야 할 것은, has_one 관계시에 resource :contact 와 같이 단수형 resource와 단수형 :contact를 사용해야 합니다. 그리고, app/controllers/people_controller.rb 파일에서 new 액션과 edit 액션에서 각각 @contact 인스터스를 생성해 주었습니다.

class PeopleController < ApplicationController 

  # GET /people 
  # GET /people.json 
  def index 
    @people = Person.all 

    respond_to do |format| 
      format.html # index.html.erb 
      format.json { render json: @people } 
    end 
  end 

  # GET /people/1 
  # GET /people/1.json 
  def show 
    @person = Person.find(params[:id]) 

    respond_to do |format| 
      format.html # show.html.erb 
      format.json { render json: @person } 
    end 
  end 

  # GET /people/new 
  # GET /people/new.json 
  def new 
    @person = Person.new 
    @contact = @person.build_contact 

    respond_to do |format| 
      format.html # new.html.erb 
      format.json { render json: @person } 
    end 
  end 

  # GET /people/1/edit 
  def edit 
    @person = Person.find(params[:id]) 
    @contact = @person.contact 
  end 

  # POST /people 
  # POST /people.json 
  def create 
    @person = Person.new(params[:person]) 

    respond_to do |format| 
      if @person.save 
        format.html { redirect_to @person, notice: 'Person was successfully created.' } 
        format.json { render json: @person, status: :created, location: @person } 
      else 
        format.html { render action: "new" } 
        format.json { render json: @person.errors, status: :unprocessable_entity } 
      end 
    end 
  end 

  # PUT /people/1 
  # PUT /people/1.json 
  def update 
    @person = Person.find(params[:id]) 

    respond_to do |format| 
      if @person.update_attributes(params[:person]) 
        format.html { redirect_to @person, notice: 'Person was successfully updated.' } 
        format.json { head :no_content } 
      else 
        format.html { render action: "edit" } 
        format.json { render json: @person.errors, status: :unprocessable_entity } 
      end 
    end 
  end 

  # DELETE /people/1 
  # DELETE /people/1.json 
  def destroy 
    @person = Person.find(params[:id]) 
    @person.destroy 

    respond_to do |format| 
      format.html { redirect_to people_url } 
      format.json { head :no_content } 
    end 
  end 
end 

@contact 인스턴스는 fields_for 헬퍼메스드의 매개변수로 넘겨 주었구요. people 컨트롤러에서 params[:person]으로 넘겨 받을 때 contact 속성값들도 함께 받기 위해서는 Person 모델에서 가상 속성을 정의해 주어야 합니다.

이미 위의 모델 정의에서 기술된 바와 같이 contact_attribute=(attribute) 라고 정의를 하게 되는 것입니다. 이 속성의 구현내용은 메소드로 넘겨받은 attribute에 대해서 person 객체의 contact 객체를 만들게 됩니다.

그것은 has_one 관계로 연결되어 있기 때문에 self.contact.build와 같이 생성하는 것이 아니라 self.build_contact(attribute)와 같이 코딩해 주어야 합니다. 이것은 레일스의 약속입니다.

다음은 app/views/people/_form.html.rb 파일을 보겠습니다.

지금까지 설명한 내용을 생각하면서 아래의 fields_for로 넘기는 매개변수 항목들을 눈여겨 볼 필요가 있습니다. 여기서는 첫번째 인수로 person[contact_attribute]라고 문자열을 넘겨 주었지만, Person 모델에 추가로 정의한 가상 속성인 contact_attribute를 이용해서 @person.contact_attribute 라고 해도 될 것이라고 생각했었습니다만, 실제로 그렇게 했을 때, contact_attribute라는 메소드가 정의되어 있지 않다는 에러를 발생시키더군요. 이렇게 가상 속성을 구현해서 fields_for를 사용시, 실제 가상속성에 대해서 구현하는 방법에 대해서는 rdoc과 레일스 가이드에 설명이 부족하더군요.

<%= form_for(@person) do |f| %> 
  <% if @person.errors.any? %> 
    <div id="error_explanation"> 
      <h2><%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved:</h2> 
      <ul> 
        <% @person.errors.full_messages.each do |msg| %> 
          <li><%= msg %></li> 
        <% end %> 
      </ul> 
    </div> 
  <% end %> 
  <div class="field"> 
    <%= f.label :name %> 
    <%= f.text_field :name %> 
  </div>
  <div class="field"> 
    <%= f.label :age %> 
    <%= f.text_field :age %>
  </div>
  <div class="field">
   <%= f.label :sex %>
   <%= f.text_field :sex %> 
  </div>
  <%= fields_for "person[contact_attribute]", @contact do |contact_form| %> 
    <div class="field"> 
      <%= contact_form.label :address %>
      <%= contact_form.text_field :address %> 
    </div>
    <div class="field">
      <%= contact_form.label :telephone %>
      <%= contact_form.text_field :telephone %> 
    </div> 
  <% end %>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %> 

역시 Ryanrailscasts.com에 있는 http://railscasts.com/episodes/73-complex-forms-part-1 를 보고 많은 공부가 되었습니다.

이렇게 has_one 관계로 여러개의 모델을 연결하면, 테이블의 컬럼이 너무 많아 여러개의 테이블로 나누어서 작업해야 할 경우에 유용할 것같습니다. 그러나 실제로 하나의 테이블에 어느 정도의 컬럼갯수를 가지는 것이 효율적인지는 모르겠습니다. 여기서는 has_one 연결을 한 예를 소개했지만, has_many 관계로 연결된 모델객체를 사용할 때는 가상 속성의 정의와 폼 뷰 템플릿의 fields_for로 넘겨주는 첫번째 문자열 인수를 약간 변경해야 합니다. 예를 들면, Person 모델이 has_many :contacts이고, Contactbelongs_to :person이라고 가정하면,

class Person  :destroy 
  attr_accessible :age, :name, :sex, :contact_attributes 

  def contact_attributes=(attributes) 
    attributes.each { |attribute| contact.build(attribute) } 
  end 
end 

와 같이 수정해 주어야 하고, people_controller.rb파일에서는

# GET /people/new 
# GET /people/new.json 
def new 
  @person = Person.new 
  3.times { @person.contacts.build } 

  respond_to do |format| 
    format.html # new.html.erb 
    format.json { render json: @person } 
  end 
end 

# GET /people/1/edit 
def edit 
  @person = Person.find(params[:id]) 
end 

_form.html.erb 에서는

<% for contact in @contacts %> 
  <% fields_for "person[contact_attributes][]", contact do |contact_form| %> 
    <div class="field"> 
      <%= contact_form.label :address %> 
      <%= contact_form.text_field :address %> 
    </div> 
    <div class="field"> 
      <%= contact_form.label :telephone %> 
      <%= contact_form.text_field :telephone %> 
    </div> 
  <% end %> 
<% end %> 

와 같이 수정해야 합니다. 물론 라우터에서는

Hasone::Application.routes.draw do 
  root :to => "people#index" 
  resources :people do 
    resources :contacts 
  end 
end 

와 복수형으로 resources :contacts 라고 수정해 주어야 하겠죠.

레일스를 처음시작하는 분들을 위해서 저의 작은 경험을 함께 공유합니다.

2 Comments

  1. 루비에 막 입문한 초보자 입니다. 다른언어와 다른 특성때문에 잘 이해가 안되는 부분이 많아 이것저것 찾아보는 중입니다. 특히 DB관련이 그렇네요. 좋은글 감사합니다.

  2. 핑백: Ruby & Rails

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.