はじめに
公式ドキュメントのポリモーフィック関連の項目を見た際に、挙動がイメージしにくかったので、ポリモーフィック関連を持つモデルを作成しつつ、DBのデータやコンソールで動きを確認します。
環境
- OS : Ubuntu 17.04
- Ruby : 2.6.3
- Rails: 5.2.3
- MySQL: 5.7.20
ポリモーフィズム(多様性)
ポリモーフィックと聞くと、オブジェクト指向プログラミングで出てくるポリモーフィズムを思い出す人が多いはずです。
ポリモーフィズムとは、プログラミング言語の持つ性質の一つで、ある関数やメソッドなどが、引数や返り値の数やデータ型などの異なる複数の実装を持ち、呼び出し時に使い分けるようにできること。
(略)
オブジェクト指向プログラミング言語では親クラスから派生(継承)した子クラスがメソッドの内容を上書き(オーバーライド)したり、インターフェースで定義されたメソッドを実装することによりこれを実現している。
オブジェクト指向プログラミングの、ポリモーフィズムでは、継承やインターフェース1を使って同名のメソッドを複数のクラスで定義します。それによって同名のメソッドでも、インスタンスごとに振る舞いを変える(多様性2が生まれる)というものです。
ただ、ActiveRecordのポリモーフィック関連は、継承やインターフェース1を用いるのではなく、ActiveRecordで定義されている関連付けの機能を使用します。
(用途は異なりますが継承を用いるシングルテーブル継承 (STI)というものもあります)
ポリモーフィック関連を持つモデルの作成
- ActiveRecordマイグレーション - Railsガイド 2.2 モデルを生成する
- ActiveRecordの関連付け(アソシエーション) - Railsガイド 2.9 ポリモーフィック関連付け
Railsガイドを元に、ポリモーフィック関連付けのモデルを作成します。
作成するモデルは、Picture/Employee/Productの3つです。
ここでは、モデルPictureと複数のモデルEmployee/Productとの関連を、imageableという関連1つで表現します。
$ bin/rails generate model Product name:string
Running via Spring preloader in process 21448
invoke active_record
create db/migrate/20190504040556_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
$ bin/rails generate model Employee name:string
# 省略
$ bin/rails generate model Picture name:string
# 省略
PictureのMigrationファイルを修正します。
class CreatePictures < ActiveRecord::Migration[5.2]
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
作成した、それぞれのモデルに関連を記載します。
# app/models/picture.rb
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
# app/models/employee.rb
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
# app/models/product.rb
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
マイグレーションを実行します。
$ bin/rails db:migrate
== 20190504040556 CreateProducts: migrating ===================================
-- create_table(:products)
-> 0.0588s
== 20190504040556 CreateProducts: migrated (0.0590s) ==========================
== 20190504040742 CreateEmployees: migrating ==================================
-- create_table(:employees)
-> 0.0648s
== 20190504040742 CreateEmployees: migrated (0.0650s) =========================
== 20190504041111 CreatePictures: migrating ===================================
-- create_table(:pictures)
-> 0.0630s
-- add_index(:pictures, [:imageable_type, :imageable_id])
-> 0.0678s
== 20190504041111 CreatePictures: migrated (0.1311s) ==========================
MySQLのデータを確認
DBeaver をつかってデータを確認します。
ポリモーフィック関連を実現するために、テーブルpicturesに、imageable_id/imageable_typeというカラムが追加されてます。
Migrationファイルの定義が適応された影響ですが、imageableという単語は、モデルEmployee/Productでも定義されていす。
has_many :pictures, as: :imageable
説明をすると、「複数のpictureを持ち、その関連はimageableとする」といったところでしょうか。
モデルの作成
動きを確認するために、モデルを作成します。
$ bin/rails c
> product = Product.create(name: '商品1')
=> #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12">
> employee = Employee.create(name: '田中一郎')
=> #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23">
> product.pictures
=> #<ActiveRecord::Associations::CollectionProxy []>
> employee.pictures
=> #<ActiveRecord::Associations::CollectionProxy []>
ここはhas_many
の動きです。まだPictureを作成していないのでデータがヒットしません。
product.pictures.create(name: '商品1-1.jpg')
=> #<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46">
employee.pictures.create(name: '田中一郎_1.jpg')
=> #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">
それぞれのProduct/Employeeで、picturesを作成したところ、こちらで指定していないのにもかかわらずimageable_idとimageable_typeに値が入っています。
今度はPictureから、Product/Employeeの関連を見てみましょう。
> pictures = Picture.all
Picture Load (0.9ms) SELECT `pictures`.* FROM `pictures` LIMIT 11
=> #<ActiveRecord::Relation [#<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46">, #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">]>
> pictures.first.imageable
Picture Load (0.8ms) SELECT `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' ASC LIMIT 1
Product Load (1.1ms) SELECT `products`.* FROM `products` WHERE `products`.'id' = 1 LIMIT 1
=> #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12">
> pictures.last.imageable
Picture Load (0.8ms) SELECT `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' DESC LIMIT 1
Employee Load (1.2ms) SELECT `employees`.* FROM `employees` WHERE `employees`.'id' = 1 LIMIT 1
=> #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23">
(省略していた、SQL文のログも記載してます)
Pictureでは、Product/Employeeの値を参照する際に、Product/Employeeで定義していたimageableを使用して参照することができます。
ログから察するに、1回目のSQLでテーブルpicturesのデータを取得、2回目のSQLでpicturesのimageable_id/imageable_typeの値を使用して、検索するテーブルと、id値を決めているようです。
MySQLのデータを確認
コンソールで確認したように、それぞれのテーブルにデータが格納されています。
注意する点としてはpicturesのimageable_id/imageable_typeはRails(アプリ側)だと、ポリモーフィック関連を提供するカラムとして認識されますが、DB側にはポリモーフィック関連を表現する機能がないため「インデックスが張られているカラム」という認識しかありません。
例えば、アプリ側でEmployeeもしくはProductのデータを削除した際にdependent
を使い、Pictureのimageable_id/imageable_typeのデータに手を加え関連の整合性を保つことができます。しかし、DB側でemployeeもしくはproductsを削除しても外部キー制約などを設定できない(このカラムでは外部キーとなるテーブルが明確でない)ため、picturesとの関連の整合性が取れなくなります。
まとめ
- ActiveRecordのポリモーフィック関連は、ポリモーフィズムと目的は同様
- しかし、オブジェクト指向プログラミングなどと手法が異なる
- x_id/x_typeといったカラムを追加して、対象のオブジェクト(テーブル)名と主キーのidを格納する
- 1つの関連で、複数のオブジェクトの関連を表す事ができる
- ポリモーフィック関連はActiveRecordの機能であり、DBはポリモーフィックの整合性を担保しない
-
ここでいうインターフェースは、Javaの実装におけるインターフェースを想定しているような気がします。 ↩
-
オブジェクト指向プログラミングでよく例に挙げられるものは、厳密には「ポリモーフィズムの部分型付け」を指すようです。Wikipedia ポリモーフィズム ↩