Help us understand the problem. What is going on with this article?

ポリモーフィック関連付け(Railsガイド写経)

More than 3 years have passed since last update.

Railsガイド「Active Record の関連付け (アソシエーション)」の章、2.9 ポリモーフィック関連付け の写経です。

目的

写経により、ポリモーしてるぜ感を味わう。

準備

railsプロジェクト testapp を作ってあります。

[ykt68@macbook testapp]$ ruby -v
ruby 2.2.6p396 (2016-11-15 revision 56800) [x86_64-darwin15]
[ykt68@macbook testapp]$ rails -v
Rails 5.0.1
[ykt68@macbook testapp]$ ls
Gemfile     README.md   app     config      db      log     test        vendor
Gemfile.lock    Rakefile    bin     config.ru   lib     public      tmp
[ykt68@macbook testapp]$ rails s
=> Booting Puma
=> Rails 5.0.1 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.7.0 (ruby 2.2.6-p396), codename: Snowy Sagebrush
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
^CExiting
[ykt68@macbook testapp]$

データベースはMySQLです。

[ykt68@macbook testapp]$ cat config/database.yml 
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password: root
  socket: /Applications/MAMP/tmp/mysql/mysql.sock

development:
  <<: *default
  database: testapp_dev

モデル作成

(1) rails g model にて以下の3つのモデルを作成します。

Picture

[ykt68@macbook testapp]$ rails g model picture name:string imageable:references
Running via Spring preloader in process 5156
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170219012701_create_pictures.rb
      create    app/models/picture.rb
      invoke    test_unit
      create      test/models/picture_test.rb
      create      test/fixtures/pictures.yml
[ykt68@macbook testapp]$

Employee

[ykt68@macbook testapp]$ rails g model employee name:string
Running via Spring preloader in process 5261
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170219012813_create_employees.rb
      create    app/models/employee.rb
      invoke    test_unit
      create      test/models/employee_test.rb
      create      test/fixtures/employees.yml
[ykt68@macbook testapp]$

Product

[ykt68@macbook testapp]$ rails g model product name:string
Running via Spring preloader in process 5331
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170219012838_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml
[ykt68@macbook testapp]$

(2) 作成されたモデルを以下のように修正していきます。

Picture

修正前:

class Picture < ApplicationRecord
  belongs_to :imageable
end

polymorphic: trueを追加。

修正後:

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

Employee

修正前:

class Employee < ApplicationRecord
end

has_many :pictures, as: :imageableを追加します。
修正後:

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

Product

修正前:

class Product < ApplicationRecord
end

has_many :pictures, as: :imageableを追加します。
修正後:

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

マイグレーション

CreatePictures

修正前:

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, foreign_key: true

      t.timestamps
    end
  end
end

foreign_key を polymorphicに修正して以下とします。
修正後:

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true

      t.timestamps
    end
  end
end

CreatePicturesおよびCreateEmployeesは修正不要で、migration を実行


[ykt68@macbook testapp]$ rails db:migrate
== 20170219102701 CreatePictures: migrating ===================================
-- create_table(:pictures)
   -> 0.0274s
== 20170219102701 CreatePictures: migrated (0.0275s) ==========================

== 20170219102813 CreateEmployees: migrating ==================================
-- create_table(:employees)
   -> 0.0246s
== 20170219102813 CreateEmployees: migrated (0.0247s) =========================

== 20170219102838 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0186s
== 20170219102838 CreateProducts: migrated (0.0187s) ==========================

[ykt68@macbook testapp]$

picturesテーブルの確認

MySQLコンソールから以下で確認

