Go言語で簡単にDBアクセスを実現!GORMを使ってORMを体験しよう-Create編(1)

はじめに

前回は GORM の導入方法と基本的な機能を紹介しました。

gorm.Open 関数を呼び出した結果、取得される gorm.DB インスタンス ( インスタンスと呼んでいいのか Go 言語の思想がわからませんが ) のメソッドを使って様々な操作を行うことができます。

今回は Create (登録処理)に関するさまざまな機能を紹介します。

Create ( 登録 )

gorm.DB#Create() を使います。

サンプルコードを以下に記載します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name      string
	Age       uint
	Biarthday time.Time
}

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	user := User{Name: "Jinzhu", Age: 18, Biarthday: time.Now()}
	result := db.Create(&user)

	fmt.Printf("%+v\n", user.ID)             // Value of user.ID is 1
	fmt.Printf("%+v\n", result.Error)        // Value of result.Error is nil
	fmt.Printf("%+v\n", result.RowsAffected) // Value of result.RowsAffected is 1
}

gorm.DB#Create() を実行した結果は「戻り値」と「引数で指定した構造体(のポインタ)」から取得できます。

Create() メソッドには構造体のスライスを指定し同時に複数のデータを登録できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"fmt"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name      string
	Age       uint
	Biarthday time.Time
}

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	users := []*User{
		&User{Name: "Jinzhu", Age: 18, Biarthday: time.Now()},
		&User{Name: "Jackson", Age: 20, Biarthday: time.Now()},
	}

	result := db.Create(&users)

	fmt.Printf("%+v\n", result.Error)        // Value of result.Error is nil
	fmt.Printf("%+v\n", result.RowsAffected) // Value of result.RowsAffected is 2

	for _, user := range users {
		fmt.Printf("%+v\n", user.ID) // First user's ID is 1, second user's ID is 2
	}
}

一部のフィールドだけを使う

構造体に設定されているフィールドのうち、一部のフィールドを選択して登録に利用できます。

User 構造体には Name / Age / Birthday の 3 フィールドに値をセットしましたが、登録には Name , Birthday だけを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
	"fmt"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name      string
	Age       uint
	Biarthday time.Time
}

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	time, _ := time.Parse("2006-01-02", "1990-01-01")
	user := User{Name: "Jinzhu", Age: 18, Biarthday: time}

	db.Select("Name", "Biarthday").Create(&user)

	fmt.Printf("%+v\n", user.ID)        // 1
	fmt.Printf("%+v\n", user.Name)      // Jinzhu
	fmt.Printf("%+v\n", user.Age)       // 18
	fmt.Printf("%+v\n", user.Biarthday) // 1990-01-01 00:00:00 +0000 UTC

	// データをロードし直す
	db.Find(&user, user.ID)

	fmt.Printf("%+v\n", user.Name)      // Jinzhu
	fmt.Printf("%+v\n", user.Age)       // 18
	fmt.Printf("%+v\n", user.Biarthday) // 1990-01-01 00:00:00 +0000 UTC
}

逆に、無視するフィールドを指定することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
	time, _ := time.Parse("2006-01-02", "1990-01-01")
	user := User{Name: "Jinzhu", Age: 18, Biarthday: time}

	db.Omit("Age").Create(&user)

	fmt.Printf("%+v\n", user.ID)        // 1
	fmt.Printf("%+v\n", user.Name)      // Jinzhu
	fmt.Printf("%+v\n", user.Age)       // 18
	fmt.Printf("%+v\n", user.Biarthday) // 1990-01-01 00:00:00 +0000 UTC

	// データをロードし直す
	db.Find(&user, user.ID)

	fmt.Printf("%+v\n", user.Name)      // Jinzhu
	fmt.Printf("%+v\n", user.Age)       // 18
	fmt.Printf("%+v\n", user.Biarthday) // 1990-01-01 00:00:00 +0000 UTC

バッチインサート

先程も紹介したとおり、大量レコードを効率的に登録するためにCreate メソッドに対してスライスを渡すことができます。

GORM は単一の insert 文に複数の値を詰め込んだ SQL ( バッチインサート ) を生成してくれます。

データベースの機能で、トランザクションは単一となるため、すべての insert に成功するか、失敗するかのいずれかになります。

1
2
3
4
5
6
	var users = []User{{Name: "taro"}, {Name: "jiro"}, {Name: "saburo"}}
	db.Create(&users)

	for _, user := range users {
		fmt.Println(user.ID) // 1, 2, 3
	}

