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

# お題
Golangで有名なORマッパーライブラリの[Gorm](https://github.com/jinzhu/gorm/)には、定義した構造体に応じたRDBのテーブルを自動生成する機能がある。
このオートマイグレーションをいろいろ試してみる。

# 開発環境
### # 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>
```

## 各試行共通の`main.go`

```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.LogMode(true)
	if err := db.DB().Ping(); err != nil {
		panic(err)
	}
	db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8").AutoMigrate(
		&model.User{},
		&model.UserGroup{},
		// ★↑のオートマイグレーション対象モデルは各試行によって変わる★
	)
}
```

## ■`gorm.Model`を使ったカラム自動生成

以下のように中身は同じ２つの構造体を定義。

```go:[model/user.go]
type User struct {
	gorm.Model
}
```

```go:[model/usergroup.go]
type UserGroup struct {
	gorm.Model
}
```

マイグレート実行。

```
$ go run main.go
```

すると、↓のようなテーブル名は異なるが中身が同じテーブルが生成される。

```
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| testdb             |
+--------------------+
2 rows in set (0.00 sec)

mysql> use testdb;
Database changed
mysql> show tables;
Empty set (0.00 sec)

mysql> 
mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| user_groups      |
| users            |
+------------------+
2 rows in set (0.00 sec)

mysql> desc users;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
| deleted_at | timestamp        | YES  | MUL | NULL    |                |
+------------+------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> desc user_groups;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
| deleted_at | timestamp        | YES  | MUL | NULL    |                |
+------------+------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
```

明示的に定義しなくても、なんかいろいろカラム作ってくれてる。

## ■明示的にカラムの定義を指示

```go:[model/hogefuga.go]
type HogeFuga struct {
	HogeID             int    `gorm:"primary_key"`           // プライマリーキーの指示
	Name               string `gorm:"column:hoge_fuga_name"` // プロパティとは別の名前でカラム定義
	Field              string `gorm:"type:varchar(100)"`     // 型・桁を指示
	UniqueField        string `gorm:"unique"`                // 重複不可を指示
	NotNullField       string `gorm:"not null"`              // Not Nullを指示
	IgnoreField        string `gorm:"-"`                     // カラム化しない指示
	AutoIncField       int    `gorm:"AUTO_INCREMENT"`        // オートインクリメント指示
	IndexField         int    `gorm:"index"`                 // インデックスを貼る
	UniqueIndexField   int    `gorm:"unique_index"`          // ユニークインデックスを貼る
	NamedIndexField    int    `gorm:"index:other_name_idx"`  // インデックスの名前を指示
	NoInstructionField string // 指定なし
}
```

こうなる。

```
mysql> desc hoge_fugas;
+----------------------+--------------+------+-----+---------+----------------+
| Field                | Type         | Null | Key | Default | Extra          |
+----------------------+--------------+------+-----+---------+----------------+
| hoge_id              | int(11)      | NO   | PRI | NULL    | auto_increment |
| hoge_fuga_name       | varchar(255) | YES  |     | NULL    |                |
| field                | varchar(100) | YES  |     | NULL    |                |
| unique_field         | varchar(255) | YES  | UNI | NULL    |                |
| not_null_field       | varchar(255) | NO   |     | NULL    |                |
| auto_inc_field       | int(11)      | YES  |     | NULL    |                |
| index_field          | int(11)      | YES  | MUL | NULL    |                |
| unique_index_field   | int(11)      | YES  | UNI | NULL    |                |
| named_index_field    | int(11)      | YES  | MUL | NULL    |                |
| no_instruction_field | varchar(255) | YES  |     | NULL    |                |
+----------------------+--------------+------+-----+---------+----------------+
10 rows in set (0.01 sec)

mysql> show index from hoge_fugas;
+------------+------------+-----------------------------------+--------------+--------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table      | Non_unique | Key_name                          | Seq_in_index | Column_name        | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+-----------------------------------+--------------+--------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| hoge_fugas |          0 | PRIMARY                           |            1 | hoge_id            | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
| hoge_fugas |          0 | unique_field                      |            1 | unique_field       | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |
| hoge_fugas |          0 | uix_hoge_fugas_unique_index_field |            1 | unique_index_field | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |
| hoge_fugas |          1 | idx_hoge_fugas_index_field        |            1 | index_field        | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |
| hoge_fugas |          1 | other_name_idx                    |            1 | named_index_field  | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |
+------------+------------+-----------------------------------+--------------+--------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
5 rows in set (0.01 sec)
```

ユニークキー制約を確認。

```
mysql> insert into hoge_fugas(not_null_field, unique_field) values('one', '001');
Query OK, 1 row affected (0.01 sec)

mysql> insert into hoge_fugas(not_null_field, unique_field) values('one', '001');
ERROR 1062 (23000): Duplicate entry '001' for key 'unique_field'
```

## ■テーブル名を指示

```go:[model/foo.go]
type Foo struct {
	gorm.Model
}

func (Foo) TableName() string {
	return "foo_alias"
}
```

```
mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| foo_alias        |
+------------------+

mysql> desc foo_alias;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
| deleted_at | timestamp        | YES  | MUL | NULL    |                |
+------------+------------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)
```

## ■初期化時のプロパティに応じてテーブル名を変える

```go:[model/baa.go]
type Baa struct {
	gorm.Model
	Kind int
}

func (b Baa) TableName() string {
	if b.Kind == 1 {
		return "admin_baa"
	} else {
		return "user_baa"
	}
}
```

これを↓のようにオートマイグレーション時にプロパティを指示することでテーブル名を変える。

```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")
　　　〜〜省略〜〜
	db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8").AutoMigrate(
		&model.Baa{},
		&model.Baa{Kind: 1},
	)
}
```

すると、条件に応じたテーブル名のテーブルができている。

```
mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| admin_baa        |
| user_baa         |
+------------------+
```

# まとめ
ひとまず基礎中の基礎のような部分をピックアップ。
次回はリレーション関係にトライしよう。