Rails 7: PostgreSQLの外部キー制約にdeferrableを指定可能になった(翻訳)
データベースの外部キー制約は、参照整合性を強制します。これによって子レコードを作成する前に親レコードが存在しなければならないことを保証できるようになり、制約は各ステートメントの直後でただちに強制されます。
しかし、制約のdeferrable
を変更すればこの動作を変更可能です。deferrable
は、制約をただちにチェックするのではなく、トランザクションがコミットされるまでチェックを延期(defer)するという意味です。PostgreSQLは制約を延期可能なSET CONSTRAINTS
機能をサポートしています。
Rails 7より前は、PostgreSQLでdeferrable
制約を使うために生SQLを書く必要がありました。
変更前
# /db/migrate/20220322071034_add_foreign_key_to_reviews.rb
add_foreign_key :reviews, :products
Product.transaction do
review = Review.create!(product_id: 11, comment: "amazing product")
product = Product.create!(id: 11, title: "Hand Sanitizer", description: "new sanitizer in town")
end
上のコードによって以下のエラーが発生します。
TRANSACTION (1.0ms) ROLLBACK
/Users/murtazabagwala/.rvm/gems/ruby-3.0.2/gems/activerecord-7.0.2.3/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': PG::ForeignKeyViolation: ERROR: insert or update on table "reviews" violates foreign key constraint "fk_rails_bedd9094d4" (ActiveRecord::InvalidForeignKey)
DETAIL: Key (product_id)=(11) is not present in table "products".
変更後
Rails 7では、マイグレーションのadd_foreign_key
ステートメントに:deferrable
オプションを渡すことでこのチェックを延期できるようになりました。
このオプションは、外部キーをdeferrableにするかどうかを指定できます。:deferrable
に指定できる値はブーリアン、 :deferred
、:immediate
のいずれかです。
deferrable: false
- デフォルト値。制約のチェックは常に即時実行される。
deferrable: true
- デフォルトの振る舞いは変更しない。トランザクション内でチェックを手動でdeferできる。
deferrable: :deferred
- 制約チェックはトランザクションのコミット後に実行される。トランザクション内で制約の振る舞いを変更できる。
deferrable: :immediate
- 制約のチェックは即時実行される。トランザクション内で制約の振る舞いを変更できる。
# /db/migrate/20220322071050_add_foreign_key_to_reviews.rb
add_foreign_key :reviews, :products, deferrable: :deferred
Product.transaction do
review = Review.create!(product_id: 11, comment: "amazing product")
product = Product.create!(id: 11, title: "Hand Sanitizer", description: "new sanitizer in town")
end
上のコードによるレコードの作成は以下のように成功します。
TRANSACTION (1.2ms) BEGIN
Review Create (1.7ms) INSERT INTO "reviews" ("comment", "created_at", "updated_at", "product_id") VALUES ($1, $2, $3, $4) RETURNING "id" [["comment", "amazing product"], ["created_at", "2022-03-22 07:44:17.296514"], ["updated_at", "2022-03-22 07:44:17.296514"], ["product_id", 11]]
Product Create (1.5ms) INSERT INTO "products" ("id", "title", "description", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["id", 11], ["title", "Hand Sanitizer"], ["description", "new sanitizer in town"], ["created_at", "2022-03-22 07:44:17.306941"], ["updated_at", "2022-03-22 07:44:17.306941"]]
TRANSACTION (2.4ms) COMMIT
変更について詳しくは#41487を参照してください。
関連記事
Rails 7: ActiveRecord::Coreのfindがfind_byキャッシュキーを再利用するようになった(翻訳)
概要
元サイトの許諾を得て翻訳・公開いたします。