🚗

【Go】Gormの使い方(CRUD)

2022/05/25に公開

はじめに

普段Xormを使っているのですが、Gormを使ってみたくなり、CRUDを少しまとめてみました。
(内容は少しですが、思ったよりだいぶ時間かかりました)
詳細を知りたい方は、Gormは日本語の公式ドキュメントがありまして、とても分かりやすいのでそちらをご覧ください。

環境

  • go 1.18
  • gorm.io/driver/mysql v1.3.3
  • gorm.io/gorm v1.23.5

準備

今回はdocker使って試してみます。
main.goの中で、今回使用する、User構造体を定義しているのと、dbの作成をしています。

docker-compose.yml

version: "3"
services:
  db:
    image: mysql:5.7
    environment:
    - MYSQL_DATABASE=sample_db
    - MYSQL_ROOT_PASSWORD=password
    command: >
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
    ports:
      - 3306:3306

main.go

package main

import (
	"fmt"

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

type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model
	
	Name     string
	Age      int
	IsActive bool	
}

func main() {
	// dbを作成します
	db := dbInit()
	
	// dbをmigrateします
	db.AutoMigrate(&User{})
}

func dbInit() *gorm.DB {
	dsn := "root:password@tcp(127.0.0.1:3306)/sample_db?charset=utf8mb4&parseTime=true"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	return db
}

Gormは、Userという構造体の名前でmigrateすると、usersテーブルという名前でちゃんと複数形にしてくれるのが良いですね。
(Xormだと、Usersという名前の構造体にしないと、usersテーブルという名前にならない)

上記の状態でgo run main.goをすると、
下記のようなテーブルが作成されます。
db.AutoMigrate(&User{})
この部分でテーブルの作成を行ってくれます。
簡単で良いですね。

構造体の中に、gorm.Modelを書くことで、
自動的に下記の4つのカラムが作成されます。

  • id
  • created_at
  • updated_at
  • deleted_at
    そして、idにはPrimaryKeyと auto_incrementが設定されています。
    また、後述しますが、gormでは、gorm.Modelでdeleted_atを作成している場合、Deleteすると、自動的に論理削除になるという仕様があります。

CRUD

Create

// 単体作成
func insert(db *gorm.DB) {
	user := User{
		Name:     "太郎",
		Age:      20,
		IsActive: true,
	}
	result := db.Create(&user)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)
}

結果

1つのレコードが作成されました。
idとcreated_atとupdated_atは勝手にデータが入ります。
エラーがある場合は、result.Errorの中にエラーが入ります。
また、result.RowsAffectedの中には、今回の結果のRow数(行数)が入ります。

// 複数作成
func inserts(db *gorm.DB) {
	users := []User{
		{
			Name:     "花子",
			Age:      25,
			IsActive: true,
		},
		{
			Name:     "龍太郎",
			Age:      30,
			IsActive: false,
		},
		{
			Name:     "太一",
			Age:      35,
			IsActive: false,
		},
	}
	result := db.Create(&users)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)
}

結果

gormのCreate関数はスライスを渡すと、複数作成してくれます。

Read

単体取得

単体取得には主に3つのメソッドがあります。
特に条件が無ければ、Takeになるかと思います。

  • First

    • プライマリーキーの昇順で取得
    • プライマリーキーが無い場合は、モデルの最初のフィールドで順序付けされる。
  • Take

    • 特に条件を指定せず取得
  • Last

    • プライマリーキーの降順で取得
    • プライマリーキーが無い場合は、モデルの最初のフィールドで順序付けされる。
// 単体取得
func getOne(db *gorm.DB) {
	// 昇順で単体取得
	user1 := User{}
	result1 := db.First(&user1)
	// SELECT * FROM users ORDER BY id LIMIT 1;
	fmt.Println("first:", user1)
	// check error ErrRecordNotFound
	if errors.Is(result1.Error, gorm.ErrRecordNotFound) {
		log.Fatal(result1.Error)
	}
	fmt.Println("count:", result1.RowsAffected)

	// 何も指定せず、単体取得
	user2 := User{}
	result2 := db.Take(&user2)
	// SELECT * FROM users LIMIT 1;
	fmt.Println("take:", user2)
	if errors.Is(result2.Error, gorm.ErrRecordNotFound) {
		log.Fatal(result2.Error)
	}

	// 降順で単体取得
	user3 := User{}
	result3 := db.Last(&user3)
	// SELECT * FROM users ORDER BY id DESC LIMIT 1;
	fmt.Println("last:", user3)
	if errors.Is(result3.Error, gorm.ErrRecordNotFound) {
		log.Fatal(result3.Error)
	}
}

