Rails5へのアップグレードを諦めた話(或いは、has_many :through の片方がreadonlyな場合の問題)


2017/3/12追記 : 続きのお話をこちらに書きました。
http://qiita.com/moperon/items/02ee2eef17e2ed2d769f

# TL;DR

[アソシエーションの片方だけreadonlyとした場合の、Rails 4と5での動作の違い](https://teratail.com/questions/52290) と全く同じシチュエーションに私もハマってしまったので、Rails4.2からRails5へのアップグレードを諦めました。

# 再現手順
## Railsアプリ作成
参考 : [システムのgemにrailsをインストールせずrails newする](http://qiita.com/youcune/items/222777415f00d19cccb4)

例によってアプリ作ります。

```
$ 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
```

以下のようにモデルを修正する。

```rb:app/models/user.rb
class User < ActiveRecord::Base
  has_many :role_users
  has_many :roles, through: :role_users
end
```

```rb:app/models/role_user.rb
class RoleUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end
```

```rb:app/models/role.rb
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
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)](http://qiita.com/ryo511/items/2a387df126268fec8c78)

```rb:Gemfile
gem 'rails', '~> 5.0'
```

```
$ bundle update rails
$ bin/rails app:update
```

モデルクラスの継承関係を変更する。

```rb:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
```
```rb:app/models/user.rb
class User < ApplicationRecord
...snip...
```
```rb:app/models/role_user.rb
class RoleUser < ApplicationRecord
...snip...
```
```rb:app/models/role.rb
class Role < ApplicationRecord
...snip...
```

## Rails5での挙動
```irb
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```しようとしているのか謎です。

原因を追いかけてみたのですが、追いきれず。心折れたので試合終了です。
しばらく寝かせてからまた調べます。

お疲れ様でした。