31
26

More than 5 years have passed since last update.

Railsで外部キー制約の参照制約の書き方

Last updated at Posted at 2018-01-11

はじめに

 Rails(というかActive Record)で「TableAを削除時、TableAを参照しているTableBも削除する」というように実装しようと思った時、DBの外部キー参照制約を使おうと思いました。ただ、「Rails 外部キー制約」などで検索しても思いの外、目的の情報が引っかからなかったので、ここでまとめようと思います。

Migrationファイルを使用して、DBに設定する方法

下記のURLを参照。

migrationファイルを作成します。

$ bundle exec rails generate migration CreateOrderModel

Running via Spring preloader in process 30867
      invoke  active_record
      create    db/migrate/20171218102816_create_order_model.rb

作成された db/migrate/20171218102816_create_order_model.rbを編集して
参照先テーブル、参照元テーブルを作ります。

$ vi db/migrate/20171218102816_create_order_model.rb

class CreateOrderModel < ActiveRecord::Migration[5.1]
  def change
    create_table :orders do |t|
      t.string :address
    end

    create_table :order_details do |t|
      t.references :order, index: true
      t.string :item_name
      t.integer :item_num
    end

    add_foreign_key :order_details, :orders, on_delete: :cascade
  end
end

on_deleteだと削除時がトリガーとなります。cascadeで参照元が削除された場合、参照しているテーブルを削除するようになります。

  • on_update
  • on_delete

といったトリガーに

  • :nullify
  • :cascade
  • :restrict

という種別。意味はもちろんSQLの外部キー参照制約と同じです。

migrateでテーブルを作成します。

$ bundle exec rake db:migrate
== 20171218102816 CreateOrderModel: migrating =================================
-- create_table(:orders)
   -> 0.0098s
-- create_table(:order_details)
   -> 0.0110s
-- add_foreign_key(:order_details, :orders, {:on_delete=>:cascade})
   -> 0.0191s
== 20171218102816 CreateOrderModel: migrated (0.0404s) ========================

さらにモデルも作成します。


$ vi app/models/order.rb

class Order < ApplicationRecord
end

$ vi app/models/orderdetail.rb

class OrderDetail < ApplicationRecord
end

これで完了。 rails consoleでテストします。

$ bundle exec rails c
Running via Spring preloader in process 28634
Loading development environment (Rails 5.1.4)

