はじめに

 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の設定が入っています。

Gemを使用して、DBに設定する方法

下記のURLを参照。

foreignerというGemを使用してDBのテーブルに外部キー参照制約をつけることが可能です。こちらのGemを使用すると生のSQL文を発行することもできそうです。

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を削除
=> #<OrderDetail id: 1, order_id: 1, item_name: "タブレットPC", item_num: 1>
irb(main):004:0> order.delete
  SQL (1.4ms)  DELETE FROM `orders` WHERE `orders`. `id` = 1

# OrderDetailが削除されているのを確認
=> #<Order id: 1, address: "東京都目黒区">
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しない前提にするかでも異なりますが。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.