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から付番するようにして、以下でサンプルデータ作成
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">
上記のとおりエラーになりませんでした。
が、pict1
のimageable
を取りだそうとすると、以下のように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>
以上です。
Picture
のimageable
プロパティで取得できるクラスを、Employee
とProduct
だけではなく
後から増やしていけますね。便利。