fields_for 사용기

여러개의 모델 객체를 하나의 form_for 헬퍼메소드에서 사용하는 것이 그렇게 쉽게 되지 않는 것 같습니다. 여기에는 약간의 꽁수가 필요하더군요.

설명을 위해서 리소스를 아래와 같이 가정하겠습니다.

class Person < ActiveRecord::Base

  has_one :contact, ;dependent => :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 %>

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

이렇게 has_one 관계로 여러개의 모델을 연결하면, 테이블의 컬럼이 너무 많아 여러개의 테이블로 나누어서 작업해야 할 경우에 유용할 것같습니다. 그러나 실제로 하나의 테이블에 어느 정도의 컬럼갯수를 가지는 것이 효율적인지는 모르겠습니다.

여기서는 has_one 연결을 한 예를 소개했지만, has_many 관계로 연결된 모델객체를 사용할 때는 가상 속성의 정의와 폼 뷰 템플릿의 fields_for로 넘겨주는 첫번째 문자열 인수를 약간 변경해야 합니다. 예를 들면, Person 모델이 has_many :contacts이고, Contact가 belongs_to :person이라고 가정하면,

class Person < ActiveRecord::Base

  has_one :contact, ;dependent => :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 라고 수정해 주어야 하겠죠.

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

글쓴이: 최효성

외과전문의,웹프로그래밍,컴퓨터 일러스트레이션 / Surgeon, Medical Illustration, Web Programmer

2 thoughts on “fields_for 사용기”

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

  2. 핑백: Ruby & Rails

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중