Table of Contents
どんな課題?
(課題はデフォルメしてあります)
あるお店では、家電製品を販売していて、客とスタッフがいます。客が例えば、Aを購入すると、そのAの購入に対して専任のアフターサービススタッフを付けます。しかし、そのアフターサービススタッフは変更される場合があるので、履歴を残しておきたいです。
データベースとモデルはこんな感じ↓
purchase: 購入
- belongs_to :client
- belongs_to :staff
client: 客
- has_many :purchases
staff: スタッフ
- has_many :purchases
purchase_main_staff_history: 担当スタッフの履歴(←これから作っていきます)
この課題を通して学べること
MVCのモデルに対する理解が深まり、少しだけ自由になれました!
バージョン
- ruby 2.7.2
- Rails 6.1.3
実装!
Migration
purchase_main_staff_historiesテーブルを作ります。
class CreatePurchaseMainStaffHistories < ActiveRecord::Migration[6.1]
def change
create_table :purchase_main_staff_histories do |t|
t.references :purchase, null: false, foreign_key: true
t.datetime :changed_at, null: false
t.references :old_main_staff, null: false, foreign_key: { to_table: :staffs }
t.references :new_main_staff, foreign_key: { to_table: :staffs }
t.references :changed_by, null: false, foreign_key: { to_table: :staffs }
t.timestamps
end
end
end
- 外部キーのカラム名は自由につけられる!!:foreign_keyはそのリレーション先のテーブルのモデル名にするのが一般的ですが、それが絶対的なルールではないです(知らなかった!)。いろんな名前で外部キーのカラム名を設定できます。今回の例では、staffsテーブルへのリレーションを
staff
ではなく、old_main_staff
,new_main_staff
, はたまたchanged_by
とわかりやすい名前にすることに成功しています。ただこの場合、マイグレーションファイルでは、foreign_key: true
ではなくforeign_key: { to_table: :staffs }
としてテーブル名を明示してあげる必要があります。 - 外部キーのカラム名は自動的に_idが付与される:マイグレーションで
t.references :old_main_staff_id
とする必要はありません。ま、これは通常のモデル名で外部キー張る時の挙動を考えれば当然といえば当然ですが。。 - テーブルAとBの間で外部キーは複数張れちゃう:システムの規模が大きくなればなるほど、こういうケースが出てくるのかもしれませんね。
- ちなみに、担当を削除することもあるので、new_main_staffにはnull制約をつけておりません。
あと蛇足ではありますが、以下が僕が最初に提出したMigrationコードです。上のポイントがわかってなかったという思い出に貼っておきますw。
class CreatePurchaseMainStaffHistories < ActiveRecord::Migration[6.1]
def change
create_table :purchase_main_staff_histories do |t|
t.integer :staff_before_id, null: false
t.integer :staff_after_id, null: false
t.datetime :changed_at, null: false
t.references :purchase, null: false, foreign_key: true
t.references :staff, null: false, foreign_key: true
t.timestamps
end
end
end
Model
# == Schema Information
#
# Table name: purchase_main_staff_histories
#
# id :bigint not null, primary key
# purchase_id :bigint not null
# changed_at :datetime not null
# old_main_staff_id :bigint not null
# new_main_staff_id :bigint
# changed_by_id :bigint not null
# created_at :datetime not null
# updated_at :datetime not null
class PurchaseMainStaffHistory < ApplicationRecord
belongs_to :purchase
belongs_to :changed_by, class_name: 'staff'
belongs_to :old_main_staff, class_name: 'staff'
belongs_to :new_main_staff, class_name: 'staff', optional: true
- 外部キーのカラム名は自動的に
_id
になる:通常のモデル名でリレーションを張る場合と同じ挙動なので、当然といえば当然ですが。。😅 - リレーション宣言のbelongs_toの引数には
_id
不要:これも通常のモデル名でリレーションを張る場合と同じ挙動ですね。。 - リレーションの宣言は相思相愛でなくてもいい:テーブル間でリレーション持たせる時は、モデルでお互いがお互いに宣言しなくてはならないのかと勘違いしていたが、参照する側が参照される側に対して宣言すればいい。今回のケースでは、リレーション先のStaffモデルからのhas_manyは今回は宣言する必要はないです。
View
セレクトボックスでstaffを選択できるようにします。
# app/views/purchase_main_staffs/_form.html.erb
<%= form_with(model: purchase, local: true) do |f| %>
<label><%= Purchase.t(:main_staff) %></label>
<%= f.select(:staff_id,
@staffs_select_options,
{ include_blank: true }) %>
<%= f.submit(t('views.common.submit'), class: 'btn btn-primary') %>
<% end %>
nullを許容するので、include_blankを入れてあげます。 @staffs_select_optionsは、staffの選択肢セットで、コントローラから渡します。
routes
resources :clients, param: :code do
resources :purchases do
resource :purchase_main_staffs, param: :purchase_code, only: %i[edit update]
end
end
Controller
class PurchaseMainStaffsController < ApplicationController
before_action :set_client
before_action :set_purchase
def edit
@staffs_select_options = Staff.all.pluck(:name, :id)
end
def update
new_main_staff = Staff.find_by(id: purchase_params[:staff_id])
@purchase.change_main_staff_with_history!(
Time.current,
new_main_staff,
current_staff
)
if main_staff_history
redirect_to client_purchase_path(@purchase.client, @purchase),
notice: "担当スタッフを更新しました。"
else
@purchase = main_staff_history.purchase
flash.now[:alert] = main_staff_history.errors.first if main_staff_history.errors.first.present?
render :edit
end
end
private
def purchase_params
params.require(:purchase).permit(:staff_id)
end
def set_client
@client = Client.find(params[:client_id])
end
def set_purchase
@purchase = @client.purchases.find(params[:purchase_id])
end
end
PurchaseMainStaffsControllerクラスのコントローラを作って、@purchaseインスタンス変数に対してモデルメソッドchange_main_staff_with_history!
を呼び出します。このメソッドは次に定義していきます。
Model method
class Purchase < ApplicationRecord
def change_main_staff_with_history!(new_main_staff, current_time, current_staff)
purchase_main_staff_histories.create!(
changed_at: current_time,
old_main_staff: main_staff,
new_main_staff: new_main_staff,
changed_by: current_staff
)
update!(staff_id: new_main_staff&.id)
end
end
先ほどのinteractorで@purchaseインスタンスに対して履歴を作るように書いたので、Purchaseモデルにメソッドを定義してあげます。create!メソッドでレコードを作成して、自身の@purchaseに関しても外部キーのmain_staffを更新します。
これで実装完了です!
最後に
何か参考になれば幸いです。
Related Posts
Daiki Urata
2024/06/10
Yuhei Okazaki
2021/07/17