Gormにおける多層外部キーの定義の仕方とコードの書き方
最近バイトの方でgormの振る舞いとデータベースのテーブル構造のことでハマったのでそれについてメモっておきます
テーブル構造とgoのモデル
最初にあったテーブル構造は上の図のような感じ。
今回は例として地域、地域に存在する書店たち、書店の持つ本をマッピングしてみました。
地域と書店の関係はhas many、書店と本もhas manyとなっています。
これをgoのコードにmodelとして書き出すと以下のような感じになりました。
package main import "time" type Region struct { ID int Shops []Shop `gorm:"foreignkey:ID"` CreatedAt time.Time UpdatedAt time.Time } type Shop struct { ID int Name string Books []Book `gorm:"foreignkey:ShopID"` CreatedAt time.Time UpdatedAt time.Time } type Book struct { ID int ShopID int Name string Price int CreatedAt time.Time UpdatedAt time.Time }
テーブル作成のsqlは次のようになります
CREATE TABLE IF NOT EXISTS region ( id serial NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT region_id PRIMARY KEY ( id ) ); CREATE TABLE IF NOT EXISTS shop ( id serial NOT NULL, name varchar(255) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT shop_id PRIMARY KEY ( id ), CONSTRAINT shop_id_region_id_foreign FOREIGN KEY ( id ) REFERENCES region( id ) ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE IF NOT EXISTS book ( id serial NOT NULL, shop_id bigint(20) unsigned NOT NULL, name varchar(255) NOT NULL, price int NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT book_id PRIMARY KEY ( id ), CONSTRAINT book_shop_id_shop_id_foreign FOREIGN KEY ( shop_id ) REFERENCES shop( id ) ON UPDATE NO ACTION ON DELETE NO ACTION );
shopは自身のIDは外部キーでregionのidと紐づけています bookはshop idを外部キーでshopのidと紐づけています
gormでinsertする
これらのデータ構造はgormにおいて次のように一括でcreateすることができます。
r := Region{ Shops: []Shop{ Shop{ Name: "shop1", Books: []Book{ Book{ Name: "book1", Price: 100, }, Book{ Name: "book2", Price: 200, }, }, }, }, } if err := mysql.Create(&r).Error; err != nil { log.Fatal(err) }
gormがタグに紐づけた外部キーの情報からよしなに親をinsertした後、確定したIDの情報を用いて, 子のテーブルにまでinsertしてくれるのです。
しかし、このデータ構造の場合次のようなエラーが起こります。
/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] [3.76ms] INSERT INTO `region` (`created_at`,`updated_at`) VALUES ('2018-08-25 13:27:44','2018-08-25 13:27:44') [1 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] [10.12ms] UPDATE `shop` SET `name` = 'shop1', `created_at` = '0001-01-01 00:00:00', `updated_at` = '2018-08-25 13:27:44' WHERE `shop`.`id` = '2' [0 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] [5.02ms] INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('2','book1','100','2018-08-25 13:27:44','2018-08-25 13:27:44') [0 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:27:44] Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) 2018/08/25 13:27:44 Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)
エラーの内容はupdateが走り, それはconstraintに違反しているというものでした(多分)
これの原因はShopの主キーであり、外部キーでもあるIDとRegionの主キーを紐づけていることに起因すると思われます。
正しいテーブル構造
なので、ちゃんとしたテーブル構造は次のようになります
sqlはこんな感じ
CREATE TABLE IF NOT EXISTS region ( id serial NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT region_id PRIMARY KEY ( id ) ); CREATE TABLE IF NOT EXISTS shop ( id serial NOT NULL, region_id bigint(20) unsigned NOT NULL, name varchar(255) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT shop_id PRIMARY KEY ( id ), CONSTRAINT shop_region_id_region_id_foreign FOREIGN KEY ( region_id ) REFERENCES region( id ) ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE IF NOT EXISTS book ( id serial NOT NULL, shop_id bigint(20) unsigned NOT NULL, name varchar(255) NOT NULL, price int NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , CONSTRAINT book_id PRIMARY KEY ( id ), CONSTRAINT book_shop_id_shop_id_foreign FOREIGN KEY ( shop_id ) REFERENCES shop( id ) ON UPDATE NO ACTION ON DELETE NO ACTION );
goのmodelのコードはこんな感じ
type Region struct { ID int Shops []Shop `gorm:"foreginkey:RegionID"` CreatedAt time.Time UpdatedAt time.Time } type Shop struct { ID int RegionID int Name string Books []Book `gorm:"foreignkey:ShopID"` CreatedAt time.Time UpdatedAt time.Time } type Book struct { ID int ShopID int Name string Price int CreatedAt time.Time UpdatedAt time.Time }
ShopにRegionIDという外部キー用のカラムを追加しています。
これで先程のcreateを実行してみると
(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:18:15] [2.14ms] INSERT INTO `region` (`created_at`,`updated_at`) VALUES ('2018-08-25 13:18:15','2018-08-25 13:18:15') [1 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:18:15] [6.41ms] INSERT INTO `shop` (`region_id`,`name`,`created_at`,`updated_at`) VALUES ('2','shop1','2018-08-25 13:18:15','2018-08-25 13:18:15') [1 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:18:15] [1.57ms] INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('1','book1','100','2018-08-25 13:18:15','2018-08-25 13:18:15') [1 rows affected or returned ] (/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32) [2018-08-25 13:18:15] [3.31ms] INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('1','book2','200','2018-08-25 13:18:15','2018-08-25 13:18:15') [1 rows affected or returned ] main.Region{ ID: 2, Shops: []main.Shop{ main.Shop{ ID: 1, RegionID: 2, Name: "shop1", Books: []main.Book{ main.Book{ ID: 1, ShopID: 1, Name: "book1", Price: 100, CreatedAt: 2018-08-25 13:18:15 Local, UpdatedAt: 2018-08-25 13:18:15 Local, }, main.Book{ ID: 2, ShopID: 1, Name: "book2", Price: 200, CreatedAt: 2018-08-25 13:18:15 Local, UpdatedAt: 2018-08-25 13:18:15 Local, }, }, CreatedAt: 2018-08-25 13:18:15 Local, UpdatedAt: 2018-08-25 13:18:15 Local, }, }, CreatedAt: 2018-08-25 13:18:15 Local, UpdatedAt: 2018-08-25 13:18:15 Local, }
ちゃんと一括でinsertされているのがわかりました!
まとめ
そもそも最初失敗のケースでハマっていたのは親子関係のみの場合はうまく行っていたからです。親子孫のように増えると先程のようなエラーが出て四苦八苦していました。
ちなみに例に上げた親子の関係は1to1でもエラーは起きます。
テーブルの主キーと他のテーブルの主キーを外部キーとして紐付けるのは割とやりがちなケースな気がしますが、しっかり別カラムで紐づけた方が安全な気はします。
今回のサンプルコードは以下に載せてあります