Gormを使ってみた（ORMライブラリでMySQL接続）

# 概要

- GoでMySQL接続をしようと思っていた時に、「gorm」というORMライブラリを見つけたので使ってみた
- [公式のドキュメント](https://gorm.io/ja_JP/docs/)を参考に実装時に必要になった知識をまとめていきます

# 準備

gormをインストールします
またMySQLと接続するためのDriverもインストールします

```bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
```

# DB接続

- `gorm.Open`を使用します。
- パスワードのみ環境変数から取得するようにしてみました。

```go
import (
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

USER := "gorm"
// PASS := os.Getenv("MYSQL_PASSWORD")
PASS := "gorm"
HOST := "localhost"
PORT := "3306"
DBNAME := "gorm"

DSN := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", USER, PASS, HOST, PORT, DBNAME)
connection, err := gorm.Open("mysql", DSN)

if err != nil {
	panic(err.Error())
}
```

以降、DB操作時にここで作成した`connection`を使用します

# CRUD

## 準備

まずModelとなる構造体から作成します。

- 主キーとなるフィールドが必須です。
- 主キーとなるフィールドには``gorm:"primaryKey"``というタグを付与します
    - デフォルトではIDが主キーとして認識されています
    - なので以下の構造体だとタグはなくても同じです💦
- フィールドはキャメルケース（CamelCase）で定義します

```go
type User struct {
	Id   int `gorm:"primaryKey"`
	Name string
}
```

次にテーブルを作成します。

- テーブル名はモデルの複数形にします
- 主キーの設定が必須です
    - 主キーをベースにUpdate,Deleteが実行されるため、主キーがないと全件操作されてしまいます
- 構造体のフィールドをスネークケース（snake_case）に変換したもので定義します

```sql
create table users (
  id   int primary key,
  name varchar(10)
);
```

## Create (insert)

- `Create`メソッドを使用します
- 引数にはModelのポインタを渡します

```
user := User{Id: 1, Name: "User-1"}
connection.Create(&user)
```

```sql
mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | User-1 |
+----+--------+
```

### 一意制約エラーの場合

Insert時に気にすることの代表格として一意制約エラーがありますが、
SQLでエラーとなった場合には以下のようにErrorを確認できます
※一意制約エラー以外でも同様です。

```go
user := User{Id: 1, Name: "User-1"}
result := connection.Create(&user1)
fmt.Println(result.Error)
// Error 1062: Duplicate entry '1' for key 'PRIMARY'
```

### auto_incrementを利用している場合

Idにauto_incrementを使用している場合、Idを空にしておくことで機能を使用することができます。
またgormの機能により、採番された番号がオートフィルされます！

```go
user := User{Name: "User-1"}
fmt.Println(user)  // {0 User-1}
connection.Create(&user)
fmt.Println(user)  // {1 User-1}
```

## Read (select)

USERSテーブルに以下の４件のレコードがある状態で実行していきます。

```
mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | User-4 |
|  2 | User-3 |
|  3 | User-2 |
|  4 | User-1 |
+----+--------+
```

### 用意されているメソッド

基本的にメソッドチェーンで記述していきます。
以下のようなメソッドが用意されています。

|メソッド | サンプル |  SQL展開イメージ|
|---|---|---|
| Find   | Find(&users) | ※リストを取得するための終端操作
| First  | First(&user) | order by id limit 1<br>※単一行を取得するための終端操作
| last   | Last(&user) | order by id desc limit 1<br>※単一行を取得するための終端操作
| Take   | Take(&user)  | limit 1<br>※単一行を取得するための終端操作
| Select | Select("id", "name") | select id, name
| Distinct | Distinct("name") | select distinct name
| Joins  | Joins("left join users on hoge.id = users.id") | left join users on hoge.id = users.id
| Table  | Table("users") | from users
| Where  | Where("id = ?", 1) | where id = 1 
| Or     | Or("name = ?", "User-1") | or name = 'User-1'
| Not    | Not("id = ?", 1) | where not id = 1
| Order  | Order(id desc) | order by id desc
| Limit  | Limit(5) | limit 5
| Offset | Offset(2) | offset 2
| Group  | Group("name") | group by name
| Having | Having("count >= ?", 5) | having count >= 5
| Raw    | Raw("select * from users") | ※SQLを直接指定する場合に使用
| Scan   | Scan(&users) | ※Raw使用時等にレコードを格納する構造体を指定するために使用

以下これらのメソッドを使用して実際にレコードを取得するサンプルです。

### 全件取得

```go
users := []User{}
connection.Find(&users)
// select * from users
fmt.Println(users)
// [{1 User-4} {2 User-3} {3 User-2} {4 User-1}]
```

### 主キー検索

`Find`や`First`の第２引数に検索条件を渡すせばOK！

```go:一致
user := User{}
connection.First(&user, 1)
// select * from users where id = 1 limit 1
fmt.Println(user)
// {1 User-4}
```

```go:in句
users := []User{}
connection.Find(&users, []int{1, 2})
// select * from users where id in (1, 2)
fmt.Println(users)
// [{1 User-4} {2 User-3}]
```

> 補足：`First`はlimit 1が付与されます。

### 条件指定

`Where`や`Or`メソッドを使うことでクエリを表現できます。
それぞれ

- 第一引数：検索条件
- 第二引数以降：バインドする値

というように指定します。

```go
user := User{}
users := []User{}

connection.Where("id = ?", 1).First(&user)
// select * from users where id = 1
fmt.Println(user)
// {1 User-4}

connection.Where("id IN ?", []int{1, 2}).Find(&users)
// select * from users where id in (1, 2)
fmt.Println(users)
// [{1 User-4} {2 User-3}]

connection.Where("id = ?", 1).Or("name = ?", "User-1").Find(&users)
// 同じ：connection.Where("id = ? or name = ?", 1, "User-1").Find(&users)
// select * from users where id = 1 or name = 'User-1'
fmt.Println(users)
// [{1 User-4} {4 User-1}]
```

### 構造体を用いて検索

構造体に検索項目を埋めてWhereに指定することでも条件指定の検索ができます！
ただし、`0`, `''`, `false`は値が設定されていないという判定がされるため、使用できないようです

```go
users := []User{}
user := User{Name: "User-1"}
connection.Where(&user).Find(&users)
// select id, name from users where name = 'User-1'
fmt.Println(users)
// [{4 User-1}]
```

### SQLを直接記述

SQLを`Raw`で、結果を格納する構造体を`Scan`で指定します

```go
users := []User{}
connection.Raw("select id, name from users where id = ?", 1).Scan(&users)
// select id, name from users where id = 1
fmt.Println(users)
// [{1 User-4}]
```

## Update

Updateには以下の２つのメソッドを使用します
※公式の説明ではなく個人の見解です💦

| メソッド | サンプル | 使い分け |
|-----|-----|-----|
| Save   | .Save(&user) | selectで取得した構造体の一部を更新したいとき
| Update | .Model(&user).Update("name", "updated name") | 条件やカラムを指定して更新したいとき

以下のようなテーブルの状態でサンプルを実装・実行していきます

```
mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | User-4 |
|  2 | User-3 |
|  3 | User-2 |
|  4 | User-1 |
+----+--------+
```

### Save

```go
user := User{}
// 「ID = 1」で検索
connection.Where("id = ?", 1).First(&user)
fmt.Println(user)
// {1 User-4}

// Nameを「User-X」に更新
user.Name = "User-X"
connection.Save(&user)

// 「ID = 1」で検索
connection.Where("id = ?", 1).First(&user)
fmt.Println(user)
// {1 User-X}
```

### Update（単一カラム）

単一カラムを更新したい場合は引数に「更新カラム」と「更新後の値」を設定して`Update`メソッドを呼び出します
また`Model`で指定した構造体に更新後の値が自動で設定されます

```go
user := User{Id: 1}
fmt.Println(user)
// {1 }

// nameを「User-X」に更新
connection.Model(&user).Update("name", "User-X")
fmt.Println(user)
// {1 User-X}
```

### Updates（複数カラム）

ここではサンプルのテーブルの構成上、単一カラムの更新となっていしまいますが、
構造体の更新したいフィールドに更新後の値を設定して`Updates`メソッドに渡すことで複数カラムの更新が可能です。

```go
// 検索用にIDを指定
userForSearch := User{Id: 1}
fmt.Println(userForSearch)

// 更新したい内容を別の構造体に定義
userForUpdate := User{Name: "User-X"}
// 同じ: map[string]interface{}{"name": "User-X"}

// 更新
connection.Model(&userForSearch).Updates(&userForUpdate)
// update users set id = 1 where name = 'User-X'
fmt.Println(userForSearch)
// {1 User-X}
```

### 複数行の更新

- `Where`で更新条件を指定して`Updates`を実行します
- 全件更新の場合にも`1 = 1`等で条件指定が必要です

```go
connection.Table("users").Where("name like ?", "User-%").Updates(User{Name: "User-X"})
// 「.Table("users")」は「.Model(User{})」でも可

users := []User{}
connection.Find(&users)
fmt.Println(users)
```

### 計算式を用いて更新後の値を表現する

`gorm.Expr`を使用すると固定値以外を更新後の値に設定することができます

```go
user := User{}
// gorm.Exprで計算式を記述
connection.Model(&user).Update("name", gorm.Expr("concat('User-', id)"))
// 全件取得
users := []User{}
connection.Find(&users)
fmt.Println(users)
// [{1 User-1} {2 User-2} {3 User-3} {4 User-4}]
```

### SQLを指定して更新

```go
// SQLを指定して更新
connection.Exec("update users set name = 'User-X'")
// 全件取得
users := []User{}
connection.Find(&users)
fmt.Println(users)
// [{1 User-X} {2 User-X} {3 User-X} {4 User-X}]
```

### 更新件数・結果

`Update`の戻り値を取得し、`RowsAffected`・`Error`で確認できます

```go
result := connection.Model(&user).Update("name", "User-X")
fmt.Println(result.RowsAffected)
// 1
fmt.Println(result.Error)
// <nil> ※エラーなしのためNil
```

## Delete

以下のようなテーブルの状態でサンプルを実装・実行していきます

```
mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | User-4 |
|  2 | User-3 |
|  3 | User-2 |
|  4 | User-1 |
+----+--------+
```

### 1件削除

`Delete`メソッドに主キー項目が設定された構造体を渡して実行します

```
// 削除
user := User{Id: 1}
connection.Delete(&user)
// delete from users where id = 1

users := []User{}
connection.Find(&users)
fmt.Println(users)
// [{2 User-3} {3 User-2} {4 User-1}]
```

### 主キー以外の条件指定

`Where`メソッドを使用して条件を指定します

```go
// 削除
connection.Where("id >= ?", 3).Delete(&User{})
// delete from users where id >= 3

users := []User{}
connection.Find(&users)
fmt.Println(users)
// [{1 User-4} {2 User-3}]
```

### 論理削除/物理削除

以下の条件がそろっているときに`Delete`はデフォルトで論理削除を実行します

- テーブルに`deleted_at datetime`カラムがある
- 構造体に`DeletedAt gorm.DeletedAt`フィールドがある

論理削除されたレコードは`Unscoped`メソッドを実行して`Delete`を実行することで物理削除されます

```go
user := User{}
connection.Where("id = ?", 1).First(&user)

// 論理削除
connection.Delete(&user)
// update users set deleted_at="2020-09-08 22:21:43" where id = 1

connection.Unscoped().Where("id = ?", 1).First(&user)
fmt.Println(user)
// {1 User-4 {2020-09-08 22:21:43 +0900 JST true}}

// 物理削除
connection.Unscoped().Delete(&user)
// delete from users where id = 1

result := connection.Unscoped().Where("id = ?", 1).First(&user)
fmt.Println(result.Error)
// record not found
```

# トランザクション管理

例えば①②③の順で実行されることで一貫性が保てる以下のような処理について

```go
connection.Create(&user1)  // ①
connection.Save(&user2)    // ②
connection.Delete(&user3)  // ③
```

1行ずつ実行ごとにcommitがされるため、仮に③で失敗すると①②がロールバックされず一貫性が保てなくなります。

このような場合は`Transaction`メソッドを使用すると制御が可能になります

- `*gorm.DB`を引数にとり、`error`を返す関数を渡します
- `error`を返すとブロック内の処理がrollbackされます
- nilを返すとcommitされます

```go
connection.Transaction(func(tx *gorm.DB) error {
	if err := tx.Create(&user1).Error; err != nil {
		return err  // rollback
	}
	if err := tx.Save(&user2).Error; err != nil {
		return err  // rollback
	}
	if err := tx.Delete(&user3).Error; err != nil {
		return err  // rollback
	}

	return nil  // commit
})
```