CodeKitchen

STI(Single Table Inheritance)でRailsアプリケーションを効率化しよう

rails oop

はじめに

RailsでアプリケーションをTikを作成する際、モデル間の継承関係を適切に表現することが重要です。STI(Single Table Inheritance)は、この継承関係を単一のデータベーステーブルで効率的に管理する手法です。本記事では、STIの基本概念から実装、活用例までを網羅的に解説し、初学者からマスターレベルまでSTIについての理解を深めていきます。

STIとは何か

STIは、Railsにおけるオブジェクト指向プログラミングの概念を、データベース設計に反映させる手法のひとつです。複数のモデルが継承関係にある場合、STIを使うことで、それらのモデルを単一のデータベーステーブルで表現できます。これにより、テーブル数を減らし、アプリケーションの管理を簡素化できます。

STIを使うメリット

STIを使うことで、以下のようなメリットが得られます:

  1. テーブル数の削減により、データベースのスキーマが簡素化される
  2. 継承関係にあるモデル間で共通のカラムを一元管理できる
  3. ポリモーフィック関連などの高度な関連付けを実現できる
  4. コードの重複を減らし、DRY(Don’t Repeat Yourself)原則に従ったコードを書ける

STIを使う場面

以下のような場面でSTIの使用を検討しましょう:

  1. 複数のモデルが多くの共通カラムを持つ場合
  2. モデル間の継承関係が明確で、階層構造が複雑ではない場合
  3. ポリモーフィック関連を使ってモデル間の関連付けを行う場合
  4. ロール管理やマルチテナント機能など、ユーザーやアカウントの種類に応じて動作を変える必要がある場合

次の章では、STIの基本概念について詳しく説明します。

STIの基本概念

STIを効果的に使うには、その基本概念を理解することが重要です。ここでは、継承関係とテーブル設計、typeカラムの役割、STIの動作原理について説明します。

継承関係とテーブル設計

STIでは、継承関係にあるモデルを単一のテーブルで表現します。例えば、Vehicle(乗り物)を親モデルとし、Car(車)とMotorcycle(バイク)を子モデルとする継承関係を考えてみましょう。

VehicleCarMotorcycle

STIを使わない場合、これらのモデルはそれぞれ別のテーブルで管理されます。しかし、STIを使うことで、次のように単一のテーブルで表現できます。

Vehiclesintegeridstringtypestringmakestringmodelintegeryearstringcolor

typeカラムの役割

STIでは、継承関係にあるモデルを区別するために、typeカラムを使用します。このtypeカラムには、レコードがどのモデルに属しているかを示す文字列が格納されます。

上記の例では、Vehicleモデルのtypeカラムには、"Car"または"Motorcycle"という文字列が格納されます。これにより、単一のテーブルに異なるモデルのレコードを保存しつつ、それぞれのモデルを区別することができます。

STIの動作原理

STIは、Railsのモデルとデータベースの間で行われる処理によって実現されています。モデルの継承関係を定義すると、Railsは自動的にtypeカラムを使ってレコードを区別します。

以下は、STIの動作原理を示す簡略化したコード例です:

class Vehicle < ApplicationRecord
end

class Car < Vehicle
end

class Motorcycle < Vehicle
end

ここで、Car.createを実行すると、vehiclesテーブルに新しいレコードが作成され、typeカラムには"Car"が格納されます。同様に、Motorcycle.createを実行すると、typeカラムに"Motorcycle"が格納されます。

次の章では、実際にSTIを使ったモデルの実装方法について説明します。

STIを使ったモデルの実装

STIを使ってモデルを実装する際は、親モデルと子モデルを定義し、typeカラムを設定する必要があります。ここでは、その手順を詳しく説明します。

親モデルの作成

まず、親モデルを作成します。この親モデルには、子モデルに共通するカラムを定義します。先ほどのVehicleモデルを例に取ると、以下のようになります:

class Vehicle < ApplicationRecord
  validates :make, :model, :year, :color, presence: true
end

ここでは、make(メーカー)、model(モデル)、year(年式)、color(色)を共通のカラムとして定義しています。また、これらのカラムにはバリデーションを設定しています。

子モデルの作成

次に、子モデルを作成します。子モデルは親モデルを継承し、必要に応じて独自のカラムやメソッドを定義します。CarモデルとMotorcycleモデルを作成してみましょう。

class Car < Vehicle
  validates :number_of_doors, presence: true
end

class Motorcycle < Vehicle
  validates :engine_displacement, presence: true
end

Carモデルには、number_of_doors(ドアの数)というカラムを追加し、バリデーションを設定しています。同様に、Motorcycleモデルには、engine_displacement(排気量)というカラムを追加しています。

