前提
最小限に、です。もろもろ省略していますし、命名もよろしくありません。用語の使い方も厳密ではありません。
が、何よりもまず最初に簡単に知ることが大切だと思うからです。
状況
Fullnameモデルを作る
Fullname
というモデルを作ります。このモデルは以下の属性を持たせたいです。
fullname
*1this_is_firstname
this_is_lastname
FirstnameモデルとLastnameモデルについて
Fullname
とは別に、Firstname
と Lastname
というモデルも作りたいと思います。これらの内容は次のとおりです。
Firstname
は、Fullname
にある属性のうち、fullname
とthis_is_firstname
を持つLastname
は、Fullname
にある属性のうち、fullname
とthis_is_lastname
を持つ
具体値は?
上記までのモデルを作った場合に、それぞれの属性に次のような値が入ることを想定しています。
fullname
田中二郎
this_is_firstname
二郎
this_is_lastname
田中
ただ、Firstname
モデルと Lastname
モデルには、それぞれ一部の属性が無いという設計にしたいとします。
どうするか
前述の「状況」を実現するために考えられる単純な方法としては以下の方法です。つまり、3つのモデルを $ rails g model
を実行することで作る方法です。
$ rails g model Fullname $ rails g model Firstname $ rails g model Lastname
それぞれのモデルに、それぞれのモデルが必要な属性を作るという方法があります*2。
ここで STI を使う(作る)
上記の方法でもよいですが、ここで STI を使ってみます。
必要なファイルを作る
まず、必要なファイルを作ります。ファイルを作るコマンドは次のようになります。
$ rails g model Fullname $ touch app/models/firstname.rb $ touch app/models/lastname.rb
Firstname
モデルとLastname
モデルは、最初は空っぽのファイルを作るだけです。
Fullnameモデルの属性をマイグレーションする(データベースにテーブルを作る)
Fullname
モデルには、Firstname
モデルに必要な属性とLastname
に必要な属性を合算したすべての属性(カラム)を詰め込みます。そしてそれはデータベースに実態として作られます。
なので、マイグレーションファイルを作り、$ rails db:migrate
しましょう。マイグレーションファイルは以下のとおりです。
class CreateFullnames < ActiveRecord::Migration[6.0] def change create_table :fullnames do |t| t.string :type t.string :fullname t.string :this_is_firstname t.string :this_is_lastname t.timestamps end end end
ここで最重要なのが :type
という属性(カラム)です。この type
という属性名は予約語です*3。したがって、t.string :type
という書き方は定型文です。
残りのカラムは前述の通り、Firstname
に必要なカラムとLastname
に必要なカラムそれぞれのカラムを合算したカラムです。つまり、fullname
とthis_is_firstname
とthis_is_lastname
になります。
上記のマイグレーションファイルが書けたら、$ rails db:migrate
しましょう。
app/models/firstname.rbとapp/models/lastname.rbの中身を書く
先ほど空っぽのファイルだけを作った、firstname.rb
とlastname.rb
の中身を書きます。ポイントは、継承元のスーパークラスを Fullname
にすることです。具体的には以下のとおりです。
class Firstname < Fullname end
class Lastname < Fullname end
最低限の動作を目指しますので、クラスの中身は空っぽでよいです。
この2つのクラスは、データベースにテーブルを持ちません。データベースという実態に含まれるカラムはすべてFullname
の方に作りましたので、テーブルを持つ必要がありません*4。
上記で完成
上記までで、STIの最低限の実装はOKです。
動作確認
動作確認をします。まずレコードを作ります。
テーブルとデータベースが直接紐付いているのは Fullname
ですが、Firstname
やLastname
からレコードを取り扱うことができます。具体的には以下のとおりです。
Lastname.create(fullname: '田中二郎', this_is_firstname: '二郎')
として、Lastnameモデルのレコードを作ってみます。
irb(main):001:0> Lastname.create(fullname: '田中二郎', this_is_firstname: '二郎') (0.1ms) begin transaction Lastname Create (0.9ms) INSERT INTO "fullnames" ("type", "fullname", "this_is_firstname", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["type", "Lastname"], ["fullname", "田中二郎"], ["this_is_firstname", "二郎"], ["created_at", "2020-04-18 12:22:45.112188"], ["updated_at", "2020-04-18 12:22:45.112188"]] (0.9ms) commit transaction => #<Lastname id: 1, type: "Lastname", fullname: "田中二郎", this_is_firstname: "二郎", this_is_lastname: nil, created_at: "2020-04-18 12:22:45", updated_at: "2020-04-18 12:22:45">
データベースにテーブルを持たないLastname
モデルの操作により、レコードを作ることができました。このレコードはデータベース内の実態としては、Fullname
のテーブルであるfullnames
に以下のように格納されています*5。
まとめ
Firstname
モデルとLastname
モデルという2つのモデルを作りたいという欲求があり、それぞれのモデルの属性に共通の属性があるため、それらをまとめたFullname
モデルというモデルを作りました。
Fullname
モデルだけが物理データベースのテーブルという実体を持ちます。そのテーブルにFirstname
が必要としている属性(カラム)と、Lastname
が必要としている属性(カラム)の全てを合算して作成します。
その上で、Fullname
モデルにtype
という属性を追加します。このtype
という属性には、レコードが、どちらのモデル(Firstname
か、Lastname
か)が元になって作られたレコードなのがが記録されます。
以上により、データベースに実体を持たないFirstname
およびLastname
というモデルの操作を用いて、Fullname
モデルと結びついているfullnames
というテーブルのレコードを操作できます。
何が嬉しいのか
STIを用いてモデルを設計することで、Firstname
およびLastname
のモデルでのふるまいをそれぞれのモデル内で書くことができます。それぞれのモデルの用途により、異なる振る舞いを持たせることができるようになります。
2つのモデルに共通の振る舞いは、継承元であるFullname
に書けばいいでしょう。
補足
「2つのモデル」ではなく、それ以上のモデルを継承関係にすることも可能です。
そして状況によっては、STIを用いるのではなく、ポリモーフィック関連やenumなどを用いる方が設計に合致することがあります。STIはいわゆるEAV(Entity Attribute Value)に直面したときの解決策とまずは考えるのが良いのではないでしょうか。