はじめに
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側の設定として入っていないとちょっと気持ち悪い感じがします。
-
設定を変更するために既存のmigrationファイルを編集していますが、あんまりよろしくない方法です。基本は新しくmigrationファイルを作成して、差分を追記していく形になるかと思います。この辺はmigrationファイルを使ってrollbackできるようにしておくか、もしくはrollbackしない前提にするかでも異なりますが。 ↩