Techioz Blog

Rails の has_one 関連付けは、トランザクションがロールバックされた後でも保存されます [クローズ]

概要

Rails 3 アプリを Rails 5 バージョンにアップグレードしています。 has_one とbelongs_to の関連付けがあります。トランザクションブロックにレコードを保存しています。 Rails 5では、トランザクションがロールバックされた場合にnullデータが「タスク」テーブルに挿入されることがわかりました。 Rails 3 の場合、これは起こりません。これは Rails 5 で予期される動作ですか?既存の機能を壊したくないので、Rails 5でもRails 3の動作を維持する方法はありますか?

以下は、この問題を発生させるサンプルコードです。

class Student < ApplicationRecord
    has_one :task
end

class Task < ApplicationRecord
    belongs_to :student
    validates_presence_of :name
end

student = Student.first
student.name = nil
def student.testing
 begin
  ActiveRecord::Base.transaction do
   t = Task.new
   t.name = 'ddd'
   t.student = self
   t.save!
   puts "raising exception"
   raise "exception"
  end
 rescue => e
 end
end
student.testing
student.save!

解決策

この例から、私が理解できる問題は 1 つだけです。それは、rails が関連付けの inverse_of を検出する方法に関するものです。

class Task < ApplicationRecord
  belongs_to :student
end

class Student < ApplicationRecord
  has_one :task, dependent: :destroy
end
>> Task.reflect_on_association(:student).inverse_of.name
=> :task

inverse_of が設定されているため、student を task.student に割り当てると、その逆も、student.task に割り当てられます。

student = Student.first

student.association(:task).target    #=> nil
Task.new(student: student)
student.association(:task).target    #=> #<Task:0x00007f2333a18ad0 id: nil, name: nil, student_id: 2>

# something to save ---------------------^
student.save!

# INSERT INTO "tasks" ("name", "student_id") VALUES (?, ?)  [["name", nil], ["student_id", 2]]

タスクトランザクションをロールバックしたかどうかは問題ではありません。オブジェクトはまだstudent.taskに割り当てられており、親が保存されるときに新しい関連付けが保存されます。これはまったく同じことを行います:

student = Student.first
Task.transaction do
  Task.create!(student: student)
  raise ActiveRecord::Rollback
end
student.save!

TRANSACTION (0.1ms)  begin transaction
Task Create (0.2ms)  INSERT INTO "tasks" ("name", "student_id") VALUES (?, ?)  [["name", nil], ["student_id", 2]]
TRANSACTION (0.1ms)  rollback transaction
TRANSACTION (0.0ms)  begin transaction
Task Create (0.1ms)  INSERT INTO "tasks" ("name", "student_id") VALUES (?, ?)  [["name", nil], ["student_id", 2]]
TRANSACTION (9.8ms)  commit transaction
class Task < ApplicationRecord
  belongs_to :student, inverse_of: false
end

has_many と同じように動作します。

Task.reflect_on_association(:student).inverse_of #=> nil
student = Student.first

student.association(:task).target    #=> nil
Task.new(student: student)
student.association(:task).target    #=> nil

# nothing to save -----------------------^
student.save!
class Task < ApplicationRecord
  belongs_to :student
end

class Student < ApplicationRecord
  has_many :tasks, dependent: :destroy
end
Task.reflect_on_association(:student).inverse_of # => nil
student = Student.first

student.association(:tasks).target    #=> []
Task.new(student: student)
student.association(:tasks).target    #=> []

# nothing to save ------------------------^
student.save!
class Task < ApplicationRecord
  belongs_to :student, inverse_of: :tasks
end
Task.reflect_on_association(:student).inverse_of.name #=> :tasks
student = Student.first

student.association(:tasks).target    #=> []
Task.new(student: student)
student.association(:tasks).target    #=> [#<Task:0x00007f007d82b2c8 id: nil, name: nil, student_id: 2>]

# something to save ----------------------^
student.save!

# INSERT INTO "tasks" ("name", "student_id") VALUES (?, ?)  [["name", nil], ["student_id", 2]]