出力結果

first: {{1 2022-05-23 04:33:26.367 +0000 UTC 2022-05-23 04:33:26.367 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太郎 20 true}
count: 1
take: {{1 2022-05-23 04:33:26.367 +0000 UTC 2022-05-23 04:33:26.367 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太郎 20 true}
last: {{4 2022-05-23 04:33:26.413 +0000 UTC 2022-05-23 04:33:26.413 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太一 35 false}

Gormで単体取得を行うと、DBにデータが無い場合は、NotFoundのエラーとなってしまいます。
その為、次のように記載すると、データが無い場合、err有りと判断されてしまいます。

	if result.Error != nil {
		log.Fatal(result.Error)
	}

ですので、次のような書き方が良いかと思います。

	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		log.Fatal(result.Error)
	}

他にもいろんな取得方法があります。

// プライマリーキーで取得
db.First(&user, 1)
db.First(&user, "id = ?", 1)

全件取得

	// 全件取得
	func find(db *gorm.DB) {
		users := []User{}
		result := db.Find(&users)
		fmt.Println("user:", users)
		if result.Error != nil {
			log.Fatal(result.Error)
		}
		fmt.Println("count:", result.RowsAffected)
	}

出力結果

user: [{{1 2022-05-22 14:32:38.139 +0000 UTC 2022-05-22 14:32:38.139 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太郎 20 true} {{2 2022-05-22 14:37:42.548 +0000 UTC 2022-05-22 14:37:42.548 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 花子 25 true} {{3 2022-05-22 14:37:42.548 +0000 UTC 2022-05-22 14:37:42.548 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 龍太郎 30 false} {{4 2022-05-22 14:37:42.548 +0000 UTC 2022-05-22 14:37:42.548 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太一 35 false}]
count: 4

ちゃんと全件取得出来ています。

Update

updateは主に3つのメソッドがあります。

  • save
    • upsertになる。
    • 既存のものがあれば上書き、なければ追加となります。
    • 使い方
      • 先に単体取得をしておいて、構造体とマッピングしておく。
      • その構造体の中身を更新したい値に変更する。
      • 更新した構造体をsaveに渡すと、更新される。
      • その構造体の中にidやプライマリーキーが入っていないと、新しいレコードが作成される。
  • update
    • 1つのカラムだけを更新する。
  • updates
    • 複数のカラムを更新する。
// 更新(upsert)
func save(db *gorm.DB) {
	// 構造体にidが無い場合はinsertされる
	user1 := User{}
	user1.Name = "花子"
	result1 := db.Save(&user1)
	if result1.Error != nil {
		log.Fatal(result1.Error)
	}
	fmt.Println("count:", result1.RowsAffected)
	fmt.Println("user1:", user1)

	// 先にユーザーを取得する
	user2 := User{}
	db.First(&user2)

	// 構造体にidがある場合はupdateされる
	user2.Name = "たけし"
	result2 := db.Save(&user2)
	if result2.Error != nil {
		log.Fatal(result2.Error)
	}
	fmt.Println("count:", result2.RowsAffected)
	fmt.Println("user2:", user2)
}

出力結果

count: 1
user1: {{5 2022-05-24 13:34:18.663 +0900 JST 2022-05-24 13:34:18.663 +0900 JST {0001-01-01 00:00:00 +0000 UTC false}} 花子 0 false}
count: 1
user2: {{1 2022-05-23 04:33:26.367 +0000 UTC 2022-05-24 13:34:18.688 +0900 JST {0001-01-01 00:00:00 +0000 UTC false}} たけし 20 true}

最初のuser1では、user情報のidを渡していなかった為、新しくレコードが追加されました。
2個目のuser2では、user情報を持っていた(idを持っていた)ので、新しくレコードは追加されず、既存のレコードが更新される事になりました。

// 単一のカラムを更新する
func update(db *gorm.DB) {
	result := db.Model(&User{}).Where("id = 2").Update("name", "ジョージ")
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)

	user := User{}
	db.Where("id = 2").Take(&user)
	fmt.Println("user:", user)
}

出力結果

count: 1
user: {{2 2022-05-23 04:33:26.413 +0000 UTC 2022-05-24 04:49:01.612 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} ジョージ 25 true}

名前が更新されました。
updateでは一つのカラムしか更新出来ないです。
複数のカラムを更新する場合は、updatesを使う必要があります。

// 複数のカラムを更新する
func updates(db *gorm.DB) {
	result := db.Model(&User{}).Where("id = 1").Updates(User{Name: "Taro", Age: 10, IsActive: true})
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)

	user := User{}
	db.Where("id = 1").Take(&user)
	fmt.Println("user:", user)
}

出力結果

count: 1
user: {{1 2022-05-23 04:33:26.367 +0000 UTC 2022-05-24 04:57:56.759 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} Taro 10 true}

基本はupdatesを利用して、upsertしたい時にsaveを使う感じでしょうか。

注意点1

Modelで主キーを指定していない場合、GORMは一括更新になるので注意

// 一括更新
func updatesAll(db *gorm.DB) {
	user := User{
		Name:     "Ryu",
		Age:      100,
		IsActive: true,
	}
	result := db.Where("name = ?", "花子").Updates(&user)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)

	users := []User{}
	db.Find(&users)
	fmt.Println("users:", users)
}

このような書き方をしてしまうと、nameカラムの値が、花子のレコードの全てが、name="Ryu",
age=100,is_active=trueになってしまいます。

変更前


変更後

こうなります。

count: 2
users: [{{1 2022-05-22 14:32:38.139 +0000 UTC 2022-05-25 12:29:26.623 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} Ryu 100 true} {{2 2022-05-22 14:37:42.548 +0000 UTC 2022-05-25 12:29:26.623 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} Ryu 100 true} {{3 2022-05-22 14:37:42.548 +0000 UTC 2022-05-22 14:37:42.548 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 龍太郎 30 false} {{4 2022-05-22 14:37:42.548 +0000 UTC 2022-05-22 14:37:42.548 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 太一 35 false}]

誤って一括更新しないようにちゃんとプライマリーキーを指定して更新しましょう。
なお、Where句が無い状態で、更新をかけようとすると、それは流石にエラーとなるようです。
gorm.ErrMissingWhereClauseというエラーになります。

// これはエラー
db.Model(&User{}).Update("name", "ジョン")

注意点2

  • 構造体での更新の場合、ゼロ値を更新しない。
  • Bool型の時にfalseに更新したいけど、更新されていない...。という事が発生してしまいます。
  • Selectで指定する事でする事で回避できます。

// ゼロ値更新されない
func noUpdates(db *gorm.DB) {
	result := db.Model(User{}).Where("id = 1").Updates(User{Name: "マリオ", IsActive: false})
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("count:", result.RowsAffected)

	user := User{}
	db.Where("id = 1").Take(&user)
	fmt.Println("user:", user)
}

更新前


更新後

出力結果

count: 1
user: {{1 2022-05-22 14:32:38.139 +0000 UTC 2022-05-25 13:07:13.618 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} マリオ 100 true}

名前だけ更新されていて、bool型のis_activeはfalseに更新されていません。
これはやりがちなミスなので気をつけましょう。

// Selectで指定することで更新されます
result := db.Model(User{}).Where("id = 1").Select("name", "is_active").Updates(User{Name: "マリオ", IsActive: false})

Delete

通常の削除

func delete(db *gorm.DB) {
	db.Where("id = 1").Delete(&User{})
}

これでidが1のユーザーが削除されます。

注意点1

  • プライマリーキーを指定しないと、一括削除になります。
  • Updateと一緒ですね。
func delete(db *gorm.DB) {
	db.Where("name = ?", "太郎").Delete(&User{})
}

名前が太郎のユーザーが全員消えてしまいます。
ちなみに、Where句が無いとエラーとなります。
これもUpdateと一緒です。

注意点2

  • 対象のテーブルとマッピングさせる構造体のフィールドにgorm.DeletedAtがあると、論理削除になります。
  • これは大きな特徴です。

type Product struct {
	Code    string
	Price   uint
	Deleted gorm.DeletedAt
}

type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model

	Name     string
	Age      int
	IsActive bool
}

上の二つの構造体の場合は、通常の削除をしても、論理削除となってしまいます。
どうしても物理削除をしたい場合は、

db.Unscoped().Where("id = 1").Delete(&User{})

とする事で、gorm.DeletedAtを使用していても、削除する事が出来ます。

さいごに

普段はXormを使っているのですが、Gormはいろんな書き方があって面白い反面、ちゃんとルールを作らないと、みんないろんな書き方をしてしまうのでは無いかと思いました。
また、Xormより色々出来るので、面白いですが、罠がいっぱい潜んでいるので、しっかりドキュメント読んで仕様把握しないとバグ生みそうです。
Gormの一番良いところは、日本語のとても読みやすいドキュメントが用意されている事ですね。
Xormにも日本語のドキュメント用意して欲しい...。中国語は読めません。

参考

Discussion