먼저 History 모델을 세부적으로 바꾸기 위해 다음과 같은 작업을 하였다.
- 필드명 변경
price → total_price
point → point_used
is_cash → is_credit
- 필드 추가
point_accumulated :integer
- 필드 삭제
note :text
content_id :integer
- 하위 테이블 생성
detail_histories history:references content:references price:integer note:text
위의 작업을 db에서 직접 ddl을 통해 변경할 수 있지만 rails의 마이그레이션을 통해 루비 코드 형태로 정의된 데이터베이스 스키마를 만들 수 있다.
먼저 마이그레이션 파일을 만든다.
$ rails g migration AddDetailsToHistory
/db/migrate 폴더에 새로운 마이그레이션 파일이 생성된다.
db/migrate/20161216164045_add_details_to_history.rb
class AddDetailsToHistory < ActiveRecord::Migration[5.0]
def change
add_column :histories, :point_used, :integer
rename_column :histories, :point, :point_accumulated
rename_column :histories, :is_cash, :is_credit
rename_column :histories, :price, :total_price
remove_column :histories, :note, :text
remove_column :histories, :content_id, :integer
end
end
필드 추가는 add_column 메소드를, 필드명 변경은 rename_column, 필드 삭제는 remove_column 메소드를 사용한다.
다음으로 detail_history 모델을 생성하기 위해 아래의 명령어를 입력한다.
$ rails g model detail_history history:references content:references price:integer note:text
모델을 생성하면 데이터베이스에 해당 모델 형식을 갖는 테이블을 생성하기 위한 마이그레이션 파일이 /db/migrate에 자동으로 생성된다.
지난 시간에 association관계 설정을 위해 모델 클래스에 belongs_to, has_many 등의 관계를 설정해 두었다. 아래와 같이 수정하였다.
app/models/content.rb
class Content < ApplicationRecord
belongs_to :category
has_many :detail_histories
# 생략
end
app/models/history.rb
class History < ApplicationRecord
belongs_to :member
has_many :details_histories
end
app/models/detail_history.rb
class DetailHistory < ApplicationRecord
belongs_to :history
belongs_to :content
end
같은 방법으로 Member 모델에 성별과 연령대를 추가한다.
$ rails g migration AddGenderAndAgesToMembers gender:string ages:string
마이그레이션명 뒤에 직접 추가할 필드명과 타입을 적어 자동으로 마이그레이션 파일에 추가시킨다.
rails erd 플러그인을 이용하여 erd를 확인해 본다.
플러그인: http://rails-erd.rubyforge.org
$ rake erd
현재 테이블에는 not null이나 default 제약조건이 전혀 설정되어있지 않다. 모든 테이블에 필요한 제약조건을 추가하였다. member테이블의 예는 다음과 같다.
$ rails g migration AlterConstraintToMembers
db/migrate/20161216182645_alter_constraint_to_members.rb
class AlterConstraintToMembers < ActiveRecord::Migration[5.0]
def change
change_column_null :members, :name, false
change_column_default :members, :ages, 'unknown'
change_column_null :members, :gender, false
change_column_default :members, :acc_point, 0
change_column_default :members, :use_point, 0
change_column_default :members, :rem_point, 0
end
end
포인트의 기본값을 0으로 설정했으므로 앞선 개발과정에서 컨트롤러에서 기본값을 추가 했던 코드는 삭제해도 좋을 것이다.
마지막으로 외래키로 연결된 데이터가 삭제 될 때 CASCADE 형태로 삭제 될 것인지, NOT NULL 형태로 유지할 것인지 조건을 설정하려 했다. 그러나 rails는 references 헬퍼로 외래키를 연결하는 듯 하지만 실제로는 외래키 제약조건을 설정하지 않는다. dbconsole을 열어서 contents 테이블을 확인해 보면 다음과 같다.
$ rails dbconsole
> .schema contents
어디에도 외래키 제약조건은 없고, category_id와 관련된 인덱스가 생성되는 코드가 있음을 알 수 있다. 따라서 DDL로 CASCADE나 NOT NULL 제약조건을 추가 할 수 없는 듯 하다. 만약 이렇게 하고 싶다면 외래키 제약조건을 직접 걸어야 한다.
참고: http://rubykr.github.io/rails_guides/migrations.html
references 헬퍼를 없애고 외래키 제약조건을 설정하기 보다 컨트롤러를 통해 유사한 기능을 추가할 것이다.
위의 제약조건대로 설정하고 rails db:migrate를 하면 에러가 발생한다. 기존의 데이터들이 gender를 가지고 있지 않은데 not null로 설정되어 있기에 에러가 발생하는 듯 하다. db를 초기화 하고 다시 마이그레이션 한다.
$ rake db:reset
$ rake db:migrate
이제 수정된 모델 형태로 뷰와 컨트롤러를 수정하자. members에는 gender와 ages가 추가된 필드를 모두 추가해 주고, history에도 변경된 필드를 추가해 주었다. 그리고 회원의 시술이력에서 큰 History객체와 여러개의 Detail_history 객체를 동시에 생성하는 코드를 작성하였다. fields_for 폼 헬퍼를 이용하여 부모와 자식 객체를 동시에 생성 할 수 있었다.
참고: https://withrails.com/2016/01/09/1187/
app/models/history.rb
class History < ApplicationRecord
belongs_to :member
has_many :detail_histories
accepts_nested_attributes_for :detail_histories, reject_if: :all_blank
end
하나의 history객체 밑에 여러 개의 detail_history객체를 가지므로 1:n 관계가 형성된다. has_many 관계를 설정하고 nested attributes로 detail_history의 필드를 가져온다. 모든 필드가 비어있으면 자식 객체를 생성하지 않도록 reject_if: :all_blank를 추가한다.
app/models/detail_history.rb
class DetailHistory < ApplicationRecord
belongs_to :history, optional: true
belongs_to :content
end
현재 개발하는 rails의 버전이 5버전이므로 optional: true를 꼭 설정해 주어야 한다. 그렇지 않으면 아래 사진처럼 뷰에서 컨트롤러로 파라미터는 정확히 전달되나 rollback transaction을 수행하면서 부모와 자식객체를 동시에 생성하여 저장하지 않는다.
app/controllers/histories_controller.rb
class HistoriesController < ApplicationController
# 생략
private
# 생략
# Never trust parameters from the scary internet, only allow the white list through.
def history_params
params.require(:history).permit(:member_id, :date, :total_price, :point_used, :point_accumulated, :is_credit,
detail_histories_attributes: [:content_id, :price, :note]) # fields_for 메서드를 사용하기 위해 nested attribute 추가
end
end
위 모델에서 nested_attributes를 선언한 것 처럼 컨트롤러에서도 뷰에서 전달되는 파라미터를 history_params 메서드를 통해 접근할 수 있도록 detail_histories_attributes를 추가해준다.
app/controllers/members_controller.rb
class MembersController < ApplicationController
# 생략
# GET /members/1
# GET /members/1.json
def show
# 생략
@history = History.new
2.times do
@history.detail_histories.build
end
# 생략
end
# 생략
end
<!-- 생략 -->
<h2>시술 기록</h2>
<%= form_for @history do |f| %>
<!-- 생략 -->
<table>
<thead>
<tr>
<th>시술종류</th>
<th>시술별 가격</th>
<th>비고</th>
<th></th>
</tr>
</thead>
<tbody>
<%= f.fields_for :detail_histories do |df| %>
<tr>
<td><%= df.grouped_collection_select :content_id, @categories, :contents, :name, :id, :name %></td>
<td><%= df.number_field :price %></td>
<td><%= df.text_area :note %></td>
<td><!-- 추가 버튼 --></td>
</tr>
<% end %>
</tbody>
</table>
<!-- 생략 -->
<% end %>
<!-- 생략 -->
history 객체 생성을 위한 form_for 헬퍼 안에 fields_for 헬퍼를 통해 하위 객체를 함께 생성 할 수 있다. 컨트롤러에서 두개의 객체를 생성했으므로 여기서도 두개의 하위 객체에 대한 필드가 생성된다.
위와 같이 동시에 저장 되는걸 확인 할 수 있다.
'개발 > Ruby On Rails' 카테고리의 다른 글
Rails로 개발하기 5일차 - 배포하기(ubuntu, unicorn, nginx, capistrano) (0) | 2016.12.30 |
---|---|
Rails로 개발하기 4일차 - 커피스크립트, 필터, json builder (0) | 2016.12.24 |
Rails로 개발하기 2일차 - 요구사항 기능 만들기 (0) | 2016.12.16 |
Rails로 개발하기 1일차 - DB설계하고 Scaffold 만들기 (0) | 2016.12.12 |
macOS Sierra에 Ruby와 Rails 설치하기. (0) | 2016.11.08 |