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

はじめに

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

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

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

Create ( 登録 )

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

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

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() メソッドには構造体のスライスを指定し同時に複数のデータを登録できます。

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 だけを使用します。

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
}

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

    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 に成功するか、失敗するかのいずれかになります。

    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 で登録されるレコード数を制御することも可能です。
( 登録件数が多すぎる場合、長時間のトランザクション発生によりデータベースの負荷が懸念されるケースに対処できます。
)

    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 メソッド呼び出し時には小音設定値にしたがってマルチインサートを行うようになります。

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 を実行するメソッドです。

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)
}

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

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 のいずれのフックも実行されません。

    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 メソッドの引数に指定します。

    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},
    })

ひとこと

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

Posted by genzouw