LoginSignup
29
15

More than 3 years have passed since last update.

[golang]Gormでのモデル定義とオートマイグレーションでできるテーブル定義(リレーション編)

Last updated at Posted at 2018-12-16

お題

前回に引き続き、今回は主にリレーション関係にトライ。
やることはシンプル。↓にあるリレーションのパターンを実際に試して、生成されるテーブル定義を確認する。
http://doc.gorm.io/associations.html

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"

# Golang

$ go version
go version go1.11.2 linux/amd64

バージョンの切り替えはgoenvで行っている。

# Gorm

Version: v1.9.2

実践

各試行共通の準備

RDBはMySQLを対象とする。
ローカルで↓のようなYamlを作っておく。
(dockerとdocker-composeは当然インストール済)

[docker-compose.yml]
version: '3'
services:
  db:
    image: mysql:5.7.24
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: testuser
      MYSQL_PASSWORD: testpass
      MYSQL_DATABASE: testdb

起動。

$ sudo docker-compose up

試しにコンテナ内のDBに接続。

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
831188d7e688        mysql:5.7.24        "docker-entrypoint.s…"   5 minutes ago       Up 5 minutes        0.0.0.0:3306->3306/tcp, 33060/tcp   gorm_db_1_46a022cef614
$
$ sudo docker exec -it gorm_db_1_46a022cef614 bash
root@831188d7e688:/# mysql -utestuser -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.24 MySQL Community Server (GPL)

 〜〜省略〜〜

mysql>

■Belongs To(gorm.Model使用版)

ソース説明

↓に記載のまま構造体を定義。
http://doc.gorm.io/associations.html#belongs-to

[main.go]
// `User` belongs to `Profile`, `ProfileID` is the foreign key
type User struct {
    gorm.Model
    Profile   Profile
    ProfileID int
}

type Profile struct {
    gorm.Model
    Name string
}

両テーブルのマイグレーションと動作確認用にレコード登録。そして、登録内容確認。

[main.go]
func main() {
    db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    db.AutoMigrate(&User{}, &Profile{})

    db.Create(&User{Profile: Profile{Name: "dummy"}})

    profile := Profile{}
    profile.ID = 1
    user := User{Profile: profile}
    user.ID = 1
    user.ProfileID = 1

    db.Model(&user).Related(&profile)
    fmt.Printf("user: {ID: %v, ProfileID: %v, Profile: {ID: %v, Name: %v}}\n", user.ID, user.ProfileID, user.Profile.ID, user.Profile.Name)
}

実行

$ go run main.go 
user: {ID: 1, ProfileID: 1, Profile: {ID: 1, Name: }}

う〜ん、参照サイトにはdb.Model(&user).Related(&profile)の説明として「SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID」とあったのだけど、ID=1で登録されたレコードのName="dummy"が入っていない?

テーブル定義確認

