LoginSignup
5
2

More than 5 years have passed since last update.

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

Posted at

1ヶ月ほど寝かせておいたらRails5へのアップグレードを諦めた話(或いは、has_many :through の片方がreadonlyな場合の問題)の件が片付いていたので続き。

TL;DR

Rails5.0.2にバージョンアップするか、readonlyなモデルへのhas_many throughのオプションでautosave: falseすると解決するので、Rails4.2からRails5へアップグレードする希望を持てました。

(ただし、元ネタの方は「has_manybelongs_toautosave: 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を利用しています。

テストコードはこんな感じです。

spec/models/user_spec.rb
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とすることです。

app/models/user.rb
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で解決しました。

解決したコードがこちら。

activerecord/lib/active_record/associations/has_many_through_association.rb
      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の同じ場所のコードはこちらです。

activerecord/lib/active_record/associations/has_many_through_association.rb
      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!されません。なのでうまくいっていたんですね。

現場からは以上です。
ご安全に。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2