環境
- Rails 5.2.3
やり方
マイグレーションファイルの作成
$ bundle exec rails g model ChannelFair channel:references fair:references
invoke active_record
create db/migrate/xxxxxxxxxxxxxx_create_channel_fairs.rb
create app/models/channel_fair.rb
生成されるマイグレーションファイル
class CreateChannelFairs < ActiveRecord::Migration[5.2]
def change
create_table :channel_fairs do |t|
t.references :channel, foreign_key: true
t.references :fair, foreign_key: true
t.timestamps
end
end
end
マイグレーションファイルを編集
-
t.referencesはindexも作成してくれます。-
foreign_key: trueがあれば、外部キー制約もつけてくれます。 - 今回はSTIのため、
foreign_key: trueを削除し、テーブル名とカラム名を指定して外部キー制約を手動で設定します。(※3)
-
-
t.referencesを使いつつ、中間テーブルでは、以下の通り変更します。- 複合キーでunique制約を張ります。(※1)
- 上記のunique制約で
channel_idに対するindexも張られるため、index: falseを指定します。(※2) -
fair_idに対するindexも不要であれば、index: falseを指定します。
-
STIのため、
typeカラムを追加します。(※4)
class CreateChannelFairs < ActiveRecord::Migration[5.2]
def change
add_column :channels, :type, :string # (※4)
create_table :channel_fairs do |t|
t.references :channel, null: false, index: false # (※2)
t.references :fair, null: false
t.timestamps
end
# (※3)
add_foreign_key :channel_fairs, :channels, column: :channel_id
add_foreign_key :channel_fairs, :channels, column: :fair_id
add_index :channel_fairs, [:channel_id, :fair_id], unique: true # (※1)
end
end
モデル
app/model/channel_fair.rb
class ChannelFair < ApplicationRecord
belongs_to :channel
belongs_to :fair
validates :channel_id, presence: true
validates :fair_id, presence: true
validates :channel_id, uniqueness: { scope: :fair_id }
end
app/model/fair.rb
class Fair < Channel
has_many :channel_fairs, dependent: :destroy
has_many :channels, through: :channel_fairs
end
- 以下、(※A)の部分は要件に応じて、主にviewに連動して必要であれば追加します。
- 基本的にtypeカラムにvalidationは不要ですが(子クラスで指定した文字列以外は
validaion errorが発生します)、空文字列は許可されているため、それも防ぎたい場合はvalidationを追加しましょう。
app/model/channel.rb
class Channel < ApplicationRecord
# (※A)
TYPES = %w(Fair)
TYPES_WITH_BLANK = [''] + TYPES
has_many :channel_fairs, dependent: :destroy
has_many :fairs, through: :channel_fairs
scope :only_channels, -> { where(type: nil) }
scope :only_fairs, -> { where(type: 'Fair') }
# (※A)
before_validation do
self.type = nil if type.blank?
end
def channel?
type.nil?
end
def fair?
type == 'Fair'
end
end
コントローラ
-
becomesを使う必要があるかもしれません。
app/controllers/channels_controller.rb
def create
:
# 子クラスと親クラスでcontrollerをまとめている場合に必要になるかもしれません
@channel = Channel.find(params[:id]).becomes(Channel)
:
end
def channel_params
params.require(type_key).permit(
:type,
:
)
end
def type_key
return :channel if params.key?(:channel)
return :fair if params.key?(:fair)
end
becomesについては、decent_exposureを使っている場合は、以下のような変更をする必要があるかもしれません。
- expose :channel, find_by: :slug, scope: -> { channels }
+ expose :channel, find: ->(id, scope){ scope.find_by!(slug: id).becomes(Channel) }, scope: -> { channels }
ビュー
select_boxで、こういう感じのコードが必要になるかもしれません。
<%= f.select :type, Channel::TYPES_WITH_BLANK %>
ファクトリー
spec/factories/channels.rb
FactoryBot.define do
factory :channel do
..
end
factory :fair, class: Fair, parent: :channel do
end
end
スペック
spec/models/channel_spec.rb
describe Channel, type: :model do
let(:channel) { create(:channel) }
it 'has type with nil' do
expect(channel.type).to be_nil
end
it 'has many fairs' do
channel.fairs << create_list(:fair, 2)
expect(channel.fairs).to be_many
end
end
spec/models/fair_spec.rb
describe Fair, type: :model do
let(:fair) { create(:fair) }
it 'has type with Fair' do
expect(fair.type).to eq('Fair')
end
it 'has many channels' do
fair.channels << create_list(:channel, 2)
expect(fair.channels).to be_many
end
end