JQ Blog

RailsのモデルのSTI(Single Table Inheritance)とポリモーフィックの実装

RailsのActiveRecordには単一テーブル継承(STI)とポリモーフィック関係という二つの便利な関係性設定がある。同じような機能を持っているモデルが多数存在してそのモデルたちをまとめたいときとか、多数のモデルが一つのモデルを持ちたいときなどの複雑な関係性をもっと簡単にしてくれるのでその二つの関係性をまとめる。

STI

状況設定

例えば、本とコンピューターを同時に管理するproductsというモデルを作る。もちろんbooks、computersの二つのモデルを作ってもできなくはないけどSTIを通して実装すればテーブルを作らなくてもproductsの一つのテーブルで管理できる。

Productモデル作成

1
2
$ rails g model product name:string price:integer type:string author:string category:string maker:string inch:integer
$ rake db:migrate

ここでtypeは必須になる。author, categoryはbookのために、maker, inchはcomputerのためのcolumnになる。

class作成

1
2
class Product < ApplicationRecord
end

こういうclassが作られたら実際に使いたいbook、computerのclassを作成。

1
2
class Book < Product
end
1
2
class Computer < Product
end

気をつけないとならないのはProductのclassを継承すること。
これで設定は完了。
すると、

1
Computer.create(name: 'お得パソコン', price: 100000, maker: 'apple', inch: 15)

こういうふうにデータを作成したら

1
INSERT INTO "products" ("name", "price", "type", "maker", "inch", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["name", "お得パソコン"], ["price", 100000], ["type", "Computer"], ["maker", "apple"], ["inch", 15], ["created_at", 2017-01-18 08:57:39 UTC], ["updated_at", 2017-01-18 08:57:39 UTC]]

こんな結果になり、Productテーブルに保存される。typeはclass名になる。
呼び出すのも簡単にComputer.find(Product.id)Computer.allでできる。
Computer.find(Product.id)を実行したら

1
<Computer id: 1, name: "お得パソコン", price: 100000, type: "Computer", author: nil, category: nil, maker: "apple", inch: 15, created_at: "2017-01-25 06:44:17", updated_at: "2017-01-25 06:44:17">

bookのために作ったcolumnは全部nilになる。つまり、一つのテーブルで三つのテーブルを使うようにはなるけど無駄なcolumnが多くなるデメリットもある。

Polymorphic

状況設定

プロジェクトの中で一つのモデルを多数のモデルに関係をつける時がある。
例えば、掲示板の投稿をユーザーが作成した場合と管理者が作成した場合があるとするとPolymorphicで管理できる。

ポリモフィックなテーブルの作成

1
2
3
4
$ rails g model admin name:string email:string
$ rails g model user name:string email:string
$ rails g model post owner:references{polymorphic}:index title:string content:text
$ rake db:migrate

するとpostのmigrationは下記のようになる。

1
2
3
4
5
6
7
8
9
10
11
class CreatePosts < ActiveRecord::Migration[5.0]
  def change
    create_table :posts do |t|
      t.references :owner, polymorphic: true
      t.string :title
      t.text :content

      t.timestamps
    end
  end
end

モデルにbelongs_toとhas_many追加

上記のように実行したらpostのクラスはさらにこういうふうになってます。そしてpostのテーブルにはowner_type, owner_idが生成されます。

1
2
3
class Post < ApplicationRecord
  belongs_to :owner, polymorphic: true
end

そこでadminとuser側だけhas_manyを追加したら良いです。

1
2
3
4
5
6
7
class Admin < ApplicationRecord
  has_many :posts, as: :owner, dependent: :destroy
end

class User < ApplicationRecord
  has_many :posts, as: :owner, dependent: :destroy
end

実行

そうした上にconsoleに下記のように実行してみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
admin = Admin.create(name: 'admin1', email: 'admin@example.com')
=> #<Admin id: 1, name: "admin1", email: "admin@example.com", created_at: "2017-01-25 07:46:25", updated_at: "2017-01-25 07:46:25">
post1 = admin.posts.create(title: 'post1', content: 'content1')
=> #<Post id: 1, owner_type: "Admin", owner_id: 1, title: "post1", content: "content1", created_at: "2017-01-25 07:47:21", updated_at: "2017-01-25 07:47:21">
post1.owner
=> #<Admin id: 1, name: "admin1", email: "admin@example.com", created_at: "2017-01-25 07:46:25", updated_at: "2017-01-25 07:46:25">
user = User.create(name: 'user1', email: 'user@example.com')
=> #<User id: 1, name: "user1", email: "user@example.com", created_at: "2017-01-25 07:48:00", updated_at: "2017-01-25 07:48:00">
post2 = user.posts.create(title: 'post2', content: 'content2')
=> #<Post id: 2, owner_type: "User", owner_id: 1, title: "post2", content: "content2", created_at: "2017-01-25 07:49:04", updated_at: "2017-01-25 07:49:04">
post2.owner
=> #<User id: 1, name: "user1", email: "user@example.com", created_at: "2017-01-25 07:48:00", updated_at: "2017-01-25 07:48:00">


Post.find_by(owner: admin)
=> #<Post id: 1, owner_type: "Admin", owner_id: 1, title: "post1", content: "content1", created_at: "2017-01-25 07:47:21", updated_at: "2017-01-25 07:47:21">
Post.find_by(owner: user)
=> #<Post id: 2, owner_type: "User", owner_id: 1, title: "post2", content: "content2", created_at: "2017-01-25 07:49:04", updated_at: "2017-01-25 07:49:04">

こういうふうに活用できる。


参照

Rails4でポリモフィックのリレーションを実装する
Rails4でSTI(単一継承テーブル)を行う
Rails ActiveRecordのSTI(Single Table Inheritance)の使い方