ActiveRecordのpolymorphic

2014-08-07T00:00:00+00:00 rspec Ruby Rails

公式ドキュメント(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はレコードとの結びつけかと)

[1]: http://wiki.usagee.co.jp/ruby/rails/RailsGuides%E3%82%92%E3%82%86%E3%81%A3%E3%81%8F%E3%82%8A%E5%92%8C%E8%A8%B3%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F%E3%82%88/Active%20Record%20Associations#t758e640

JAX-RSをやってみる (16) - ParamConverter - FactoryGirlのロードの仕組み