typeカラムの設定

STIを使うには、typeカラムをデータベースに追加する必要があります。これは、マイグレーションファイルで行います。

class CreateVehicles < ActiveRecord::Migration[6.1]
  def change
    create_table :vehicles do |t|
      t.string :type
      t.string :make
      t.string :model
      t.integer :year
      t.string :color
      t.integer :number_of_doors
      t.integer :engine_displacement

      t.timestamps
    end
  end
end

このマイグレーションファイルでは、vehiclesテーブルを作成し、typeカラムを含む各カラムを定義しています。typeカラムは、Railsが自動的に認識し、STIのために使用します。

サンプルコード: 親モデルと子モデルの作成

以下は、これまでの内容をまとめたサンプルコードです:

# app/models/vehicle.rb
class Vehicle < ApplicationRecord
  validates :make, :model, :year, :color, presence: true
end

# app/models/car.rb
class Car < Vehicle
  validates :number_of_doors, presence: true
end

# app/models/motorcycle.rb
class Motorcycle < Vehicle
  validates :engine_displacement, presence: true
end

次の章では、STIを使ったCRUD操作について説明します。

STIを使ったCRUD操作

STIを使ったモデルでは、通常のRailsモデルと同様にCRUD操作(Create, Read, Update, Delete)を行うことができます。ここでは、レコードの作成、取得、更新、削除の方法を説明します。

レコードの作成

STIを使ったモデルでレコードを作成するには、通常のRailsモデルと同じようにcreateメソッドを使用します。

car = Car.create(make: 'Toyota', model: 'Camry', year: 2021, color: 'Red', number_of_doors: 4)
motorcycle = Motorcycle.create(make: 'Honda', model: 'CBR1000RR', year: 2021, color: 'Blue', engine_displacement: 1000)

これらのコードを実行すると、vehiclesテーブルに新しいレコードが作成されます。carレコードのtypeカラムには"Car"が、motorcycleレコードのtypeカラムには"Motorcycle"が自動的に設定されます。

レコードの取得

レコードを取得する際も、通常のRailsモデルと同じようにfindwhereallなどのメソッドを使用できます。

car = Car.find(1)
motorcycles = Motorcycle.where(make: 'Honda')
vehicles = Vehicle.all

これらのコードは、それぞれCarモデルのidが1のレコード、Motorcycleモデルのmakeが’Honda’のレコード、Vehicleモデルの全レコードを取得します。

レコードの更新と削除

レコードの更新と削除も、通常のRailsモデルと同様に行えます。

car = Car.find(1)
car.update(color: 'Blue')

motorcycle = Motorcycle.find(1)
motorcycle.destroy

これらのコードは、Carモデルのidが1のレコードのcolorを’Blue’に更新し、Motorcycleモデルのidが1のレコードを削除します。

サンプルコード: CRUD操作

以下は、STIを使ったモデルでのCRUD操作のサンプルコードです:

# レコードの作成
car = Car.create(make: 'Toyota', model: 'Camry', year: 2021, color: 'Red', number_of_doors: 4)
motorcycle = Motorcycle.create(make: 'Honda', model: 'CBR1000RR', year: 2021, color: 'Blue', engine_displacement: 1000)

# レコードの取得
car = Car.find(1)
motorcycles = Motorcycle.where(make: 'Honda')
vehicles = Vehicle.all

# レコードの更新
car.update(color: 'Blue')

# レコードの削除
motorcycle.destroy

次の章では、STIとアソシエーションについて説明します。

STIとアソシエーション

STIを使ったモデルでも、他のモデルとのアソシエーションを定義することができます。ここでは、STIモデルと他のモデルとのアソシエーションの設定方法と、親モデルと子モデルでのアソシエーションの違いについて説明します。

他のモデルとのアソシエーション

STIを使ったモデルと他のモデルとのアソシエーションは、通常のRailsモデルと同様に定義できます。例えば、UserモデルとVehicleモデルをhas_manybelongs_toで関連付けるとします。

class User < ApplicationRecord
  has_many :vehicles
end

class Vehicle < ApplicationRecord
  belongs_to :user
end

これにより、UserモデルとVehicleモデル(および子モデル)の間に一対多のアソシエーションが定義されます。

親モデルと子モデルでのアソシエーションの違い

親モデルと子モデルでは、アソシエーションの動作に若干の違いがあります。

  • 親モデル(Vehicle)からアソシエーションを定義すると、子モデル(CarMotorcycle)にもそのアソシエーションが継承されます。
  • 子モデルからアソシエーションを定義すると、そのアソシエーションは子モデルにのみ適用されます。

