STI(Single Table Inheritance)でRailsアプリケーションを効率化しよう
rails oopはじめに
RailsでアプリケーションをTikを作成する際、モデル間の継承関係を適切に表現することが重要です。STI(Single Table Inheritance)は、この継承関係を単一のデータベーステーブルで効率的に管理する手法です。本記事では、STIの基本概念から実装、活用例までを網羅的に解説し、初学者からマスターレベルまでSTIについての理解を深めていきます。
STIとは何か
STIは、Railsにおけるオブジェクト指向プログラミングの概念を、データベース設計に反映させる手法のひとつです。複数のモデルが継承関係にある場合、STIを使うことで、それらのモデルを単一のデータベーステーブルで表現できます。これにより、テーブル数を減らし、アプリケーションの管理を簡素化できます。
STIを使うメリット
STIを使うことで、以下のようなメリットが得られます:
- テーブル数の削減により、データベースのスキーマが簡素化される
- 継承関係にあるモデル間で共通のカラムを一元管理できる
- ポリモーフィック関連などの高度な関連付けを実現できる
- コードの重複を減らし、DRY(Don’t Repeat Yourself)原則に従ったコードを書ける
STIを使う場面
以下のような場面でSTIの使用を検討しましょう:
- 複数のモデルが多くの共通カラムを持つ場合
- モデル間の継承関係が明確で、階層構造が複雑ではない場合
- ポリモーフィック関連を使ってモデル間の関連付けを行う場合
- ロール管理やマルチテナント機能など、ユーザーやアカウントの種類に応じて動作を変える必要がある場合
次の章では、STIの基本概念について詳しく説明します。
STIの基本概念
STIを効果的に使うには、その基本概念を理解することが重要です。ここでは、継承関係とテーブル設計、type
カラムの役割、STIの動作原理について説明します。
継承関係とテーブル設計
STIでは、継承関係にあるモデルを単一のテーブルで表現します。例えば、Vehicle
(乗り物)を親モデルとし、Car
(車)とMotorcycle
(バイク)を子モデルとする継承関係を考えてみましょう。
STIを使わない場合、これらのモデルはそれぞれ別のテーブルで管理されます。しかし、STIを使うことで、次のように単一のテーブルで表現できます。
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モデルと同じようにfind
、where
、all
などのメソッドを使用できます。
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_many
とbelongs_to
で関連付けるとします。
class User < ApplicationRecord
has_many :vehicles
end
class Vehicle < ApplicationRecord
belongs_to :user
end
これにより、User
モデルとVehicle
モデル(および子モデル)の間に一対多のアソシエーションが定義されます。
親モデルと子モデルでのアソシエーションの違い
親モデルと子モデルでは、アソシエーションの動作に若干の違いがあります。
- 親モデル(
Vehicle
)からアソシエーションを定義すると、子モデル(Car
、Motorcycle
)にもそのアソシエーションが継承されます。 - 子モデルからアソシエーションを定義すると、そのアソシエーションは子モデルにのみ適用されます。
例えば、Car
モデルとParkingSpace
モデルをhas_one
とbelongs_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
モデルにmake
とmodel
の存在を検証するバリデーションを定義し、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"
になっています。カラム名を変更する必要がある場合は、ApplicationRecord
でinheritance_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の活用例を図解したものです。
これらの例は、STIの一部の活用法に過ぎません。アプリケーションの要件に応じて、STIを柔軟に適用してください。
次の章では、本記事のまとめを述べます。
まとめ
本記事では、RailsのSTIについて、基本概念からモデルの実装、アソシエーション、バリデーション、活用例まで幅広く解説してきました。STIは、継承関係にあるモデルを単一のテーブルで管理する手法であり、適切に使用することでアプリケーションの設計を改善できます。
STIの利点と欠点
STIの主な利点は以下の通りです:
- テーブル数を減らし、データベースのスキーマを簡素化できる
- 継承関係にあるモデル間で共通の属性を一元管理できる
- ポリモーフィック関連など、高度な関連付けを実現できる
一方、STIには以下のような欠点もあります:
- 単一のテーブルに大量のレコードが集中し、パフォーマンスが低下する可能性がある
- モデル間の関係が複雑になると、かえって設計が難しくなることがある
- 子モデルを追加・削除する際に、データの整合性を維持するための追加作業が必要になる
STIを使うべきシチュエーション
以下のようなシチュエーションでは、STIの使用を検討しましょう:
- モデル間に明確な継承関係があり、共通の属性が多数ある場合
- ポリモーフィック関連を使って、複数のモデルと関連付ける必要がある場合
- ユーザーのロールやテナントごとに、共通の機能を持ちつつ、固有の属性も必要な場合
ただし、STIはあくまでも設計のオプションの一つです。モデル間の関係が複雑になりすぎる場合は、別のアプローチを検討してください。
STIをマスターするために
STIを効果的に使いこなすには、以下のポイントを押さえておくことが重要です:
- STIの基本概念と利点・欠点を理解する
- モデルの実装、アソシエーション、バリデーションの設定方法を習得する
- パフォーマンスへの影響を考慮し、必要に応じて最適化する
- STIが適切でない場合は、別のアプローチを柔軟に選択する
また、実際のアプリケーション開発で STI を使用し、試行錯誤を重ねることが大切です。経験を積むことで、STIをより効果的に活用できるようになるでしょう。
本記事が、皆さんのRailsアプリケーション開発におけるSTIの理解と活用に役立てば幸いです。