コンテンツにスキップ

SQLパッケージ@Go

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. gorm

gormとは

Go製のORMである。

その他のORMについては、以下のリポジトリが参考になる。

執筆時点 (2022/01/31) では、gormとbeegoが接戦している。


DBとの接続

▼ MySQLの場合

package db

func NewDB() (*gorm.DB, error) {

    // 接続情報。sprintf関数を使用すると、可読性が高い。
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        os.Getenv("DB_DATABASE"),
    )

    // DBに接続します。
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        return nil, err
    }

    return db, nil
}

func Close(db *gorm.DB) error {

    sqlDb, err := db.DB()

    if err != nil {
        return err
    }

    // DBとの接続を切断します。
    err = sqlDb.Close()

    if err != nil {
        return err
    }

    return nil
}


gormモデル

▼ gormモデル埋め込み

構造体にgormモデルを埋め込むと、IDやタイムスタンプレコードをフィールドとして追加する。

構造体をマッピングしたテーブルに、対応するカラム (idcreated_atupdated_atdeleted_at) を持つレコードを作成する。

package model

type User struct {
    // gormモデルを埋め込む
    gorm.Model
    Name string
}

// 以下と同じ
type User struct {
    ID        uint `gorm:"primaryKey"`
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeleteAt `gorm:"index"`
}

▼ データベースへのマッピング

Gormは、構造体をデータベースに自動的にマッピングする。

明示的に設定しない限り、スネークケースに変換して自動的にマッピングする。

package model

// usersテーブルにマッピング
type User struct {
    // テーブルの主キーにマッピング
    ID        uint
    // nameカラムにマッピング
    Name      string
    // created_atカラムにマッピング
    CreatedAt time.Time
    // updated_atカラムにマッピング
    UpdatedAt time.Time
    // deleted_atカラムにマッピング
    DeletedAt gorm.DeleteAt
}

▼ プライマリーキー

『ID』という名前のフィールドを認識して、これをプライマリーキーとしてデータをマッピングする。

もし、他の名前のフィールドをIDとして使用したい場合は、gorm:"primaryKey"タグをつける。

package model

type User struct {
    ID   string // プライマリーキーとして使用される。
    Name string
}
package model

type User struct {
    UserID string `gorm:"primaryKey"` // プライマリーキーとして使用される。
    Name   string
}

▼ SoftDelete

構造体が、gorm.DeleteAtをデータ型とするフィールドを持っていると、その構造体を使用したDELETE処理では論理削除が実行される。

gormモデルを埋め込むことによりこのフィールドを持たせるか、または自前定義することにより、SoftDeleteを有効化できる。

package model

type User struct {
    ID      int
    Deleted gorm.DeletedAt
    Name    string
}
user := User{Id: 111}

// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
db.Delete(&user)

// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
db.Where("age = ?", 20).Delete(&User{})

// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
db.Where("age = 20").Find(&user)


DBマイグレーション

TableName関数

デフォルトではgormモデルの名前をスネークケースに変更し、加えて複数形とした名前のテーブルを作成する。

TableName関数により、ユーザー定義のテーブル名をつけられる。

package model

// テーブル名はデフォルトでは『users』になる。
type User struct {
    ID      int
    Deleted gorm.DeletedAt
    Name    string
}

// テーブル名を『foo』になる。
func (User) TableName() string {
    return "foo"
}


01-02. トランザクション

トランザクションの流れ

  1. トランザクションを開始する
  2. 複数のCRUD処理を実行する
  3. コミットする
  4. 1-3の処理が途中で失敗した場合にロールバックする
package db

func CreateFoo(db *gorm.DB) error {

    // トランザクションを開始する
    tx := db.Begin()

    // ロールバックは遅延実行する
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    if err := tx.Error; err != nil {
        return err
    }

    // CREATE処理を実行する
    if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
        tx.Rollback()
        return err
    }

    // CREATE処理を実行する。
    if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
        tx.Rollback()
        return err
    }

    // コミットする
    return tx.Commit().Error
}


CREATE処理

▼ CREATE処理とは

gormモデルのフィールドに設定された値を元に、レコードを作成する。

作成したレコードのプライマリーキーを、構造体から取得できる。

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user)

user.ID

result.Error

result.RowsAffected


READ処理

▼ READ理とは