例えば、CarモデルとParkingSpaceモデルをhas_onebelongs_toで関連付けるとします。

class Car < Vehicle
  has_one :parking_space
end

class ParkingSpace < ApplicationRecord
  belongs_to :car
end

この場合、CarモデルとParkingSpaceモデルの間に一対一のアソシエーションが定義されますが、Motorcycleモデルにはこのアソシエーションは適用されません。

サンプルコード: アソシエーションの設定

以下は、STIモデルと他のモデルとのアソシエーションを設定するサンプルコードです:

class User < ApplicationRecord
  has_many :vehicles
end

class Vehicle < ApplicationRecord
  belongs_to :user
end

class Car < Vehicle
  has_one :parking_space
end

class Motorcycle < Vehicle
  # Motorcycleに固有のアソシエーションがあれば、ここに定義
end

class ParkingSpace < ApplicationRecord
  belongs_to :car
end

次の章では、STIとバリデーションについて説明します。

STIとバリデーション

STIを使ったモデルでは、バリデーションを親モデルと子モデルのそれぞれに設定することができます。ここでは、親モデルと子モデルでのバリデーションの設定方法と、共通のバリデーションと個別のバリデーションについて説明します。

親モデルと子モデルでのバリデーションの設定

親モデルに定義したバリデーションは、子モデルにも継承されます。一方、子モデルに定義したバリデーションは、その子モデルにのみ適用されます。

例えば、Vehicleモデルにmakemodelの存在を検証するバリデーションを定義し、Carモデルにnumber_of_doorsの値を検証するバリデーションを定義するとします。

class Vehicle < ApplicationRecord
  validates :make, :model, presence: true
end

class Car < Vehicle
  validates :number_of_doors, inclusion: { in: 2..4 }
end

class Motorcycle < Vehicle
  # Motorcycleに固有のバリデーションがあれば、ここに定義
end

この場合、VehicleモデルのバリデーションはCarモデルとMotorcycleモデルの両方に適用されますが、CarモデルのバリデーションはCarモデルにのみ適用されます。

共通のバリデーションと個別のバリデーション

親モデルに定義したバリデーションは、全ての子モデルに共通のバリデーションとして機能します。一方、子モデルに定義したバリデーションは、その子モデルに固有のバリデーションとして機能します。

必要に応じて、親モデルと子モデルにバリデーションを適切に配置することで、DRYな設計を維持しつつ、各モデルの要件を満たすことができます。

サンプルコード: バリデーションの設定

以下は、STIモデルでバリデーションを設定するサンプルコードです:

class Vehicle < ApplicationRecord
  validates :make, :model, :year, presence: true
  validates :year, numericality: { greater_than_or_equal_to: 1900 }
end

class Car < Vehicle
  validates :number_of_doors, inclusion: { in: 2..4 }
end

class Motorcycle < Vehicle
  validates :engine_displacement, numericality: { greater_than: 0 }
end

次の章では、STIを使う上での注意点について説明します。

STIを使う上での注意点

STIは強力な機能ですが、いくつかの注意点があります。ここでは、typeカラムの命名規則、子モデルの追加と削除、パフォーマンスへの影響について説明します。

typeカラムの命名規則

STIでは、typeカラムを使ってモデルを区別します。このカラムの名前は、デフォルトで"type"になっています。カラム名を変更する必要がある場合は、ApplicationRecordinheritance_columnを設定します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  self.inheritance_column = 'model_type'
end

ただし、カラム名を変更すると、Railsの規約から外れてしまうため、特別な理由がない限り、デフォルトの"type"を使用することをお勧めします。

子モデルの追加と削除

アプリケーションの要件が変更され、新しい子モデルを追加したり、既存の子モデルを削除したりする必要が出てくることがあります。このような場合は、以下の点に注意が必要です。

  • 新しい子モデルを追加する際は、typeカラムに対応する値を持つレコードが存在しないことを確認してください。
  • 既存の子モデルを削除する際は、そのモデルに対応するレコードを事前に削除するか、別のモデルに移行してください。

これらの作業を行わないと、エラーが発生したり、予期しない動作が起こったりする可能性があります。

パフォーマンスへの影響

STIを使うと、単一のテーブルに大量のレコードが集中することがあります。これにより、クエリのパフォーマンスが低下する可能性があります。特に、typeカラムを使った検索や、子モデル固有のカラムを使った検索は、パフォーマンスに影響を与えやすくなります。

このような問題を回避するために、以下のような対策を検討してください。

  • インデックスを適切に設定する
  • クエリの最適化を行う
  • 必要に応じて、STIではなく別のアプローチ(例えば、ポリモーフィック関連)を使用する