mysql> show create table users;
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                                                                               |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `profile_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_users_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> 
mysql> show create table profiles;
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                                                                                    |
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| profiles | CREATE TABLE `profiles` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_profiles_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 |
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

自動で外部キー貼ってくれるのかと勘違いした。貼らないんだね。

登録レコード確認

mysql> select * from users;
+----+---------------------+---------------------+------------+------------+
| id | created_at          | updated_at          | deleted_at | profile_id |
+----+---------------------+---------------------+------------+------------+
|  1 | 2018-12-16 01:32:26 | 2018-12-16 01:32:26 | NULL       |          1 |
+----+---------------------+---------------------+------------+------------+
1 row in set (0.00 sec)

mysql> select * from profiles;
+----+---------------------+---------------------+------------+-------+
| id | created_at          | updated_at          | deleted_at | name  |
+----+---------------------+---------------------+------------+-------+
|  1 | 2018-12-16 01:32:26 | 2018-12-16 01:32:26 | NULL       | dummy |
+----+---------------------+---------------------+------------+-------+
1 row in set (0.00 sec)

db.Create(&User{Profile: Profile{Name: "dummy"}})によって↑の2テーブルにレコードが入ったので、そっちの方が驚いた。
構造体「User」及び「Profile」は共にgorm.Modelを持たせているのでidという名のPKは自動生成される仕組みになっている。
なので、それぞれのテーブルに自動採番されたid値を持つレコードができることは納得。
構造体「User」には構造体「Profile」の他にProfileIDという名のプロパティも持たせている。
どうやら、これによって、usersテーブルのprofile_idカラムがprofilesテーブルのidカラムの外部キーであることを意味するようになるらしい。(結果から、そのように読み取れるというだけでソースの裏付けは取れていない・・・)
命名ルールベースの仕掛けのようだけど、ちょっと難しいね。

■Belongs To(独自モデル使用版)

ソース説明

↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#belongs-to

[main.go]
type User struct {
    ID        string  `gorm:primary_key`
    Profile   Profile `gorm:"foreignkey:ProfileID;association_foreignkey:Refer"`
    ProfileID string
}

type Profile struct {
    ID    string `gorm:primary_key`
    Refer string
    Name  string
}

両テーブルのマイグレーションと動作確認用にレコード登録。そして、登録内容確認。

[main.go]
func main() {
    db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    db.AutoMigrate(&User{}, &Profile{})

    db.Create(
        &User{
            ID: "1",
            Profile: Profile{
                ID:    "1",
                Refer: "1",
                Name:  "dummy",
            },
            ProfileID: "1",
        },
    )

    profile := Profile{}
    profile.ID = "1"
    user := User{Profile: profile}
    user.ID = "1"
    user.ProfileID = "1"

    db.Model(&user).Related(&profile)
    fmt.Printf("user: {ID: %v, ProfileID: %v, Profile: {ID: %v, Name: %v}}\n", user.ID, user.ProfileID, user.Profile.ID, user.Profile.Name)
}

実行

$ go run main.go 
user: {ID: 1, ProfileID: 1, Profile: {ID: 1, Name: }}

やはり、db.Model(&user).Related(&profile)ではName="dummy"は取れない?

テーブル定義確認

mysql> show create table users;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                               |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` varchar(255) NOT NULL,
  `profile_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> 
mysql> show create table profiles;
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                 |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| profiles | CREATE TABLE `profiles` (
  `id` varchar(255) NOT NULL,
  `refer` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

やはり、外部キーは貼られない。

登録レコード確認

mysql> select * from users;
+----+------------+
| id | profile_id |
+----+------------+
| 1  | 1          |
+----+------------+
1 row in set (0.00 sec)

mysql> select * from profiles;
+----+-------+-------+
| id | refer | name  |
+----+-------+-------+
| 1  | 1     | dummy |
+----+-------+-------+
1 row in set (0.00 sec)

mysql> select u.*, p.* from users u, profiles p where u.profile_id = p.id;
+----+------------+----+-------+-------+
| id | profile_id | id | refer | name  |
+----+------------+----+-------+-------+
| 1  | 1          | 1  | 1     | dummy |
+----+------------+----+-------+-------+
1 row in set (0.00 sec)

SQL文でJOINすれば両テーブルの全カラムが取れるんだけど。

複数テーブルをJOINしてレコード取得

上述のusersテーブルとprofilesテーブルから内部結合して以下のカラムを取得する。
- usersid
- usersprofile_id(=profilesid
- profilesname

内部結合用のメソッドがGormに用意してあるものの、タイプセーフな使い方ができない様子。
まず、取得対象のカラムを持つ構造体を定義(まあ、これは必須ではなさそうだけど)して、

type Result struct {
    ID        string
    ProfileID string
    Name      string
}

こんなふうにベースのテーブル、結合先テーブル(及び結合カラム)、取得カラムを指示してみると、

    r := &Result{}
    db.Table("users").
        Select("users.id, users.profile_id, profiles.name").
        Joins("inner join profiles on profiles.id = users.profile_id").
        First(r)
    fmt.Printf("%#v\n", r)

このように出力される。

&main.Result{ID:"1", ProfileID:"1", Name:"dummy"}

タイプセーフなままJOINする方法あるのかな・・・?

■Has One(独自モデル使用版)

正直なところ、Belongs Toとの違いがわからない・・・。

ソース説明

↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#has-one

type User struct {
    ID           string `gorm:primary_key`
    CreditCardID string
    CreditCard   CreditCard `gorm:"foreignkey:ID;association_foreignkey:CreditCardID"`
}

type CreditCard struct {
    ID     string `gorm:primary_key`
    Number string
}

構造体「User」はCreditCardIDを外部キーに構造体「CreditCard」を持つ関係。
オートマイグレーションと2テーブルへのレコード登録ロジックは下記。

    db.AutoMigrate(&User{}, &CreditCard{})

    db.Create(
        &User{
            ID:           "u1",
            CreditCardID: "c1",
            CreditCard: CreditCard{
                ID:     "c1",
                Number: "xxxx-xxxx-xxxx-xxxx",
            },
        },
    )

実行後のテーブル定義確認

このように、やはり、タグに「foreignkey:ID」と付けようと、オートマイグレーションで外部キーは貼られない。

mysql> show create table users;
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                   |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` varchar(255) NOT NULL,
  `credit_card_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> show create table credit_cards;
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table        | Create Table                                                                                                                                                  |
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| credit_cards | CREATE TABLE `credit_cards` (
  `id` varchar(255) NOT NULL,
  `number` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

実行後の登録レコード確認

mysql> select * from users;
+----+----------------+
| id | credit_card_id |
+----+----------------+
| u1 | c1             |
+----+----------------+
1 row in set (0.01 sec)

mysql> select * from credit_cards;
+----+---------------------+
| id | number              |
+----+---------------------+
| c1 | xxxx-xxxx-xxxx-xxxx |
+----+---------------------+
1 row in set (0.01 sec)

プログラムでの登録レコード取得確認

Select文の受け取り口として、

type Result struct {
    ID           string
    CreditCardID string
    Number       string
}

を定義して、

    r := &Result{}
    db.Table("users").
        Select("users.id, users.credit_card_id, credit_cards.number").
        Joins("inner join credit_cards on credit_cards.id = users.credit_card_id").
        First(r)
    fmt.Printf("%#v\n", r)

これで出力。すると、

$ go run main.go 

&main.Result{ID:"u1", CreditCardID:"c1", Number:"xxxx-xxxx-xxxx-xxxx"}

users」テーブルと「credit_cards」テーブルを内部結合してのレコード取得結果が取れる。

■Has Many

ソース説明

↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#has-many

type Organization struct {
    ID    string `gorm:primary_key`
    Name  string
    Users []User `gorm:"foreignkey:OrganizationID"`
}

type User struct {
    ID             string `gorm:primary_key`
    Name           string
    OrganizationID string
}

「組織」は複数の「ユーザ」を持つ。「ユーザ」は1つの「組織」に属するという関係。
で、↓のようにして2つの「組織」レコード(及び所属する「ユーザ」レコード)を登録する。
(一応、トランザクション貼る。)

func main() {
    db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    defer db.Close()
    db.LogMode(true)

    db.AutoMigrate(&User{}, &Organization{})

    ts := db.Begin()
    defer ts.Commit()

    ts.Create(
        &Organization{
            ID:   "o01",
            Name: "SosikiA",
            Users: []User{
                {ID: "u01", Name: "Sato", OrganizationID: "o01"},
                {ID: "u02", Name: "Takahashi", OrganizationID: "o01"},
            },
        },
    )

    ts.Create(
        &Organization{
            ID:   "o02",
            Name: "SosikiB",
            Users: []User{
                {ID: "u03", Name: "Kato", OrganizationID: "o02"},
                {ID: "u04", Name: "Ishida", OrganizationID: "o02"},
                {ID: "u05", Name: "Niwa", OrganizationID: "o02"},
            },
        },
    )

    if err := ts.Error; err != nil {
        ts.Rollback()
    }
}

実行後のテーブル定義確認

やはり、外部キーは貼られない。

mysql> show create table organizations;
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table         | Create Table                                                                                                                                                 |
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
| organizations | CREATE TABLE `organizations` (
  `id` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> show create table users;
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                        |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `organization_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

