Ruby
Rails
SQL

コールバックとトランザクションの関係(rails/SQL)

本記事の内容

  • トランザクションとはなにか
  • トランザクションの目的とは
  • 単体の処理を行う場合
  • 複数処理を行う場合
  • ActiveRecordのコールバック処理とトランザクションの関係
    • おまけ

本記事ではrailsのトランザクションの記述方法は記載しておりません。
railsのトランザクション処理が行われたときに、どういったSQLが発行されており、処理の流れをまとめてあります。

トランザクションとは

コンピュータの処理の一連の処理のまとまりのことをいう。
一連処理がすべて上手く実行されたときのみ成功とし、逆に、トランザクション内のどこかでエラーが起こった場合はすべての処理が失敗されたものとする。

銀行の例を考えてみましょう。
AさんがBさんに3000円送金する場合を考えると、この処理は大きく2つに別れます。

  1. Aさんの口座から3000円マイナスする
  2. Bさんの口座に3000円プラスする

このときのシステムについて考えると、
1の処理が行われたあとにエラーが起こり2が実行されなかったとき、Aさんは口座から3000円引かれたのにBさんにもお金がプラスされません、ただ3000円が失われただけになってしまいます。

これでは困りますよね。
コレを防ぐためには1と2の間でエラーが起こったときは1の処理もなかったことにする必要があります。
こういった複数処理の中で1つでも処理が成功しない場合は、すべてをなかったことにする。といったように処理に一貫性を持たせることができるのがトランザクション処理です。

トランザクションの目的とは

あるコードブロックにあるSQL文の変更を全部成功する事を守るためのものである。
Railsに置いては、コードブロック内で例外処理が行われたときのみ、すべての処理をロールバックする。

単体の処理を行う場合

まずはトランザクション処理がどうのようにおこなわれるのか把握するために、単体の処理を行った場合どうなるか見ていきます。単体のトランザクション処理を行った場合は、以下のようにSQL文が作成されます。

BEGIN TRANSACTION                                                                        #transactionの開始
    UPDATE TEST SET ID = '0001' WHERE ID = '0002';     #transaction内の処理を実行(今回はupdate)
COMMIT TRANSACTION                                                                       #COMMITされた瞬間にtransaction内の処理が反映される

複数処理を行う場合

今回は削除と保存の処理をrailsのトランザクション処理を行うと以下のようなSQLが発行されます。

BEGIN TRANSACTION                     #transactionの開始
    DELETE~~~~~~~~~        #transaction内の処理1を実行(delete)
    SAVE ~~~~~~~~~~~       #transaction内の処理2を実行(save)
COMMIT TRANSACTION         #COMMITされた瞬間にtransaction1、2の処理が反映される

もしUPDATEが上手く行かずに例外処理を行った場合は、DELETEもなかったことになり、上記の処理すべてロールバックされて実行されていないことになります。

ここで注意するべきことがあります。
railsのsaveなどのメソッドでは、失敗した時に例外処理を行わないということです。
saveメソッドは失敗時にエラーやfalseを返すため、ロールバック処理は行なわれません。なぜなら例外処理のみロールバック処理を行うからです。

もしrails側でsave、deleteの処理を順番で記述しトランザクションで囲っただけでは、saveでエラーが起こってもSQLはCOMMITまで進み、トランザクション内の処理が実効され、DELETEは上手く実行されているがSAVEがされない時に悲惨なことになります。

これを防ぐためには、Railsではびっくりマーク!がついているメソッドは、失敗したら例外をスローすると意図するので、transactionを使うときは、saveではなくsave!、destroyではなくdestroy!を使うべきでしょう。

ActiveRecordのコールバック処理(save/destory)とトランザクションの関係

ActiveRecordにはbefore_saveやafter_destroyなどsaveやdestroy系のコールバックが複数定義されています。これらのコールバックはRailsでtransaction処理を記述しなくても自動でsaveなどのトランザクション処理の中に含まれます。

なぜこんな事が可能かというと、ActiveRecordには「あるmodelのcreateやupdateは、あるコントローラのcreateやupdateアクションからしか呼び出されない」という事を前提としてcollbackというものを定義しています。

なので「before_saveとか用意した書いてくれたら、勝手にtransactionの中に適宜処理を入れてあげるよー」といった事をactiverecordが実効してくれているのです。

そのためコールバックを用いた場合、発行されるSQLは以下のようになります。

BEGIN TRANSACTION
    ~~~~~~~~~~~~~~(before_saveの処理)
    SAVE ~~~~~~~~~~~
    DELETE ~~~~~~~~~
    ~~~~~~~~~~~~~~(after_destoryの処理)
COMMIT TRANSACTION

詳しくコールバックのメソッドを見たい方はこちらを参考にしてください。
https://qiita.com/rtoya/items/29cef3e328299781a328

おまけ

実のところ「あるmodelのcreateやupdateは、あるコントローラのcreateやupdateアクションからしか呼び出されない事」というのは規模が大きくなってきたときにこのルールを守ることは困難になってきます。そんな時にcollbackを多用していると、callbackに条件分岐などを書くことになったりすることになり、コールバックというものの定義を無視したものになってきます。個人的には、開発を続けるサービスに置いてbefore_saveなどのコールバックはあまり書かない方がいいかもしれないなと思っております。

参考文献

https://www.sejuku.net/blog/27240
https://qiita.com/huydx/items/d946970d130b7dabe7ec