2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Rails】正しいSTIの作り方

Last updated at Posted at 2019-12-28

環境

  • 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
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?