実行後の登録レコード確認

mysql> select * from organizations;
+-----+---------+
| id  | name    |
+-----+---------+
| o01 | SosikiA |
| o02 | SosikiB |
+-----+---------+
2 rows in set (0.00 sec)

mysql> select * from users;
+-----+-----------+-----------------+
| id  | name      | organization_id |
+-----+-----------+-----------------+
| u01 | Sato      | o01             |
| u02 | Takahashi | o01             |
| u03 | Kato      | o02             |
| u04 | Ishida    | o02             |
| u05 | Niwa      | o02             |
+-----+-----------+-----------------+
5 rows in set (0.01 sec)

プログラムでの登録レコード取得確認

Select文の受け取り口として、

type ResOrgUser struct {
    OrganizationID   string
    OrganizationName string
    UserID           string
    UserName         string
}

を定義して、↓のようにして実行。

    var r []ResOrgUser
    db.Table("organizations").
        Select(
            "organizations.id AS organization_id, " +
                "organizations.name AS organization_name, " +
                "users.id AS user_id, " +
                "users.name AS user_name").
        Joins("inner join users on users.organization_id = organizations.id").
        Find(&r)
    for idx, res := range r {
        fmt.Printf("[%d]%#v\n", idx, res)
    }

↓の塩梅でちゃんとレコード取得できることが確認できた。

