RailsにおけるDB制約のワナ
DBに制約を張る
このようなテーブルがあったとします。
CREATE TABLE products( id serial NOT NULL, code varchar NOT NULL, price integer NOT NULL CHECK (price > 0), CONSTRAINT pk_products PRIMARY KEY (id) );
- 各カラムにNOT NULL制約
- priceにCHECK制約
という2点を除けば、rails g migration
で作られるものと同じだと思います。
このテーブルにおいて、
ActiveRecord::Base.transaction do CSV.foreach(csv_path) do |row| code, price = row[0], row[1] Product.create!(code: code, price: price) end end
というような処理を行おうとすると、制約に違反するデータが来た時にPG::Error
やPG::CheckViolation
という例外が発生してトランザクションがロールバックされます。
ここまでは想定通り。
制約に違反するデータを無視する
制約に違反するデータが1件でもあれば全体をロールバックしたいのであれば上記のコードで良いです。 しかし、DB接続の前後で例外が発生した場合はロールバックし、制約に違反するデータは無視して処理を続けたい場合、つまり、例外を投げる処理A,Bを含む次のようなコードの場合、
ActiveRecord::Base.transaction do # 処理A(例外を投げる可能性アリ) CSV.foreach(csv_path) do |row| code, price = row[0], row[1] Product.create!(code: code, price: price) end # 処理B(例外を投げる可能性アリ) end
素直に
ActiveRecord::Base.transaction do # 処理A(例外を投げる可能性アリ) CSV.foreach(csv_path) do |row| code, price = row[0], row[1] Product.create(code: code, price: price) # create!メソッドをcreateに変更 end # 処理B(例外を投げる可能性アリ) end
というようなコードを書くと、制約に違反するデータが渡された後に呼ばれるcreate
メソッドでActiveRecord::StatementInvalid: PG InFailedSqlTransaction
*1という例外が吐かれます。
これはPG::Error
やPG::CheckViolation
がRailsの中で発生した例外ではなく、PostgreSQL側で生じた例外なのでRailsに例外が渡されるより前にPostgreSQL内部でロールバックが走っていることに起因します。
ぽすぐれ「ロールバックされたトランザクションにはINSERTできないよ☆」
ってことです。
対策
PostgreSQLに不正なデータが渡さないということが必要なので、Rails側で値のチェックをする必要があります。
そうです、validationです。
まとめ
忘れずにvalidationしろというお話。
*1:PostgreSQLの場合。