バッチサイズを指定して、1回の SQL で登録されるレコード数を制御することも可能です。 ( 登録件数が多すぎる場合、長時間のトランザクション発生によりデータベースの負荷が懸念されるケースに対処できます。 )

1
2
3
4
5
6
7
	users := make([]*User, 100)
	for i := 0; i < 100; i++ {
		name := fmt.Sprintf("user%d", i)
		users[i] = &User{Name: name, Age: 18, Biarthday: time.Now()}
	}

	db.CreateInBatches(users, 10) // it will create 10 records per SQL

更には、デフォルトのバッチサイズを指定することもできます。 Create メソッド呼び出し時には小音設定値にしたがってマルチインサートを行うようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  CreateBatchSize: 1000,
})

db := db.Session(&gorm.Session{CreateBatchSize: 1000})

users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}

db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)

Create フック

登録や更新の前後をイベントのフックタイミングにして、処理を実行させることができます。

以下のようなメソッドを定義することで、処理を発火できます。

  • BeforeSave
  • BeforeCreate
  • AfterSave
  • AfterCreate

各フックメソッド実行のタイミングをサンプルコードで見ていきます。

ここで db.Save() というメソッドが登場していますが、これは引数に渡された構造体の ID (Primary Key)が nil 値の場合には INSERT を、 非 nil 値の場合には UPDATE を実行するメソッドです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
	"fmt"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name      string
	Age       uint
	Biarthday time.Time
}

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	u.Name += "+BC"
	fmt.Println("BeforeCreate : " + u.Name)
	return
}

func (u *User) BeforeSave(tx *gorm.DB) (err error) {
	u.Name += "+BS"
	fmt.Println("BeforeSave: " + u.Name)
	return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
	u.Name += "+AC"
	fmt.Println("AfterCreate: " + u.Name)
	return
}

func (u *User) AfterSave(tx *gorm.DB) (err error) {
	u.Name += "+AS"
	fmt.Println("AfterSave: " + u.Name)
	return
}

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	birthday, _ := time.Parse("2006-01-02", "2006-01-02")
	u1 := User{Name: "genzouw", Age: 15, Biarthday: birthday}
	db.Save(&u1)

	var u2 User
	db.First(&u2, u1.ID)
	fmt.Printf("ID=%d, Name=%s\n", u2.ID, u2.Name)

	u2.Name = "genzouw2"
	db.Save(&u2)

	var u3 User
	db.First(&u3, u1.ID)
	fmt.Printf("ID=%d, Name=%s\n", u3.ID, u3.Name)
}

実行結果は以下のようになります。

1
2
3
4
5
6
7
8
BeforeSave: genzouw+BS
BeforeCreate : genzouw+BS+BC
AfterCreate: genzouw+BS+BC+AC
AfterSave: genzouw+BS+BC+AC+AS
ID=1, Name=genzouw+BS+BC
BeforeSave: genzouw2+BS
AfterSave: genzouw2+BS+AS
ID=1, Name=genzouw2+BS

実行結果からわかることをまとめると以下のようになります。

メソッド実行順序insert 時に実行?update 時に実行?DB に反映?
BeforeSave1ooo
BeforeCreate2oo
AfterCreate3o
AfterSave4oo

insert、update 関わらずデータベースへの保存前に処理を行いたい場合は、 BeforeSave で実装するのが良いでしょう。

フック機能は便利ですが、どうしても公開の登録・更新・削除処理でだけ無効にしたい ( ex: マイグレーションしたい ) ときには、一時的に無効にできます。

以下の例では、 BeforeSave / BeforeCreate / AfterCreate / AfterSave のいずれのフックも実行されません。

1
2
3
	birthday, _ := time.Parse("2006-01-02", "2006-01-02")
	u1 := User{Name: "genzouw", Age: 15, Biarthday: birthday}
	db.Session(&gorm.Session{SkipHooks: true}).Save(&u1)

構造体ではなくマップを使う

構造体ではなくマップを使って登録を行うことができます。

  • 単一レコードの登録には &map[string]inferface{}
  • 複数レコードの登録には &[]map[string]inferface{} (マップのスライス)

を Create メソッドの引数に指定します。

1
2
3
4
5
6
7
8
	db.Model(&User{}).Create(&map[string]interface{}{
		"Name": "taro", "Age": 18,
	})

	db.Model(&User{}).Create(&[]map[string]interface{}{
		{"Name": "jiro", "Age": 20},
		{"Name": "saburo", "Age": 19},
	})

ひとこと

思ったよりも長くなりそうなので、次回に続きます。