2017/3/12追記 : 続きのお話をこちらに書きました。
http://qiita.com/moperon/items/02ee2eef17e2ed2d769f
TL;DR
アソシエーションの片方だけreadonlyとした場合の、Rails 4と5での動作の違い と全く同じシチュエーションに私もハマってしまったので、Rails4.2からRails5へのアップグレードを諦めました。
再現手順
Railsアプリ作成
参考 : システムのgemにrailsをインストールせずrails newする
例によってアプリ作ります。
$ mkdir has_many_through_readonly
$ cd has_many_through_readonly
$ echo 'source "https://rubygems.org"' > Gemfile
$ echo 'gem "rails", "4.2.6"' >> Gemfile
$ bundle install --path vendor/bundle
$ bundle exec rails new .
続いて、モデルを作る。UserとRoleが多対多の関係にあり、Roleはreadonlyであるという状態。
$ bin/rails g model user
$ bin/rails g model role
$ bin/rails g model role_user user:references role:references
以下のようにモデルを修正する。
class User < ActiveRecord::Base
has_many :role_users
has_many :roles, through: :role_users
end
class RoleUser < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :role_users
has_many :roles, through: :role_users
def readonly?
true
end
end
role.rb
のreadonly?がtrueなところがポイント。
そして、readonlyなRoleにひとつレコードを追加しておく。
$ bin/rake db:migrate
$ bin/rails dbconsole
sqlite> insert into roles values(1, datetime('now'), datetime('now'));
sqlite> .q
Rails4の挙動
Rails4ではこのようなアソシエーションであってもUserを追加することが可能でした。
irb(main):001:0> role = Role.first
Role Load (0.6ms) SELECT "roles".* FROM "roles" ORDER BY "roles"."id" ASC LIMIT 1
=> #<Role id: 1, created_at: "2017-02-17 23:21:22", updated_at: "2017-02-17 23:21:22">
irb(main):002:0> user = User.create(roles: [role])
(0.1ms) begin transaction
SQL (0.9ms) INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2017-02-17 23:22:23.717950"], ["updated_at", "2017-02-17 23:22:23.717950"]]
SQL (0.7ms) INSERT INTO "role_users" ("role_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["role_id", 1], ["user_id", 1], ["created_at", "2017-02-17 23:22:23.724668"], ["updated_at", "2017-02-17 23:22:23.724668"]]
(1.0ms) commit transaction
=> #<User id: 1, created_at: "2017-02-17 23:22:23", updated_at: "2017-02-17 23:22:23">
irb(main):003:0>
Rails5へアップグレード
参考 : Rails 5へのアップグレード手順メモ(Rails 4.2.6 => 5.0.0)
gem 'rails', '~> 5.0'
$ bundle update rails
$ bin/rails app:update
モデルクラスの継承関係を変更する。
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class User < ApplicationRecord
...snip...
class RoleUser < ApplicationRecord
...snip...
class Role < ApplicationRecord
...snip...
Rails5での挙動
irb(main):001:0> role = Role.first
Role Load (0.7ms) SELECT "roles".* FROM "roles" ORDER BY "roles"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Role id: 1, created_at: "2017-02-17 23:21:22", updated_at: "2017-02-17 23:21:22">
irb(main):002:0> user = User.create(roles: [role])
(0.1ms) begin transaction
SQL (1.0ms) INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?) [["created_at", 2017-02-18 00:17:01 UTC], ["updated_at", 2017-02-18 00:17:01 UTC]]
SQL (0.9ms) INSERT INTO "role_users" ("user_id", "role_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 3], ["role_id", 1], ["created_at", 2017-02-18 00:17:01 UTC], ["updated_at", 2017-02-18 00:17:01 UTC]]
(0.4ms) rollback transaction
ActiveRecord::ReadOnlyRecord: Role is marked as readonly
...snip...
pryでbacktraceとってみた結果はこちら。
[1] pry(main)> role = Role.first
Role Load (0.3ms) SELECT "roles".* FROM "roles" ORDER BY "roles"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Role:0x007f95acea3258 id: 1, created_at: Fri, 17 Feb 2017 23:21:22 UTC +00:00, updated_at: Fri, 17 Feb 2017 23:21:22 UTC +00:00>
[2] pry(main)> user = User.create(roles: [role])
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?) [["created_at", 2017-02-18 00:29:14 UTC], ["updated_at", 2017-02-18 00:29:14 UTC]]
SQL (0.3ms) INSERT INTO "role_users" ("user_id", "role_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 5], ["role_id", 1], ["created_at", 2017-02-18 00:29:14 UTC], ["updated_at", 2017-02-18 00:29:14 UTC]]
(0.5ms) rollback transaction
ActiveRecord::ReadOnlyRecord: Role is marked as readonly
from /Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:539:in `create_or_update'
[3] pry(main)> puts _ex_.backtrace
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:539:in `create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/callbacks.rb:298:in `block in create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:126:in `call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:506:in `block (2 levels) in compile'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:455:in `call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:750:in `_run_save_callbacks'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/callbacks.rb:298:in `create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:125:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/validations.rb:44:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/attribute_methods/dirty.rb:22:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:319:in `block (2 levels) in save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:395:in `block in with_transaction_returning_status'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/connection_adapters/abstract/database_statements.rb:230:in `transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:211:in `transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:392:in `with_transaction_returning_status'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:319:in `block in save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:334:in `rollback_active_record_state!'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:318:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/suppressor.rb:41:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/associations/has_many_through_association.rb:44:in `insert_record'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:402:in `block in save_collection_association'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:393:in `each'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:393:in `save_collection_association'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:185:in `block in add_autosave_association_callbacks'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:158:in `instance_eval'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/autosave_association.rb:158:in `block in define_non_cyclic_method'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:382:in `block in make_lambda'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:207:in `block in halting_and_conditional'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:456:in `block in call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:456:in `each'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:456:in `call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:750:in `_run_create_callbacks'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/callbacks.rb:302:in `_create_record'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/timestamp.rb:68:in `_create_record'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:540:in `create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/callbacks.rb:298:in `block in create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:126:in `call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:506:in `block (2 levels) in compile'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:455:in `call'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:750:in `_run_save_callbacks'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/callbacks.rb:298:in `create_or_update'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:125:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/validations.rb:44:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/attribute_methods/dirty.rb:22:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:319:in `block (2 levels) in save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:395:in `block in with_transaction_returning_status'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `block in transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/connection_adapters/abstract/transaction.rb:189:in `within_new_transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:211:in `transaction'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:392:in `with_transaction_returning_status'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:319:in `block in save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:334:in `rollback_active_record_state!'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/transactions.rb:318:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/suppressor.rb:41:in `save'
/Users/moperon/has_many_through_readonly/vendor/bundle/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record/persistence.rb:34:in `create'
(pry):2:in `<main>'
...snip...
というわけで、Rails4で動いていたコードがRails5では動かなくなりました。
users
とrole_users
にINSERTした後で、なぜrole
をcreate_or_update
しようとしているのか謎です。
原因を追いかけてみたのですが、追いきれず。心折れたので試合終了です。
しばらく寝かせてからまた調べます。
お疲れ様でした。