gormモデルに対応するレコードを読み出す。

▼ 全レコード取得

user := User{}

// SELECT * FROM users;
result := db.Find(&users)

result.RowsAffected

result.Error

▼ 単一/複数レコード取得

gormモデルとプライマリーキーを指定して、プライマリーキーのモデルに紐付けられたレコードを取得する。

user := User{}

// SELECT * FROM users WHERE id = 10;
db.First(&user, 10)

// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")

// SELECT * FROM users WHERE id IN (1,2,3);
db.Find(&users, []int{1,2,3})


UPDATE処理

▼ UPDATEとは

gormモデルのフィールドに設定された値を元に、レコードを変更する。

▼ 単一レコード更新 (暗黙的)

フィールドとは無関係に、渡された値を元にUPDATE分を実行する。

// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")

user := User{Id:111}

// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&user).Update("name", "hello")

// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
db.Model(&user).Where("active = ?", true).Update("name", "hello")

▼ 複数レコード更新 (暗黙的)

gormモデルのフィールドを暗黙的に指定して、複数のレコード値を更新する。

または、フィールドとは無関係に、mapを元にUPDATE文を実行する。

gormモデルを使用した場合、フィールド値がゼロ値であると、これに紐付けられたレコード値の更新はスキップされてしまう。

user := User{Id:111}

// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: "false"})

// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": "false"})

▼ 複数レコード更新 (明示的)

gormモデルのフィールドを明示的に指定して、複数のレコード値を更新する。

フィールド値がゼロ値であっても、スキップされない。

user := User{Id:111}

// UPDATE users SET name='new_name', age=0 WHERE id=111;
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})

// UPDATE users SET name='new_name', age=0 WHERE id=111;
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

▼ 全レコード更新

gormモデルのフィールドを暗黙的に全て指定して、全てのレコード値を強制的に更新する。

user := User{Id:111}

db.First(&user)

user.Name = "jinzhu 2"

user.Age = 100

// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
db.Save(&user)


01-04. クエリのオプション

WithContext

▼ WithContextとは

gormクエリにコンテキストを設定する。

// タイムアウト時間を設定し、コンテキストを作成する
ctx, cancel := context.WithTimeout(
    context.Background(),
    5 * time.Second,
)

// タイムアウト時間経過後に処理を中断する
defer cancel()

db.WithContext(ctx).Find(&users)


Exec

▼ Execとは

SQLステートメントをそのまま実行する。

db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")
db.Exec(fmt.Sprintf("SET SESSION max_execution_time=%d;", 10))


Hook

▼ Hookとは

CRUDの関数の前後に設定した独自処理を実行できるようにする。

▼ 特定のCRUD関数の前後

package db

func NewDb() {

    ...

    // Createの前
    db.Callback().Create().Before("gorm:before_create").Register("<フック名>", "<CRUD関数名>")

    ...
}

▼ 全てのCRUD関数の前後

package db

func (user *User) BeforeSave(tx *gorm.DB) (err error) {
    user.LastUpdated = time.Now()
    return nil
}
package db

func (user *User) AfterSave(tx *gorm.DB) (err error) {
    log.Println("User successfully saved:", user.ID)
    return nil
}


Statement

▼ Statementとは

gormクエリに関する情報を持つ。

package db

type DB struct {
    *Config
    Error        error
    RowsAffected int64
    Statement    *Statement
    clone        int
}

type Statement struct {
    *DB
    TableExpr            *clause.Expr
    Table                string
    Model                interface{}
    Unscoped             bool
    Dest                 interface{}
    ReflectValue         reflect.Value
    Clauses              map[string]clause.Clause
    BuildClauses         []string
    Distinct             bool
    Selects              []string
    Omits                []string
    Joins                []join
    Preloads             map[string][]interface{}
    Settings             sync.Map
    ConnPool             ConnPool
    Schema               *schema.Schema
    // SQLステートメントごとのコンテキスト
    // SQLにタイムアウト値やキャンセル関数を設定できる
    Context              context.Context
    RaiseErrorOnNotFound bool
    SkipHooks            bool
    // SQLステートメント
    SQL                  strings.Builder
    Vars                 []interface{}
    CurDestIndex         int
    attrs                []interface{}
    assigns              []interface{}
    scopes               []func(*DB) *DB
}