$ go run main.go 

[0]main.ResOrgUser{OrganizationID:"o01", OrganizationName:"SosikiA", UserID:"u01", UserName:"Sato"}
[1]main.ResOrgUser{OrganizationID:"o01", OrganizationName:"SosikiA", UserID:"u02", UserName:"Takahashi"}
[2]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u03", UserName:"Kato"}
[3]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u04", UserName:"Ishida"}
[4]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u05", UserName:"Niwa"}

■Many To Many

紐付けテーブルを自動生成する仕掛け。
タグに仕込んだキーワード「many2many」により、その先で指示した名前を持つ紐付けテーブルも自動生成する。

ソース説明

↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#many-to-many

type Organization struct {
    ID    string `gorm:primary_key`
    Name  string
    Users []User `gorm:"many2many:organizations_users"`
}

type User struct {
    ID            string `gorm:primary_key`
    Name          string
    Organizations []Organization `gorm:"many2many:organizations_users"`
}

↑こんなふうに定義してオートマイグレーションすると...

実行後のテーブル定義確認

mysql> show tables;
+---------------------+
| Tables_in_testdb    |
+---------------------+
| organizations       |
| organizations_users |
| users               |
+---------------------+
3 rows in set (0.00 sec)

構造体とマッピングされる「organizations」や「users」とは別にタグで名前を指定した「organizations_users」も生成されている。
そして、定義は。。。

mysql> desc organizations;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | varchar(255) | NO   | PRI | NULL    |       |
| name  | varchar(255) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> desc users;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | varchar(255) | NO   | PRI | NULL    |       |
| name  | varchar(255) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> desc organizations_users;
+-----------------+--------------+------+-----+---------+-------+
| Field           | Type         | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| user_id         | varchar(255) | NO   | PRI | NULL    |       |
| organization_id | varchar(255) | NO   | PRI | NULL    |       |
+-----------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

なるほど。これは、いいかも。

まとめ

個人的には、タグでforeignkeyと貼ってもオートマイグレーションでは実テーブル定義に反映されない点はイマイチ。
あと、INSERT時は構造体間の関係性によって複数テーブルの登録を一挙に行えるのはよいけど、複数テーブルからのレコード取得時にタイプセーフな取り方ができない(自分が知らないだけ?)のは難点。
これは自分がこのORMを使いこなせていない(そもそもORMより生SQL使いたい派)のが原因なんだけど、難しいね、ORM。

29
15
2

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
29
15