概要
- GoでMySQL接続をしようと思っていた時に、「gorm」というORMライブラリを見つけたので使ってみた
- 公式のドキュメントを参考に実装時に必要になった知識をまとめていきます
準備
gormをインストールします
またMySQLと接続するためのDriverもインストールします
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
DB接続
-
gorm.Open
を使用します。 - パスワードのみ環境変数から取得するようにしてみました。
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)で定義します
type User struct {
Id int `gorm:"primaryKey"`
Name string
}
次にテーブルを作成します。
- テーブル名はモデルの複数形にします
- 主キーの設定が必須です
- 主キーをベースにUpdate,Deleteが実行されるため、主キーがないと全件操作されてしまいます
- 構造体のフィールドをスネークケース(snake_case)に変換したもので定義します
create table users (
id int primary key,
name varchar(10)
);
Create (insert)
-
Create
メソッドを使用します - 引数にはModelのポインタを渡します
user := User{Id: 1, Name: "User-1"}
connection.Create(&user)
mysql> select * from users;
+----+--------+
| id | name |
+----+--------+
| 1 | User-1 |
+----+--------+
一意制約エラーの場合
Insert時に気にすることの代表格として一意制約エラーがありますが、
SQLでエラーとなった場合には以下のようにErrorを確認できます
※一意制約エラー以外でも同様です。
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の機能により、採番された番号がオートフィルされます!
user := User{Name: "User-1"}
fmt.Println(user) // {0 User-1}
connection.Create(&user)
fmt.Println(user) // {1 User-1}
Read (select)
USERSテーブルに以下の4件のレコードがある状態で実行していきます。
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 ※単一行を取得するための終端操作 |
last | Last(&user) | order by id desc limit 1 ※単一行を取得するための終端操作 |
Take | Take(&user) | limit 1 ※単一行を取得するための終端操作 |
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使用時等にレコードを格納する構造体を指定するために使用 |
以下これらのメソッドを使用して実際にレコードを取得するサンプルです。
全件取得
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
の第2引数に検索条件を渡すせばOK!
user := User{}
connection.First(&user, 1)
// select * from users where id = 1 limit 1
fmt.Println(user)
// {1 User-4}
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
メソッドを使うことでクエリを表現できます。
それぞれ
- 第一引数:検索条件
- 第二引数以降:バインドする値
というように指定します。
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
は値が設定されていないという判定がされるため、使用できないようです
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
で指定します
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には以下の2つのメソッドを使用します
※公式の説明ではなく個人の見解です💦
メソッド | サンプル | 使い分け |
---|---|---|
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
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
で指定した構造体に更新後の値が自動で設定されます
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
メソッドに渡すことで複数カラムの更新が可能です。
// 検索用に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
等で条件指定が必要です
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
を使用すると固定値以外を更新後の値に設定することができます
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を指定して更新
// 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
で確認できます
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
メソッドを使用して条件を指定します
// 削除
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
を実行することで物理削除されます
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
トランザクション管理
例えば①②③の順で実行されることで一貫性が保てる以下のような処理について
connection.Create(&user1) // ①
connection.Save(&user2) // ②
connection.Delete(&user3) // ③
1行ずつ実行ごとにcommitがされるため、仮に③で失敗すると①②がロールバックされず一貫性が保てなくなります。
このような場合はTransaction
メソッドを使用すると制御が可能になります
-
*gorm.DB
を引数にとり、error
を返す関数を渡します -
error
を返すとブロック内の処理がrollbackされます - nilを返すとcommitされます
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
})