1ヶ月ほど寝かせておいたらRails5へのアップグレードを諦めた話(或いは、has_many :through の片方がreadonlyな場合の問題)の件が片付いていたので続き。
TL;DR
Rails5.0.2にバージョンアップするか、readonlyなモデルへのhas_many through
のオプションでautosave: false
すると解決するので、Rails4.2からRails5へアップグレードする希望を持てました。
(ただし、元ネタの方は「has_many
やbelongs_to
にautosave: false
のオプションを追加する」を試したとされているので、また別の事象かもしれません。)
再現試験
こちらにPoCを置きました。
https://github.com/moperon/readonly_has_many_through
使い方
$ git clone https://github.com/moperon/readonly_has_many_through.git
$ cd readonly_has_many_through
$ git branch -r
origin/HEAD -> origin/master
origin/master
origin/rails4.2.6
origin/rails5.0.0.1
origin/rails5.0.0.1_workaround
origin/rails5.0.1
origin/rails5.0.1_workaround
origin/rails5.0.2
$
各Railsバージョンごとにbranch切ってあります。問題を確認したバージョン(Rails5.0.0.1とRails5.0.1)についてはworkaroundも試しています。
モデルの構成は前回記事を参照してください。ただし、今回のPoCではactiverecord-be_readonlyを利用しています。
テストコードはこんな感じです。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'who has existing role' do
fixtures :roles
let(:role) { Role.first }
let(:user) { User.new(roles: [role]) }
context 'when created' do
subject { user.save }
it { is_expected.to be true }
end
end
end
このPoCではworkaroundも試しています。has_many through
のオプションでautosave: false
とすることです。
class User < ApplicationRecord
has_many :role_users
has_many :roles, through: :role_users, autosave: false
end
Rails4.2.6の場合
テストは問題なくpassします。
$ git checkout rails4.2.6
$ bundle install
$ bin/rake db:setup RAILS_ENV=test
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 22179
.
Finished in 0.08095 seconds (files took 0.58613 seconds to load)
1 example, 0 failures
$
Rails5.0.0.1 の場合
テストは失敗します。
$ git checkout rails5.0.0.1
$ bundle install
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 22895
F
Failures:
1) User who has existing role when created
Failure/Error: subject { user.save }
ActiveRecord::ReadOnlyRecord:
Role is marked as readonly
# ./spec/models/user_spec.rb:11:in `block (4 levels) in <top (required)>'
# ./spec/models/user_spec.rb:13:in `block (4 levels) in <top (required)>'
# -e:1:in `<main>'
Finished in 0.08019 seconds (files took 0.56101 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:13 # User who has existing role when created
$
rails5.0.0.1 workaround
workaroundは有効でした。
$ git checkout rails5.0.0.1_workaround
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 23196
.
Finished in 0.10364 seconds (files took 0.66413 seconds to load)
1 example, 0 failures
$
Rails5.0.1の場合
前回の記事で試したバージョンです。前回確認した通りです。
$ git checkout rails5.0.1
$ bundle install
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 23743
F
Failures:
1) User who has existing role when created
Failure/Error: subject { user.save }
ActiveRecord::ReadOnlyRecord:
Role is marked as readonly
# ./spec/models/user_spec.rb:11:in `block (4 levels) in <top (required)>'
# ./spec/models/user_spec.rb:13:in `block (4 levels) in <top (required)>'
# -e:1:in `<main>'
Finished in 0.09718 seconds (files took 0.64053 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:13 # User who has existing role when created
$
Rails5.0.1 workaround
こちらもworkaroundが有効でした。
$ git checkout rails5.0.1_workaround
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 25048
.
Finished in 0.09832 seconds (files took 0.71255 seconds to load)
1 example, 0 failures
$
Rails5.0.2の場合
workaroundなしでテストは成功します。
$ git checkout rails5.0.2
$ bundle install
$ bin/rspec spec/models/user_spec.rb
Running via Spring preloader in process 24408
.
Finished in 0.07626 seconds (files took 0.69258 seconds to load)
1 example, 0 failures
$
どうしてこうなった
v5.0.1とv5.0.2の差分はこちらで見られます。今回注目している事象はこちらのPRで解決しました。
解決したコードがこちら。
def insert_record(record, validate = true, raise = false)
ensure_not_nested
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
+ if record.new_record? || record.has_changes_to_save?
+ if raise
+ record.save!(validate: validate)
+ else
+ return unless record.save(validate: validate)
+ end
end
save_through_record(record)
詳しくはPRと関連するissueを参照していただくとして、簡単に解説すると、既存のchild(role)をセットしたparent(user)をcreateするとき、childに変更がなくてもchildがsave!されるのは無駄なので、必要な時だけsave!するようにしよう。という趣旨のものです。
ちなみに、Rails4.2-stableの同じ場所のコードはこちらです。
def insert_record(record, validate = true, raise = false)
ensure_not_nested
if record.new_record?
if raise
record.save!(:validate => validate)
else
return unless record.save(:validate => validate)
end
end
save_through_record(record)
Rails5.0.2のものとも微妙に違うけど、今回のケースに関してはrecordに入ってくるchild(role)がnew_recordではないのでsave!されません。なのでうまくいっていたんですね。
現場からは以上です。
ご安全に。