Railsで子クラスに個別のカラムを持たせるSTIを実装したのでそのメモ。
内容は例になります。
仕様
LINEの投稿をイメージして以下の仕様を想定
- 1回の投稿においてテキスト、もしくはスタンプを投稿できる
- テキストの投稿においては投稿された文字列を保存する
- スタンプの投稿においては投稿されたスタンプのIDを保存する
- 投稿できるものの種類は今後増えていくことが想定されている
今後も投稿できるものの種類が増えそう、という点と投稿できるものの種類によって保存しておくデータの型が変わりそう、という点からSTIを使って実装してみることにしました。
実装方針
リクエストパラメータについて
- 1つの投稿に対して
post_type
+text
もしくはpost_type
+stamp_id
が送られる - 投稿の種類は
post_type
で表現 -
post_type
がstamp
の場合はstamp_id
に値を入れる -
post_type
がtext
の場合はtext
に値を入れる
リクエストパラメータのバリデーションについて
-
post_type
がstamp
の場合はtext
にデータを入れられないようにバリデーションを入れる -
post_type
がtext
の場合はstamp_id
にデータを入れられないようにバリデーションを入れる
工夫したところ
バリデーションはモデルに実装するわけですが、子クラス同士はお互いがどのカラムを利用するかを知らないようにしたいと思ったので、親クラスに条件付きでバリデーションを入れて、子クラスでその条件を外すという方針で実装しました。
実装
# テーブル定義
create_table :posts do |t|
t.string :type, null: true
t.string :text, null: true
t.integer :stamp_id, null: true
t.timestamps
end
# コントローラー
class PostController < ApplicationController
def create
if PostClassSelector.select(params[:post][:post_type]).new(post_params).save
# 成功時の処理
else
# 失敗時の処理
end
end
def post_params
params.require(:post).permit(:text, :stamp_id)
end
end
# モデル
class Post
validates :text, absence: true, unless: -> { self.available_attribute.include? :text }
validates :stamp_id, absence: true, unless: -> { self.available_attribute.include? :stamp_id }
class_attribute :available_attribute
def self.disable_absence_validator(attribute)
self.available_attribute ||= []
self.available_attribute << attribute
end
end
class Post::Text < Post
disable_absence_validator :text
end
class Post::Stamp < Post
disable_absence_validator :stamp_id
end
class PostClassSelector
def self.select(post_type)
case post_type
when :text then Post::Text
when :stamp then Post::Stamp
else raise StandardError.new('unsupported post type')
end
end
end