Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 7: PostgreSQLの外部キー制約にdeferrableを指定可能になった(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

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キャッシュキーを再利用するようになった(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。