Railsのトランザクションと原子性のバグ(翻訳)

注記: 本記事のコード例ではhas_and_belongs_to_many関連付け(通称HABTM)が使われていますが、Railsでこれを使ったリレーションは悪手とされています。現在のRailsでは代わりにhas_many :through関連付けを使うのが一般的です。 参考: HABTMリレーションシップは悪であるという論争 概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Transactions in Ruby on Rails and Atomic Bugs 原文公開日: 2017/10/27 著者: Kevin Sylvestre Railsのトランザクションと原子性のバグ(翻訳) Ruby on Railsのトランザクションは、熟練Rails開発者もつまづくことがあるほど扱いが微妙です。次のいくつかの例では、単純なトランザクションすら意図と違う動きをする可能性があることと、それによって気づかないうちに原子性(訳注: atomicity、不可分性とも)が損なわれることを示します。 設定 本記事では、SurveyとQuestionというモデルを持つシンプルなアプリを使って調べます。Surveyには名前が1つ必要で、Questionには何らかのテキストが必要だとします。例では、SurveyとQuestionの間には多対多(has_and_belongs_to_many)のリレーションが設定されています。 rails new sample cd sample rails generate model survey name:string rails generate model question text:string rails generate migration CreateJoinTableSurveyQuestion survey question rake db:create rake db:migrate app/models/survey.rb class Survey < ApplicationRecord validates :name, presence: true has_and_belongs_to_many :questions accepts_nested_attributes_for :questions end app/models/question.rb class Question < ApplicationRecord validates :text, presence: true has_and_belongs_to_many :questions end コード例A has_manyやhas_and_belongs_to_many関連付けによって提供されるヘルパーメソッドは興味深い挙動を示します。次のスニペットをご覧ください。 survey = Survey.create(name: “Shapes”) question = Question.create(text: “九角形の辺はいくつあるか?”) survey.attributes = { name: “”, question_ids: [question.id] } survey.save BEGIN INSERT INTO “questions_surveys” (“survey_id”, “question_id”) VALUES (…, …) COMMIT BEGIN ROLLBACK 上のとおり、attributesでquestion_ids=(またはquestions=)の代入が行われると、バリデーションの外部でCOMMITされるINSERT文がただちに実行され、その後ROLLBACKします。 以下のようにattributes=とsaveをupdateに差し替えると、期待どおりに原子性が保たれます。updateは内部でwith_transaction_returning_statusのすべての変更をラップしています(with_transaction_returning_statusは、トランザクションにラップされたブロックを1つ取るメソッドで、ブロックが「真らしい」と評価された場合はCOMMITを実行し、ブロックが「偽らしい」と評価された場合はROLLBACKします)。 survey = Survey.create(name: “Shapes”) question = Question.create(text: “九角形の辺はいくつあるか?”) survey.update({ name: “”, question_ids: [question.id] }) BEGIN SELECT “questions”.* FROM “questions” WHERE “questions”.”id” IN (…) SELECT “questions”.* FROM “questions” INNER JOIN “questions_surveys” ON “questions”.”id” = “questions_surveys”.”question_id” WHERE “questions_surveys”.”survey_id” = … INSERT INTO “questions_surveys” (“survey_id”, “question_id”) VALUES (…, …) ROLLBACK 次のように書くこともできます。 survey = Survey.create(name: “Shapes”) question = Question.create(text: “九角形の辺はいくつあるか?”) survey.with_transaction_returning_status do survey.attributes = { name: “”, question_ids: [question.id] } survey.save end … Continue reading Railsのトランザクションと原子性のバグ(翻訳)