# Orderを作成
irb(main):001:0> order = Order.create(address: '東京都目黒区')
   (0.3ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `orders` (`address`) VALUES ('東京都目黒区')
   (9.3ms)  COMMIT
=> #<Order id: 1, address: "東京都目黒区">

# OrderDetailを作成、Orderを参照
irb(main):002:0> detail = OrderDetail.create(order_id: order.id, item_name: 'タブレットPC', item_num: 1)
   (0.1ms)  BEGIN
  SQL (0.2ms)  INSERT INTO `order_details` (`order_id`, `item_name`, `item_num`) VALUES (1, 'タブレットPC', 1)
   (0.3ms)  COMMIT
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# OrderDetailを作成されているのを確認
irb(main):003:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. 'id' = 1 LIMIT 1
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# Orderを削除
irb(main):004:0> order.delete
  SQL (1.2ms)  DELETE FROM `orders` WHERE `orders`. 'id' = 1
=> #<Order id: 1, address: "東京都目黒区">

# OrderDetailが削除されているのを確認
irb(main):005:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. `id` = 1 LIMIT 1
ActiveRecord::RecordNotFound: Couldn t find OrderDetail with 'id'=1

参照先のテーブルが削除されたことにより、参照元テーブルも削除されてました。
テーブルの設定を見てもらえれば分かりますが、order_detailsテーブルに
ON DELETE CASCADEの設定が入っています。

ActiveRecordのみの設定でカバーする方法

下記のURLを参照。

ActiveRecordのdependency: deleteなど使っても同様のことができるようです。
dependency: deleteを試すために、一度RollbackしてDB設定を初期化します。

$ bundle exec rake db:rollback
== 20171218102816 CreateOrderModel: reverting =================================
-- remove_foreign_key(:order_details, :orders)
   -> 0.0241s
-- drop_table(:order_details)
   -> 0.0020s
-- drop_table(:orders)
   -> 0.0033s
== 20171218102816 CreateOrderModel: reverted (0.0334s) ========================

$ bundle exec rake db:migrate:status
database: test_project_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20171218102816  Create order model

Rollback実施後、add_foreign_keyの設定を無効化します1

$ vi db/migrate/20171218102816_create_order_model.rb

class CreateOrderModel < ActiveRecord::Migration[5.1]
  def change
    create_table :orders do |t|
      t.string :address
    end

    create_table :order_details do |t|
      t.references :order, index: true
      t.string :item_name
      t.integer :item_num
    end

    # add_foreign_key :order_details, :orders, on_delete: :cascade
  end
end

次にモデルの内容を変更します。


$ vi app/models/order.rb

class Order < ApplicationRecord
  has_many :order_details, :dependent => :destroy
end

$ vi app/models/orderdetail.rb

class OrderDetail < ApplicationRecord
  belongs_to :order
end

migrateで再度、テーブルを作成します。
その後、rails consoleでテストします。

$ bundle exec rake db:migrate
$ bundle exec rails c
Running via Spring preloader in process 28634
Loading development environment (Rails 5.1.4)

# Orderを作成
irb(main):001:0> order = Order.create(address: '東京都目黒区')
   (0.3ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `orders` (`address`) VALUES ('東京都目黒区')
   (9.3ms)  COMMIT
=> #<Order id: 1, address: "東京都目黒区">

# OrderDetailを作成、Orderを参照
irb(main):002:0> detail = OrderDetail.create(order_id: order.id, item_name: 'タブレットPC', item_num: 1)
   (0.1ms)  BEGIN
  SQL (0.2ms)  INSERT INTO `order_details` (`order_id`, `item_name`, `item_num`) VALUES (1, 'タブレットPC', 1)
   (0.3ms)  COMMIT
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# OrderDetailを作成されているのを確認
irb(main):003:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. 'id' = 1 LIMIT 1
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# Orderを削除
irb(main):004:0> order.delete
  SQL (1.4ms)  DELETE FROM `orders` WHERE `orders`. 'id' = 1

# OrderDetailが削除されているのを確認
irb(main):005:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. 'id' = 1 LIMIT 1
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

あれ? 普通に残ってる。
もしかして、OrderDetailを作成する前の古いインスタンスを使って削除した影響?

再度、DBをRollback後、migrateして再チャレンジします。

$ bundle exec rake db:rollback
$ bundle exec rake db:migrate

$ bundle exec rails c
Running via Spring preloader in process 28634
Loading development environment (Rails 5.1.4)

# Orderを作成
irb(main):001:0> order = Order.create(address: '東京都目黒区')
   (0.3ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `orders` (`address`) VALUES ('東京都目黒区')
   (9.3ms)  COMMIT
=> #<Order id: 1, address: "東京都目黒区">

# OrderDetailを作成、Orderを参照
irb(main):002:0> detail = OrderDetail.create(order_id: order.id, item_name: 'タブレットPC', item_num: 1)
   (0.1ms)  BEGIN
  SQL (0.2ms)  INSERT INTO `order_details` (`order_id`, `item_name`, `item_num`) VALUES (1, 'タブレットPC', 1)
   (0.3ms)  COMMIT
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# OrderDetailを作成されているのを確認
irb(main):003:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. 'id' = 1 LIMIT 1
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>

# Orderを確認、その後古いインスタンスが格納された変数orderを使用せずに削除。
irb(main):004:0> Order.find(1)
  Order Load (0.4ms)  SELECT  `orders`.* FROM `orders` WHERE `orders`. 'id' = 1 LIMIT 1
=> #<Order id: 1, address: "東京都目黒区">

irb(main):005:0> Order.find(1).destroy
  Order Load (0.4ms)  SELECT  `orders`.* FROM `orders` WHERE `orders`.'id' = 1 LIMIT 1
   (0.1ms)  BEGIN
  OrderDetail Load (0.4ms)  SELECT `order_details`.* FROM `order_details` WHERE `order_details`. 'order_id' = 1
  SQL (0.2ms)  DELETE FROM `order_details` WHERE `order_details`. 'id' = 1
  SQL (0.3ms)  DELETE FROM `orders` WHERE `orders`. 'id' = 1
   (16.9ms)  COMMIT
=> #<Order id: 1, address: "東京都目黒区">

# OrderDetailが削除されているのを確認
irb(main):006:0> OrderDetail.find(1)
  OrderDetail Load (0.4ms)  SELECT  `order_details`.* FROM `order_details` WHERE `order_details`. 'id' = 1 LIMIT 1
ActiveRecord::RecordNotFound: Couldn t find OrderDetail with 'id'=1

おお、無事削除できました。

最後に

 DBのテーブルにしっかりと設定したいので、今回はMigrationファイルに外部キー参照制約を書くようにしました。ActiveRecordがやってくれるのはいいですけど、やっぱりDB側の設定として入っていないとちょっと気持ち悪い感じがします。


  1. 設定を変更するために既存のmigrationファイルを編集していますが、あんまりよろしくない方法です。基本は新しくmigrationファイルを作成して、差分を追記していく形になるかと思います。この辺はmigrationファイルを使ってrollbackできるようにしておくか、もしくはrollbackしない前提にするかでも異なりますが。 

31
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
26