SWEet

A Software Engineer Is Eating Technologies

Gormにおける多層外部キーの定義の仕方とコードの書き方

最近バイトの方でgormの振る舞いとデータベースのテーブル構造のことでハマったのでそれについてメモっておきます

テーブル構造とgoのモデル

f:id:kk_river108:20180825134759p:plain

最初にあったテーブル構造は上の図のような感じ。

今回は例として地域、地域に存在する書店たち、書店の持つ本をマッピングしてみました。

地域と書店の関係は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の主キーを紐づけていることに起因すると思われます。

正しいテーブル構造

なので、ちゃんとしたテーブル構造は次のようになります

f:id:kk_river108:20180825140200p:plain

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でもエラーは起きます。

テーブルの主キーと他のテーブルの主キーを外部キーとして紐付けるのは割とやりがちなケースな気がしますが、しっかり別カラムで紐づけた方が安全な気はします。

今回のサンプルコードは以下に載せてあります

github.com