ActiveRecordのpolymorphic
公式ドキュメント(Guide): http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
公式ドキュメントの日本語翻訳: [ruby/rails/RailsGuidesをゆっくり和訳してみたよ/Active Record Associations][1]
ちょいちょい公式ドキュメントをざっくり見ながらやってみてる。今回はActiveRecordのpolymorphic association
公式的なのをそのまま使うと、ProductとEmployeeとPictureモデルがあるけど、ProductとEmployee両方がPictureモデルを使うような場合になるとPictureモデル側が所有するデータ自体がどこのモデルに所属する物か判別する目的としてpolymorphicを使用する事で、アソシエーション名_typeのカラムによってどこに所属するのかっていう識別を出来るようになる。まぁそんな感じで一つのモデル(テーブル)で複数のモデル等でレコードを参照するような多様性を持つモデルとして機能させる事が出来る仕様みたいな感じかな
やってみた結果を見た方が分かりやすいような気がするので(※プロジェクト自体はRailsプロジェクトじゃない方で検証)
lib/product.rb
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
lib/employee.rb
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
lib/picture.rb
rbclass Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
んまぁ公式ドキュメントのやつをそのまま使う。上記で書いたけどRailsプロジェクトじゃないでやってるので
CREATE TABLE products(id integer primary key, name varchar(30) not null);
CREATE TABLE pictures(id integer primary key, imageable_id integer, imageable_type varchar(50) not null);
CREATE TABLE employees(id integer primary key, name varchar(30) not null);
な感じでデータベースを作っておく(SQLite)。でその際にpicturesテーブルにはアソシエーション名_idとアソシエーション名_typeのカラムが必要になる
ちなみにActiveRecord::Migration辺りを使う場合には
class CreatePictures < ActiveRecord::Migration
def self.up
create_table :pictures do |t|
t.string :name
t.references :imageable, :polymorphic => true
t.timestamps
end
end
def self.down
drop_table :pictures
end
end
な感じで作れる模様
んじゃ必要なのは定義したんでテストなり書いてる方向で検証
spec/spec_helper.rb
require "logger"
require "database_cleaner"
require "active_record"
require "factory_girl"
require "picture"
require "product"
require "employee"
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => "sample.sqlite3"
)
# SQLのログを取る。特に必要ないなら消して良い
ActiveRecord::Base.logger = Logger.new(STDOUT)
FactoryGirl.find_definitions
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
FactoryGirl.lint
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
# 以降省略
end
な感じでActiveRecord及びFactoryGirl、あとテスト時に処理したデータベースのクリーンアップ関係を設定しておく
spec/factories/fixture.rb
FactoryGirl.define do
factory :product, :class => Product do
name "hoge"
after :create do |product|
product.pictures << create(:picture, :imageable => product)
end
end
factory :employee, :class => Employee do
name "fuga"
after :create do |employee|
employee.pictures << create(:picture, :imageable => employee)
end
end
factory :picture do
# 特に別なカラムを定義してないので
end
end
な感じで、ProductとEmployeeなレコードを一つづつ定義。そしてPictureにそれぞれの参照するレコードを定義(この場合はPictureには2つのレコードが入る)
spec/models/product_spec.rb
require "spec_helper"
describe Product do
it "product test" do
create(:product)
product = Product.find(1)
expect(product).not_to be_nil
pictures = product.pictures
expect(pictures.size).to be(1)
picture = pictures.first
expect(picture).not_to be_nil
expect(picture.imageable).to eq(product)
end
end
というようにProductを取って、そのpicturesなアソシエーションからPictureの参照(has_manyなので配列)を取る。で上記でPictureには2つ突っ込んだけど参照されてるのは1つというのはimageable_typeによってモデルの型により取れるレコードが限られるので
spec/models/employee_spec.rb
describe Employee do
it "employee test" do
create(:employee)
employee = Employee.find(1)
expect(employee).not_to be_nil
pictures = employee.pictures
expect(pictures.size).to be(1)
picture = pictures.first
expect(picture).not_to be_nil
expect(picture.imageable).to eq(employee)
end
end
Productの時と同様なので説明不要
ログに出力したSQL
※説明不明なログは省略(部分的な値は埋め込みで記載)
[Employeeのfixture投入]
begin transaction
INSERT INTO "employees" ("name") VALUES (?) [["name", "fuga"]]
commit transaction
begin transaction
INSERT INTO "pictures" ("imageable_id", "imageable_type") VALUES (?, ?) [["imageable_id", 1], ["imageable_type", "Employee"]]
commit transaction
[Employee.find(1)]
SELECT
"employees".*
FROM
"employees"
WHERE
"employees"."id" = 1
LIMIT 1
[Employee#pictures]
SELECT
"pictures".*
FROM
"pictures"
WHERE
"pictures"."imageable_id" = 1
AND
"pictures"."imageable_type" = "Employee"
ORDER BY "pictures"."id" ASC
LIMIT 1
[Productのfixture投入]
begin transaction
INSERT INTO "products" ("name") VALUES (?) [["name", "hoge"]]
commit transaction
begin transaction
INSERT INTO "pictures" ("imageable_id", "imageable_type") VALUES (?, ?) [["imageable_id", 1], ["imageable_type", "Product"]]
commit transaction
[Product.find(1)]
SELECT
"products".*
FROM
"products"
WHERE
"products"."id" = 1
LIMIT 1
[Product#pictures]
SELECT
"pictures".*
FROM
"pictures"
WHERE
"pictures"."imageable_id" = 1
AND
"pictures"."imageable_type" = "Product"
ORDER BY "pictures"."id" ASC
LIMIT 1
モデル間で関係を結びつける場合はid等が使われるが、polymorphicを使う事でアソシエーション名_typeなカラムによってどのモデルと結びつけが行われてるかを示す事で多数のモデルでのアソシエーションを可能にする多様性のあるモデルとして利用出来るような感じかと(アソシエーション名_idはレコードとの結びつけかと)