また、STIは全ての状況に適しているわけではありません。モデル間の関係が複雑になってきた場合や、子モデル固有のカラムが多数ある場合は、STIではなく別のアプローチを検討してください。

次の章では、STIの活用例について説明します。

STIの活用例

STIは、以下のようなシナリオで効果的に活用できます。ここでは、ポリモーフィック関連、ロール管理、マルチテナント機能について説明し、STIを使った実装例を示します。

ポリモーフィック関連

ポリモーフィック関連とは、単一のアソシエーションを通じて、複数のモデルと関連付ける手法です。例えば、CommentモデルをPostモデルとImageモデルの両方に関連付けるとします。

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Post < ApplicationRecord
  has_many :comments, as: :commentable
end

class Image < ApplicationRecord
  has_many :comments, as: :commentable
end

このように、STIとポリモーフィック関連を組み合わせることで、柔軟で維持しやすいアソシエーションを実現できます。

ロール管理

STIは、ユーザーのロール管理にも活用できます。例えば、Userモデルを親モデルとし、AdminモデルとGuestモデルを子モデルとして定義するとします。

class User < ApplicationRecord
  # 共通のカラムとメソッド
end

class Admin < User
  # 管理者固有のカラムとメソッド
end

class Guest < User
  # ゲストユーザー固有のカラムとメソッド
end

この設計により、ユーザーの種類に応じた機能を実装しつつ、共通の属性を一元管理できます。

マルチテナント機能

STIは、マルチテナント機能の実装にも役立ちます。例えば、Tenantモデルを親モデルとし、各テナントを子モデルとして定義するとします。

class Tenant < ApplicationRecord
  # 共通のカラムとメソッド
end

class Company1 < Tenant
  # Company1固有のカラムとメソッド
end

class Company2 < Tenant
  # Company2固有のカラムとメソッド
end

この設計により、テナントごとにデータを分離しつつ、共通の機能を一元管理できます。

図解: STIの活用例

以下は、STIの活用例を図解したものです。

polymorphicCommentCommentablePostImageUserAdminGuestTenantCompany1Company2

これらの例は、STIの一部の活用法に過ぎません。アプリケーションの要件に応じて、STIを柔軟に適用してください。

次の章では、本記事のまとめを述べます。

まとめ

本記事では、RailsのSTIについて、基本概念からモデルの実装、アソシエーション、バリデーション、活用例まで幅広く解説してきました。STIは、継承関係にあるモデルを単一のテーブルで管理する手法であり、適切に使用することでアプリケーションの設計を改善できます。

STIの利点と欠点

STIの主な利点は以下の通りです:

  1. テーブル数を減らし、データベースのスキーマを簡素化できる
  2. 継承関係にあるモデル間で共通の属性を一元管理できる
  3. ポリモーフィック関連など、高度な関連付けを実現できる

一方、STIには以下のような欠点もあります:

  1. 単一のテーブルに大量のレコードが集中し、パフォーマンスが低下する可能性がある
  2. モデル間の関係が複雑になると、かえって設計が難しくなることがある
  3. 子モデルを追加・削除する際に、データの整合性を維持するための追加作業が必要になる

STIを使うべきシチュエーション

以下のようなシチュエーションでは、STIの使用を検討しましょう:

  1. モデル間に明確な継承関係があり、共通の属性が多数ある場合
  2. ポリモーフィック関連を使って、複数のモデルと関連付ける必要がある場合
  3. ユーザーのロールやテナントごとに、共通の機能を持ちつつ、固有の属性も必要な場合

ただし、STIはあくまでも設計のオプションの一つです。モデル間の関係が複雑になりすぎる場合は、別のアプローチを検討してください。

STIをマスターするために

STIを効果的に使いこなすには、以下のポイントを押さえておくことが重要です:

  1. STIの基本概念と利点・欠点を理解する
  2. モデルの実装、アソシエーション、バリデーションの設定方法を習得する
  3. パフォーマンスへの影響を考慮し、必要に応じて最適化する
  4. STIが適切でない場合は、別のアプローチを柔軟に選択する

また、実際のアプリケーション開発で STI を使用し、試行錯誤を重ねることが大切です。経験を積むことで、STIをより効果的に活用できるようになるでしょう。

本記事が、皆さんのRailsアプリケーション開発におけるSTIの理解と活用に役立てば幸いです。

logo

Web Developer。パフォーマンス改善、データ分析基盤、生成AIに興味があり。Next.js, Terraform, AWS, Rails, Pythonを中心に開発スキルを磨いています。技術に関して幅広く投稿していきます。