mysql> SHOW CREATE TABLE pictures;
+----------+-----------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                                                                                                                          |
+----------+-----------------------------------------------------------------------------+
| pictures | 
CREATE TABLE `pictures` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `imageable_type` varchar(255) DEFAULT NULL,
  `imageable_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_pictures_on_imageable_type_and_imageable_id` (`imageable_type`,`imageable_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+----------+-----------------------------------------------------------------------------+```
1 row in set (0.00 sec)

上記のDDLを確認すると、pictures テーブルを作成するマイグレーションの

t.references :imageable, polymorphic: true

が、

  `imageable_type` varchar(255) DEFAULT NULL,
  `imageable_id` int(11) DEFAULT NULL,

として反映されている。

サンプルデータの投入

  後の確認を分かりやすくするため、employeesテーブルのidを201から、
productsのidを9001から付番するようにして、以下でサンプルデータ作成

db/seed.rb
ActiveRecord::Base.connection_pool.with_connection { |con|
  con.execute('ALTER TABLE employees AUTO_INCREMENT=201')
  con.execute('ALTER TABLE products AUTO_INCREMENT=9001')
}

%w[野比 剛田 骨川 出木杉].each do |e|
  Employee.create name:e
end

%w[どこでもドア タケコプター もしもボックス].each do |e|
  Product.create name:e
end

db:seed タスクを実行

[ykt68@macbook testapp]$ rails db:seed

動作確認

ここからは、rails console で確認

[ykt68@macbook testapp]$ rails c
Running via Spring preloader in process 8313
Loading development environment (Rails 5.0.1)

1.Employee とProductのレコードを全件表示

irb(main):001:0> Employee.all
  Employee Load (0.4ms)  SELECT `employees`.* FROM `employees`
=> #<ActiveRecord::Relation [#<Employee id: 201, name: "野比", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">, #<Employee id: 202, name: "剛田", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">, #<Employee id: 203, name: "骨川", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">, #<Employee id: 204, name: "出木杉", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">]>
irb(main):002:0> Product.all
  Product Load (0.5ms)  SELECT `products`.* FROM `products`
=> #<ActiveRecord::Relation [#<Product id: 9001, name: "どこでもドア", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">, #<Product id: 9002, name: "タケコプター", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">, #<Product id: 9003, name: "もしもボックス", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">]>

2.Picture レコードを2件保存

irb(main):003:0> pic1 = Picture.create name:'ある日ののび太', imageable: Employee.find(201)
  Employee Load (0.6ms)  SELECT  `employees`.* FROM `employees` WHERE `employees`.`id` = 201 LIMIT 1
   (0.2ms)  BEGIN
  SQL (0.4ms)  INSERT INTO `pictures` (`name`, `imageable_type`, `imageable_id`, `created_at`, `updated_at`) VALUES ('ある日ののび太', 'Employee', 201, '2017-02-19 02:26:49', '2017-02-19 02:26:49')
   (0.5ms)  COMMIT
=> #<Picture id: 1, name: "ある日ののび太", imageable_type: "Employee", imageable_id: 201, created_at: "2017-02-19 02:26:49", updated_at: "2017-02-19 02:26:49">
irb(main):004:0> pic2 = Picture.create name:'どらえもん、もしもボックスを取り出す', imageable: Product.find(9003)
  Product Load (0.6ms)  SELECT  `products`.* FROM `products` WHERE `products`.`id` = 9003 LIMIT 1
   (0.2ms)  BEGIN
  SQL (0.4ms)  INSERT INTO `pictures` (`name`, `imageable_type`, `imageable_id`, `created_at`, `updated_at`) VALUES ('どらえもん、もしもボックスを取り出す', 'Product', 9003, '2017-02-19 02:27:35', '2017-02-19 02:27:35')
   (3.3ms)  COMMIT
=> #<Picture id: 2, name: "どらえもん、もしもボックスを取り出す", imageable_type: "Product", imageable_id: 9003, created_at: "2017-02-19 02:27:35", updated_at: "2017-02-19 02:27:35">

3.Pictureのimageableプロパティに異なるモデルのオブジェクトが入ることを確認

irb(main):005:0> pic1.imageable
=> #<Employee id: 201, name: "野比", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">
irb(main):006:0> pic2.imageable
=> #<Product id: 9003, name: "もしもボックス", created_at: "2017-02-19 02:21:47", updated_at: "2017-02-19 02:21:47">

うぉぉ、ポリモーしてるぜ。ActiveRecord便利杉ワロタ。

  ちなみに、pictures テーブルの

  • imageable_type カラムには、モデルクラス名
  • imageable_id カラムには、それぞれのクラスのid

が入っていることを以下で確認

mysql> select * from pictures;
+----+---------------------------------+----------------+--------------+---------------------+---------------------+
| id | name                            | imageable_type | imageable_id | created_at          | updated_at          |
+----+---------------------------------+----------------+--------------+---------------------+---------------------+
|  1 | ある日ののび太                    | Employee       |          201 | 2017-02-19 02:26:49 | 2017-02-19 02:26:49 |
|  2 | どらえもん、もしもボックスを取り出す  | Product        |         9003 | 2017-02-19 02:27:35 | 2017-02-19 02:27:35 |
+----+---------------------------------+----------------+--------------+---------------------+---------------------+
2 rows in set (0.01 sec)

4.imageable_typeを、存在しないクラス名に変えてみると

以下のように強引にクラス名を変えてみます。

mysql> update pictures set imageable_type='Hoge' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from pictures where id=1;
+----+------------------+----------------+--------------+---------------------+---------------------+
| id | name             | imageable_type | imageable_id | created_at          | updated_at          |
+----+------------------+----------------+--------------+---------------------+---------------------+
|  1 | ある日ののび太     | Hoge           |          201 | 2017-02-19 02:26:49 | 2017-02-19 02:26:49 |
+----+------------------+----------------+--------------+---------------------+---------------------+
1 row in set (0.00 sec)

その後、このPictureレコードを取り出そうとしてみます。

[ykt68@macbook testapp]$ rails c
Running via Spring preloader in process 10855
Loading development environment (Rails 5.0.1)

Picture.find 1でエラーになるかと思いきや、

irb(main):001:0> pict1 = Picture.find 1
  Picture Load (0.6ms)  SELECT  `pictures`.* FROM `pictures` WHERE `pictures`.`id` = 1 LIMIT 1
=> #<Picture id: 1, name: "ある日ののび太", imageable_type: "Hoge", imageable_id: 201, created_at: "2017-02-19 02:26:49", updated_at: "2017-02-19 02:26:49">

上記のとおりエラーになりませんでした。
が、pict1imageableを取りだそうとすると、以下のようにNameError が発生しました。

irb(main):002:0> pict1.imageable
NameError: uninitialized constant Hoge
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/inflector/methods.rb:268:in `const_get'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/inflector/methods.rb:268:in `block in constantize'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/inflector/methods.rb:266:in `each'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/inflector/methods.rb:266:in `inject'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/inflector/methods.rb:266:in `constantize'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activesupport-5.0.1/lib/active_support/core_ext/string/inflections.rb:66:in `constantize'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/belongs_to_polymorphic_association.rb:7:in `klass'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/belongs_to_association.rb:55:in `find_target?'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/association.rb:138:in `load_target'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/association.rb:53:in `reload'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/singular_association.rb:14:in `reader'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/activerecord-5.0.1/lib/active_record/associations/builder/association.rb:111:in `imageable'
    from (irb):2
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/railties-5.0.1/lib/rails/commands/console.rb:65:in `start'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/railties-5.0.1/lib/rails/commands/console_helper.rb:9:in `start'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/railties-5.0.1/lib/rails/commands/commands_tasks.rb:78:in `console'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/railties-5.0.1/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/gems/2.2.0/gems/railties-5.0.1/lib/rails/commands.rb:18:in `<top (required)>'
    from /Users/ykt68/MyProjects/testapp/bin/rails:9:in `<top (required)>'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    from /Users/ykt68/.rbenv/versions/2.2.6/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    from -e:1:in `<main>'
irb(main):003:0>

以上です。

  Pictureimageableプロパティで取得できるクラスを、EmployeeProductだけではなく
後から増やしていけますね。便利。

jun68ykt
ときどき teratail で回答してます。
https://teratail.com/users/jun68ykt
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした