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

# お題
[前回](https://qiita.com/sky0621/items/8475c191faacd1901e2f)に引き続き、今回は主にリレーション関係にトライ。
やることはシンプル。↓にあるリレーションのパターンを実際に試して、生成されるテーブル定義を確認する。
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](https://github.com/syndbg/goenv)で行っている。

### # Gorm 
```
Version: v1.9.2
```

# 実践

## 各試行共通の準備

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

```yaml:[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

```go:[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
}
```

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

```go:[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: }}
```

う〜ん、[参照サイト](http://doc.gorm.io/associations.html#belongs-to)には`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"}})`によって↑の２テーブルにレコードが入ったので、そっちの方が驚いた。
構造体「`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

```go:[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
}
```

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

```go:[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`テーブルから内部結合して以下のカラムを取得する。
- `users`の`id`
- `users`の`profile_id`（＝`profiles`の`id`）
- `profiles`の`name`

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

```go
type Result struct {
	ID        string
	ProfileID string
	Name      string
}
```

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

```go
	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

```go
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`」を持つ関係。
オートマイグレーションと２テーブルへのレコード登録ロジックは下記。

```go
	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文の受け取り口として、

```go
type Result struct {
	ID           string
	CreditCardID string
	Number       string
}
```

を定義して、

```go
	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

```go
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
}
```

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

```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.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文の受け取り口として、

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

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

```go
	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

```go
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。