7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

gorm

Posted at

下準備

  • gormのインストール。
$ go get -u gorm.io/gorm
  • 使用するドライバのインストール。
$ go get -u gorm.io/driver/sqlite #

モデル

gormにおけるモデルとは?

  • Goの基本型
  • (基本型の)ポインタ/エイリアス
  • Scanner インターフェイスを実装するカスタム型
  • Valuer インターフェイスを実装するカスタム型
    からなる通常の構造体のこと。

このうち Scanner、Valuerに関しては後々詳細な説明があるのでここでは解説しない。

ドキュメントのサンプルモデル

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}

規約

GORMの方針は「設定より規約」(CoC)です。
デフォルトでは、GORMは

  • 主キーとしてID
  • テーブル名には構造体を複数形にし、かつスネークケースにしたものを
  • カラム名にはフィールドをスネークケースにしたものを
  • 作成と更新の時間を自動トラッキングするためにCreatedAt、UpdatedAtフィールド

を利用します。

GORMで採用されている規約に従うと、設定やコードを記述する手間が大幅に減少します。
規約に関しては後々詳細な説明があるのでここでは解説しない。

gorm.Model構造体

GORMは gorm.Model 構造体を定義しています。これには ID, CreatedAt, UpdatedAt, DeletedAt のフィールドが含まれます。gorm.Modelを埋め込むことで、これらのフィールドを特定の構造体に埋め込むことができます。

gorm.Modelを使ったモデルの例

// gorm.Modelの定義
// type Model struct {
//  ID        uint           `gorm:"primaryKey"`
//  CreatedAt time.Time
//  UpdatedAt time.Time
//  DeletedAt gorm.DeletedAt `gorm:"index"`
//}

type User struct {
  gorm.Model
  Name string
}

フィールドレベルの権限

公開されているフィールドは、GORMでのCRUD操作が全て可能となっています。
権限の変更(読み取り専用、書き込み専用、作成専用、更新専用、除外するフィールドの指定)にはタグを使用します。

権限変更の例

type User struct {
  Name string `gorm:"<-:create"` // 読み取り、作成が可能
  Name string `gorm:"<-:update"` // 読み取り、更新が可能
  Name string `gorm:"<-"`        // 読み取り、書き込みが可能 (createとupdate)
  Name string `gorm:"<-:false"`  // 読み取り可能、書き込み無効
  Name string `gorm:"->"`        // 読み取り専用 (変更されない限り、書き込みが無効 )
  Name string `gorm:"->;<-:create"` // 読み取りと作成が可能
  Name string `gorm:"->:false;<-:create"` // 作成専用 (dbからの読み取りは無効)
  Name string `gorm:"-"`  // 構造体を使用した書き込みと読み込みの際にこのフィールドを無視する
  Name string `gorm:"migration"` // マイグレーション時にこのフィールドを無視する
}

作成・更新日時のトラッキング/Unix (ミリ・ナノ) 秒でのトラッキング

GORMの規約では、作成/更新時間をトラッキングするのに CreatedAt, UpdatedAt を使用します。それらのフィールドがモデルに定義されている場合、作成/更新時間に現在時刻を値として自動でセットします。

time.Timeの代わりにUNIX (ミリ/ナノ) 秒を保存したい場合、フィールドのデータ型を time.Time から int に変更するだけで保存が可能

別の名前のフィールドを使用する場合、 autoCreateTime、 autoUpdateTime タグを使用することで設定を変更することができます。

トラッキングの例

type User struct {
  CreatedAt time.Time // 作成時に値がゼロ値の場合、現在時間がセットされる
  UpdatedAt int       // 更新時、または作成時の値がゼロ値の場合、現在のUNIX秒がセットされる
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 更新時間としてUNIXナノ秒を使用する
  Updated   int64 `gorm:"autoUpdateTime:milli"`// 更新時間としてUNIX msを使用する
  Created   int64 `gorm:"autoCreateTime"`      // 作成時間としてUNIX秒を使用する
}

構造体の埋め込み

匿名フィールドによる定義

匿名フィールドでモデルの定義がされている場合、埋め込まれた構造体のフィールドは親の構造体のフィールドとして含まれることになります。

匿名フィールドによる定義の例

type User struct {
  gorm.Model
  Name string
}
// equals
type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name string
}

通常のフィールド定義による埋め込み

通常のフィールドで構造体の定義がなされている場合、 embedded タグを使用して構造体の埋め込みを行うことができます。
Q: 構造体のアクセス方法が変わるか確認。
A: 構造体自体は普通の構造体。

通常のフィールド定義による埋め込みの例

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// equals
type Blog struct {
  ID    int64
    Name  string
    Email string
  Upvotes  int32
}

埋め込み構造体にプレフィクスの追加

embeddedPrefix タグを使用することで、埋め込まれた構造体のフィールド名にプレフィックスを追加することができます。

埋め込み構造体にプレフィクスの例

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded;embeddedPrefix:author_"`
  Upvotes int32
}
// equals
type Blog struct {
  ID          int64
    AuthorName  string
    AuthorEmail string
  Upvotes     int32
}

フィールドに指定可能なタグ

タグは大文字小文字を区別しませんが、 camelCase が推奨されます。
タグ一覧

データベースへの接続

サポートしているDBMS

GORMは公式にMySQL、PostgreSQL、SQLite、SQL Server をサポートしています
ここDBへの接続方法は記載しません。公式の方を見てください。

ドライバーの変更

こういうことができるということのメモを意図して、一応コードを載せてますがDBMSごとに違うのでこちらも公式を見た方がいいです。

import (
  _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
  "gorm.io/gorm"
)

db, err := gorm.Open(postgres.New(postgres.Config{
  DriverName: "cloudsqlpostgres",
  DSN: "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable",
})

既存の接続を使用する

こういうことができるということのメモを意図して、一応コードを載せてますがDBMSごとに違うのでこちらも公式を見た方がいいです。

import (
  "database/sql"
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
)

sqlDB, err := sql.Open("pgx", "mydb_dsn")
gormDB, err := gorm.Open(postgres.New(postgres.Config{
  Conn: sqlDB,
}), &gorm.Config{})

コネクションプール

GORMは database/sql を使用してコネクションプールを維持しています。

コネクションの設定

sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)

レコードの作成

レコードを作成する

Create(モデルの参照)メソッドを使う

作成の例

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

result := db.Create(&user) // pass pointer of data to Create

user.ID             // returns inserted data's primary key
result.Error        // returns error
result.RowsAffected

作成するフィールドの指定

Selectメソッドに作成するカラム名を渡す。

作成するフィールドの指定の例

db.Select("Name", "Age", "CreatedAt").Create(&user)

省略するフィールドの指定

Omitメソッドで指定する。

省略するフィールドの指定例

db.Omit("Name", "Age", "CreatedAt").Create(&user)

一括作成

一括作成するためにはスライスをCreateに渡します。
これで全てのモデルを挿入する1つのSQL文を作成します。

一括作成の例

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)

バッチサイズを指定してレコードの作成

CreateInBatchesを使用します・
Upsert や Create With Associations を使用する場合もバッチインサートはサポートされています。

バッチサイズを指定してレコードの作成の例

var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}
// batch size 100
db.CreateInBatches(users, 100)

バッチサイズのデフォルト値の設置

CreateBatchSize オプションでGORMを初期化した場合、 INSERT メソッドはその設定を参照して、レコードを作成します。

バッチサイズのデフォルト値の設置の例

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)

作成時のフック

GORMでは、 BeforeSave, BeforeCreate, AfterSave, AfterCreateといったメソッドを実装することで、独自のHook処理を定義できます。 これらのメソッドはレコードを作成するときに呼び出されます

フック作成の例

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

    if u.Role == "admin" {
        return errors.New("invalid role")
    }
    return
}

フックのスキップ

スキップしたい場合は、 SkipHooks セッションモードを使用できます

スキップの例

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

マップを使ってレコードの作成

map[string]interface{} と []map[string]interface{}{}を使ってレコード作成できます
注意 Mapを使用してレコードを作成する場合、Hookは呼び出されません。また、関連テーブルのレコードも保存されず、主キーの値も返されません。

マップでレコード作成の例

db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

SQL式/Context Valuer で作成する

SQL式でデータを挿入することができます。これを行うには map[string]interface{} と Customized Data Types の2つの方法があります

map[string]interface{}の例

// Create from map
db.Model(User{}).Create(map[string]interface{}{
  "Name": "jinzhu",
  "Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"));

Customized Data Types の例

// Create from customized data type
type Location struct {
    X, Y int
}

// Scan implements the sql.Scanner interface
func (loc *Location) Scan(v interface{}) error {
  // Scan a value into struct from database driver
}

func (loc Location) GormDataType() string {
  return "geometry"
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
  return clause.Expr{
    SQL:  "ST_PointFromText(?)",
    Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
  }
}

type User struct {
  Name     string
  Location Location
}

db.Create(&User{
  Name:     "jinzhu",
  Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"))

関連データと一緒に作成

値がゼロ値でなければ関連データもupsertされます。またその時、関連データのHooksメソッドも実行されます。

関連データの作成例

type CreditCard struct {
  gorm.Model
  Number   string
  UserID   uint
}

type User struct {
  gorm.Model
  Name       string
  CreditCard CreditCard
}

db.Create(&User{
  Name: "jinzhu",
  CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...

関連付けのスキップ

Select, Omit を使うことで関連付けをスキップできます

関連付けスキップの例

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

// skip all associations
db.Omit(clause.Associations).Create(&user)

デフォルト値を指定する

defaultタグによって、フィールドのデフォルト値を定義できます
データベースへの挿入時にフィールドが ゼロ値 の場合、デフォルト値が使用されます

デフォルト値を指定する例

type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}

デフォルト値とゼロ値

デフォルト値を定義したときに、 0, '', falseのようなゼロ値はデータベースに保存されないため、これを避けるにはポインタ型やScanner/Valuerを使用する
Q: ここの意味がわからないので調べる。
A: ポインタ型やScanner/Valuerを使うと0やfalseも初期値に代入できる。

type User struct {
  gorm.Model
  Name string
  Age  *int           `gorm:"default:18"`
  Active sql.NullBool `gorm:"default:true"`
}

レコード上は存在しない値を定義する。

レコードには存在しないフィールドを含めるときにはdefault値を(-)なしに設定する。

type User struct {
  ID        string `gorm:"default:uuid_generate_v3()"` // db func
  FirstName string
  LastName  string
  Age       uint8
  FullName  string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}

コンフリクト発生時のUpsert

Columnsで競合比較するカラムを指定。
DoUpdateでアップデートするカラムを指定。

コンフリクト発生時のUpsert例

import "gorm.io/gorm/clause"

// 競合時に何もしない。
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// Update columns to default value on `id` conflict
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL
// Assignmentsはroleがユーザだったら更新ではなく。roleをuserに更新する。
// 他のフィールドは更新しない。

// Use SQL expression
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));
// sqlite3で実行したらエラーが出た。

// Update columns to new value on `id` conflict
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL
// 引数に渡したモデルのAssignmentColumnsに対応するカラムの値でフィールドを更新する。

// Update all columns, except primary keys, to new value on conflict
db.Clauses(clause.OnConflict{
  UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;

Columnsが競合比較用に使うキーでユニークもしくは主キーである必要がある。
Doupdateで更新するからむを指定する。

レコードの取得

単一のレコードを取得

GORMは、データベースから1つのオブジェクトを取得するためにFirst, Take, Lastメソッドを提供しています。それらのメソッドは、データベースにクエリを実行する際にLIMIT 1の条件を追加し、レコードが見つからなかった場合、ErrRecordNotFoundエラーを返します。
First: 主キーをもとに最初に合致したレコードを取得。並び替えても意味ない。
Take: Orderとかと組み合わせて並び替えた状態で最初のコードを取り出す。
Last: 主キーを基準にして最後の要素を取り出す。
検索条件に関してはユニークなカラムに関しては引数のモデルに検索条件を付与しての検索ができる。
そうでない場合はWhereを使って条件を付与する。

db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
db.Take(&user)
// SELECT * FROM users LIMIT 1;
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

取得結果件数の取得

result := db.First(&user)
result.RowsAffected // returns count of records found

取得結果のエラーチェック

result.Error        // returns error or nil
// check error ErrRecordNotFound
errors.Is(result.Error, gorm.ErrRecordNotFound)

エラーを出さずに1件取得する方法

ErrRecordNotFound エラーを避けたい場合は、db.Limit(1).Find(&user)のように、Find を 使用することができます。Find メソッドは struct と slice のどちらも受け取ることができます。

プライマリキーでオブジェクトを取得する。

主キーが数値の場合は、 Inline Conditions を使用することで、主キーでオブジェクトを取得できます。 文字列を扱う場合、SQLインジェクションを避けるために十分な注意が必要です。詳細については、 Security セクションを参照してください。

プライマリキーでオブジェクトを取得する例

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

全てのオブジェクトを取得する。

db.findで全てのオブジェクトが取得できる。

取得条件

条件の指定方法には

  • 文字列
  • 構造体
  • マップ
  • インライン条件がある。

Whereを使う(文字列のみ)

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

Whereを使う(構造体とマップ)

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

構造体とマップの指定違い

構造体を使ってクエリを実行するとき、GORMは非ゼロ値なフィールドのみを利用します。つまり、フィールドの値が 0, '', false または他の ゼロ値の場合、 クエリ条件の作成に使用されません。

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

クエリ条件にゼロ値を含めるには、次のように、クエリ条件としてすべてのkey-valuesを含むマップを使用できます。

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

検索に使う構造体のフィールドを指定する

構造体を使用して検索する場合、フィールド名かテーブルのカラム名を Where() に記載することで、構造体の特定の値のみをクエリ条件で使用することができます。 例えば:

db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;

インライン条件

FindやFirstの2つ目以降の引数に条件を追加できる。

インライン条件例

// Get by primary key if it were a non-integer type
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;

Not

// Get by primary key if it were a non-integer type
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;

OR

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

Select

db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;

Order

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{
  Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)

Limit Offset

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)

Group by & Having

type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
for rows.Next() {
  ...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
for rows.Next() {
  ...
}

type Result struct {
  Date  time.Time
  Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group

Distinct

db.Distinct("name", "age").Order("name, age desc").Find(&results)

Joins

type result struct {
  Name  string
  Email string
}

db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
  ...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", 

Joinsのプレローディング

単一クエリで関連データをeager loadingするのにJoinsを使用することができます。例:

db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

条件を指定して結合

db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true;

表の作成

type User struct {
    Id  int
    Age int
}

type Order struct {
    UserId     int
    FinishedAt *time.Time
}

query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id")
db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results)
// SELECT `order`.`user_id`,`order`.`finished_at` FROM `order` join (SELECT MAX(order.finished_at) as latest FROM `order` left join user user on order.user_id = user.id WHERE user.age > 18 GROUP BY `order`.`user_id`) q on order.finished_at = q.latest

Scan
レコード取得結果の構造体へのScanはFindの使う方法と同じです。

type Result struct {
  Name string
  Age  int
}

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)

// Raw SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

高度なクエリ

### 便利なフィールドの選択
FirstやFintに指定されたフィールドしか持たない構造体を追加することでSelectができる

type User struct {
  ID     uint
  Name   string
  Age    int
  Gender string
  // hundreds of fields
}

type APIUser struct {
  ID   uint
  Name string
}

// Select `id`, `name` automatically when querying
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10

QueryFields

TODO
調べる
QueryFieldsによって何が変わるのかがわからん。
Sessionモードもわからん

ロック

StrengthがUPDATEの時は占有ロック,Shareの時は共有ロックを行う。
NOWAITを指定すると対象がロック中のときにエラーを発生させる。

db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE

db.Clauses(clause.Locking{
  Strength: "SHARE",
  Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
// SELECT * FROM `users` FOR SHARE OF `users`

db.Clauses(clause.Locking{
  Strength: "UPDATE",
  Options: "NOWAIT",
}).Find(&users)
// SELECT * FROM `users` FOR UPDATE NOWAIT

サブクエリ

引数に*gorm.DBを渡すとサブクエリにしてくれる。

db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");

subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")

Fromを使ったサブクエリ

Tableを使う。

db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18}).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18

subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p

条件のグループ化

条件をメソッドチェーンする、もしくは引数に使うことで複雑な条件を作れる。

db.Where(
    db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
    db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{}).Statement

// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")

複数カラムをINを使って検索

db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
// SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));

名前付き引数

?を使わず名前をつけることができる

db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"

db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1

取得結果をマップに代入

このときModel()やTable()を呼ばないとどのデーブルかわからない。

result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)

var results []map[string]interface{}
db.Table("users").Find(&results)

FirstOrInit

取得できなければ特定の値で初期化する。

// ユーザを取得できないため、与えられた条件でユーザを初期化
db.FirstOrInit(&user, User{Name: "non_existing"})
// user -> User{Name: "non_existing"}

// `name` = `jinzhu` の条件でユーザを取得できた
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

// `name` = `jinzhu` の条件でユーザを取得できた
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

Attr

FirstOrInitで取得できなかったときの値を設定する。

// Userが見つからないため、取得条件とAttrsで指定された属性で構造体を初期化
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// Userが見つからないため、取得条件とAttrsで指定された属性で構造体を初期化
db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// `name` = `jinzhu` のUserが見つかったため、Attrsで指定された属性は無視される
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

Assign

FirstOrInitで取得できてもできなくても値を初期化する。

// Userが見つからないため、取得条件とAssignで指定された属性で構造体を初期化
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
// user -> User{Name: "non_existing", Age: 20}

// `name` = `jinzhu` のUserが見つかったため、取得レコードの値とAssignで指定された値で構造体を生成
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 20}

FirstOrCreate

Firstで取得できればその値を、できなければ条件で新しいレコードを作成
AttrとAssignも使える

// Userが見つからないため、指定された条件でレコードを作成する
db.FirstOrCreate(&user, User{Name: "non_existing"})
// INSERT INTO "users" (name) VALUES ("non_existing");
// user -> User{ID: 112, Name: "non_existing"}

// `name` = `jinzhu` のUserが見つかった
db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
// user -> User{ID: 111, Name: "jinzhu", "Age": 18}

オプティマイザヒントをあじゃこじゃ

これはDBエンジニアがやることで普通使わんじゃろ。

行ごとに処理

rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
defer rows.Close()

for rows.Next() {
  var user User
  // ScanRows is a method of `gorm.DB`, it can be used to scan a row into a struct
  db.ScanRows(rows, &user)

  // do something
}

FindInBatches

バッチ処理を行うのに使う。

// batch size 100
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
  for _, result := range results {
    // batch processing found records
  }

  tx.Save(&results)

  tx.RowsAffected // number of records in this batch

  batch // Batch 1, 2, 3

  // returns error will stop future batches
  return nil
})

result.Error // returned error
result.RowsAffected // processed records count in all batches

Pluck

1つのカラムだけから値を取得。

var ages []int64
db.Model(&users).Pluck("age", &ages)

var names []string
db.Model(&User{}).Pluck("name", &names)

db.Table("deleted_users").Pluck("name", &names)

// Distinct Pluck
db.Model(&User{}).Distinct().Pluck("Name", &names)
// SELECT DISTINCT `name` FROM `users`

Scope

クエリを合体させられる。詳細に説明する生姜ある。

Count

Count普通のCount

db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'

db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu'; (count)

db.Table("deleted_users").Count(&count)
// SELECT count(1) FROM deleted_users;

// Count with Distinct
db.Model(&User{}).Distinct("name").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`

db.Table("deleted_users").Select("count(distinct(name))").Count(&count)
// SELECT count(distinct(name)) FROM deleted_users

// Count with Group
users := []User{
  {Name: "name1"},
  {Name: "name2"},
  {Name: "name3"},
  {Name: "name3"},
}

db.Model(&User{}).Group("name").Count(&count)
count // => 3

レコードの更新

全てのフィールドを保存する。

Saveを使う

db.First(&user)

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

1つのフィールドを更新する。

Updateメソッドで単一のカラムを更新する際には、何らかの条件を持っていないと、ErrMissingWhereClauseエラーが発生します。Modelメソッドを使用し、その実体が主キーに値するフィールドを持つ場合、条件として実体が持つ主キーが指定されます。

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

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

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

複数のフィールドを更新する。

Updatesは構造体もしくはmap[string]interface{}での更新に対応しています。構造体での更新時のみ、ゼロ値のフィールド以外を更新します。

// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"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;

更新するフィールドを選択する。

Select,Omitを使う。
// Select with Map
// User's ID is 111:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

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

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

// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})


### 一括変更
Modelで主キーを指定しないと一括変更が行われます。

### Global Updates
条件を指定しないで一括更新しようとするとエラーを吐く。
AllowGlobalUpdateモードw有効にする必要がある。

### 更新されたレコード数の取得
RawsAffectedを使う

### 高度な機能
#### SQLでの更新
```golang
// product's ID is `3`
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;

db.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)})
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;

db.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;

db.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;

Context,Valuerを使って更新

// Create from customized data type
type Location struct {
X, Y int
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "ST_PointFromText(?)",
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}

db.Model(&User{ID: 1}).Updates(User{
Name: "jinzhu",
Location: Location{X: 100, Y: 100},
})
// UPDATE user_with_points SET name="jinzhu",location=ST_PointFromText("POINT(100 100)") WHERE id = 1


#### サブクエリで更新
```golang
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);

db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))

db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")})

Hooksやタイムトラッキングなしでの更新

Hooks メソッドの実行を回避したい場合や、更新時間をトラッキングしたくない場合、 Update、 Updatesと似た、UpdateColumn、 UpdateColumns を使用することができます。

// Update single column
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;

// Update multiple columns
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE id = 111;

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

変更されたデータを返却する

Returningをサポートしていれば可能

// すべてのカラムを返却する
var users []User
DB.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 指定のカラムのみ返却する
DB.Model(&users).Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

フィールドが変更されたかチェックする

BeforeUpdateHooksでChangedを使うことによって値が片越されたか確認できる。
UpdateとUpdatesが呼ばれた時だけ行われる

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  // if Role changed
  if tx.Statement.Changed("Role") {
    return errors.New("role not allowed to change")
  }

  if tx.Statement.Changed("Name", "Admin") { // if Name or Role changed
    tx.Statement.SetColumn("Age", 18)
  }

  // if any fields changed
  if tx.Statement.Changed() {
    tx.Statement.SetColumn("RefreshedAt", time.Now())
  }
  return nil
}

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"})
// Changed("Name") => false, `Name` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{
  "name": "jinzhu2", "admin": false,
})
// Changed("Name") => false, `Name` not selected to update

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"})
// Changed("Name") => false, `Name` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"})
// Changed("Name") => false, `Name` not selected to update

更新値の変更

Saveによる変更でなければSetColumnでフック内で更新値を変更できる。

レコードの削除

一件削除

主キーを指定すると1件。しないと条件に一致したものが全て削除される

/ Email's ID is `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// Delete with additional conditions
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

インライン削除

主キーが数値ならインライン条件で削除できる。

db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

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

Global delete

間違ってレコードを消さないように条件なしで全件削除はできないようになっている。

削除されたデータを返却

Returningをサポートしていれば返却される。

// すべてのカラムを返却する
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 指定のカラムのみ返却する
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

論理削除

gorm.DeletedAtフィールドがモデルに含まれているとき論理削除さえれるようになる。

Deleteメソッドで削除すると論理削除され普通のクエリ系のメソッドでは取得できなくなる。

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

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

// 論理削除されたレコードは取得処理時に無視されます
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

Gorm.Modelを使わず論理削除を使う


type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

削除されたレコードの削除

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;

タイムスタンプ削除フラグ

import "gorm.io/plugin/soft_delete"

type User struct {
  ID        uint
  Name      string
  DeletedAt soft_delete.DeletedAt
}

// Query
SELECT * FROM users WHERE deleted_at = 0;

// Delete
UPDATE users SET deleted_at = /* current unix second */ WHERE ID = 1;

削除フラグとインデックス

ユニークなキーとUnixタイムを使っているときは複合インデックスを貼るといいらしい。
TODO 理由を調べる。

Boolean削除フラグ

import "gorm.io/plugin/soft_delete"

type User struct {
  ID    uint
  Name  string
  IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}

// Query
SELECT * FROM users WHERE is_del = 0;

// Delete
UPDATE users SET is_del = 1 WHERE ID = 1;

素のSQLとSQLビルダー

RawSQL

Raw,ExecとScanを使うことによって素のSQLでレコードを取得できる。

type Result struct {
  ID   int
  Name string
  Age  int
}

var result Result
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)

db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)

var age int
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)

var users []User
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)

db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})

// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")

名前付き引数

Gormでは名前付き引数をサポートしています

db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"

db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3)
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.`id` LIMIT 1

// Named Argument with Raw SQL
db.Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
   sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user)
// SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"

db.Exec("UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
   sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2"))
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew"

db.Raw("SELECT * FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2",
   map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

type NamedArgument struct {
    Name string
    Name2 string
}

db.Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2",
     NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

Dry Run

実行せずにSQLとその引数の生成だけ行う。デバッグの時などに使う。詳細はSessoinを確認

stmt := db.Session(&Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars         //=> []interface{}{1}

ToSQL

SQLを返します。これによって生成されたSQLはデバッグのみに使用してください。

Row & Rows

結果を*sql.Rowとして取得するのでScanする必要があります。

// GORMのAPIを利用
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
row.Scan(&name, &age)

// SQL文を利用
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()
row.Scan(&name, &age, &email)

rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // do something
}

// 素のSQL
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // do something
}

openの後にDeferでcloseしてから処理をしている。

Connectionを使うことによって1回のTCP接続で複数のSQLを実行することができます。

Belongs To

Belongs To

モデルの各いんすたんすが他のモデルの1つのインスタンスに属するときに使う。
所属する側に所属先に所属先のIDと所属先のインスタンスを含めます。これは関連の作成に使われるので含まれる必要があります。

// `User`は`Company`に属します。 `CompanyID`は外部キーです。
type User struct {
  gorm.Model
  Name      string
  CompanyID int
  Company   Company
}

type Company struct {
  ID   int
  Name string
}

外部キーの設定を上書き

デフォルトは
所有する側のモデル名+ID
上書きするにはforeignKeyタグを使う

type User struct {
  gorm.Model
  Name         string
  CompanyRefer int
  Company      Company `gorm:"foreignKey:CompanyRefer"`
  // CompanyReferを外部キーとして利用する。
}

type Company struct {
  ID   int
  Name string
}

参照フィールドの上書き

所有される側のモデルの外部キーに対応するデフォルトのフィールドは所有側のIDであるが上書きするにはreferenceを使う

type User struct {
  gorm.Model
  Name      string
  CompanyID string
  Company   Company `gorm:"references:Code"` // Codeを参照として使用する
}

type Company struct {
  ID   int
  Code string
  Name string
}

外部キー制約

ConstraintでOnupdate,OnDeleteの制約かけることができる。

Has One

あるモデルが別のモデルを1つ含んでいるときに使用する。

// Userは1つだけCreditCardを持ちます。CreditCardIDは外部キーです。
type User struct {
  gorm.Model
  CreditCard CreditCard
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

外部キーのデフォルト設定を上書きする。

foreignKeyを使う

type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"foreignKey:UserName"`
  // UserNameを外部キーとして利用する。
}

type CreditCard struct {
  gorm.Model
  Number   string
  UserName string
}

参照フィールドのデフォルト設定を上書きする

type User struct {
  gorm.Model
  Name       string     `gorm:"index"`
  CreditCard CreditCard `gorm:"foreignkey:UserName;references:name"`
}

type CreditCard struct {
  gorm.Model
  Number   string
  UserName string
}

ポリモーフィズム アソシエーション

複数のモデルで共有するのに使う。
この例では犬と猫がそれぞれおもちゃを持っている。

type Cat struct {
  ID    int
  Name  string
  Toy   Toy `gorm:"polymorphic:Owner;"`
}

type Dog struct {
  ID   int
  Name string
  Toy  Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
  ID        int
  Name      string
  OwnerID   int
  OwnerType string
}

db.Create(&Dog{Name: "dog1", Toy: Toy{Name: "toy1"}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs")

自己参照もできる。

type User struct {
gorm.Model
Name string
ManagerID *uint
Manager *User
}

外部キー制約

Ondelete,OnUpdateで制約をかけかれる。

Has Many

1:nの関係を作る。

// User は複数の CreditCards を持ちます。UserID は外部キーとなります。
type User struct {
  gorm.Model
  CreditCards []CreditCard
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

他のアソシエーション同様foreignKeyとreferenceで設定を変更できる。
ポリモーフィズムアソシエーションもできる。
自己参照もできる。
外部キーによる制約もできる。

Many To Many

n:nの関係

// User は複数の言語を所有し、かつ言語に属しています。`user_languages` が結合テーブルになります
type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}

参照キーの設定、
外部キーの設定、
自己参照、
外部キー制約が使える。

後方参照

A->B,A<-Bどちらも山椒を持つパターン

// User は複数の言語を所有し、かつ言語に属しています。`user_languages` が結合テーブルになります
type User struct {
  gorm.Model
  Languages []*Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
  Users []*User `gorm:"many2many:user_languages;"`
}

結合テーブルのカスタマイズ

Join Table(結合テーブル)は、Soft Delete、Hooksなどをサポートしたり、外部キー以外のフィールドを持つようなフル機能のモデルとして定義できます。これの設定には SetupJoinTable を使用します。
注意: 結合テーブルをカスタマイズする場合、結合テーブルの外部キーを複合主キーにする、あるいは外部キーに複合ユニークインデックスを貼る必要があります。

type Person struct {
  ID        int
  Name      string
  Addresses []Address `gorm:"many2many:person_addresses;"`
}

type Address struct {
  ID   uint
  Name string
}

type PersonAddress struct {
  PersonID  int `gorm:"primaryKey"`
  AddressID int `gorm:"primaryKey"`
  CreatedAt time.Time
  DeletedAt gorm.DeletedAt
}

func (PersonAddress) BeforeCreate(db *gorm.DB) error {
  // ...
}

// PersonモデルのAddressフィールドの結合テーブルをPersonAddressに変更する
// PersonAddressには必要な外部キーが全て定義されていなければならず、定義されていない場合はエラーとなる
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})

アソシエーション

アソシエーションの自動作成/更新

upsertで関連の挿入もしくは更新を行う。

user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
  Emails:          []Email{
    {Email: "jinzhu@example.com"},
    {Email: "jinzhu-2@example.com"},
  },
  Languages:       []Language{
    {Name: "ZH"},
    {Name: "EN"},
  },
}

db.Create(&user)
// BEGIN TRANSACTION;
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "languages" ("name") VALUES ('ZH'), ('EN') ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "user_languages" ("user_id","language_id") VALUES (111, 1), (111, 2) ON DUPLICATE KEY DO NOTHING;
// COMMIT;

db.Save(&user)

すでに存在するアソシエーションを更新したいときにはFullSaveAsscosiationを使う。

db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY SET address1=VALUES(address1);
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY SET email=VALUES(email);
// ...

アソシエーションの自動作成/更新をスキップ

SelectもしくはOmitを使用

user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
  Emails:          []Email{
    {Email: "jinzhu@example.com"},
    {Email: "jinzhu-2@example.com"},
  },
  Languages:       []Language{
    {Name: "ZH"},
    {Name: "EN"},
  },
}

db.Select("Name").Create(&user)
// INSERT INTO "users" (name) VALUES ("jinzhu", 1, 2);

db.Omit("BillingAddress").Create(&user)
// ユーザ作成時に BillingAddress の作成をスキップする

db.Omit(clause.Associations).Create(&user)
// ユーザ作成時に全てのアソシエーションの保存をスキップする

アソシエーションフィールドの選択/省略

user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1", Address2: "addr2"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1", Address2: "addr2"},
}

// Create user and his BillingAddress, ShippingAddress
// When creating the BillingAddress only use its address1, address2 fields and omit others
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)

db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)

アソシエーションモード

// Start Association Mode
var user User
db.Model(&user).Association("Languages")
// `user` は大元のモデルであるため、主キーを含んでいる必要があります
// `Languages` は関連フィールドの名前です
// 上記2つの条件が揃うと、 AssociationMode は正常に開始されます。条件が揃っていない場合はエラーが返却されます。
db.Model(&user).Association("Languages").Error

取得

db.Model(&user).Association("Languages").Find(&languages)
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)

db.Model(&user).Where("code IN ?", codes).Order("code desc").Association("Languages").Find(&languages)

追加

db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})

db.Model(&user).Association("Languages").Append(&Language{Name: "DE"})

db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})

更新

db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})

db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)

削除

db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)

この場合削除されるのは三章だけです。

全削除

db.Model(&user).Association("Languages").Clear()

一括処理

// 全てのユーザの役割を全て取得する
db.Model(&users).Association("Role").Find(&roles)

// 全ユーザのチームからユーザAを削除する
db.Model(&users).Association("Team").Delete(&userA)

// 重複を取り除いた全ユーザのチームの件数を取得する
db.Model(&users).Association("Team").Count()

// 一括処理で `Append` や `Replace` を使用する場合は、それらの関数の引数とデータの数(以下でいう users の数)が一致している必要があります。
// 一致していない場合はエラーが返却されます。
var users = []User{user1, user2, user3}

// 例1: 3人のユーザABCを新たに追加するとした場合、以下のコードでは user1のチームにユーザA、user2のチームにユーザB、user3のチームにユーザABCを全員追加します
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})

// 例2: user1のチームをユーザAのみに、user2のチームをユーザBのみに、user3のチームをユーザABCのみにそれぞれリセットします
db.Mode

指定して削除

レコード削除時に Select を使用することで、has one / has many / many2many 関係にある関連も同時に削除することができます

// ユーザ削除時に ユーザのアカウントも削除します
db.Select("Account").Delete(&user)

// ユーザ削除時に ユーザの注文とクレジットカードの関連レコードも削除します
db.Select("Orders", "CreditCards").Delete(&user)

// ユーザ削除時に ユーザ全ての has one / has many / many2many の関連レコードも削除します
db.Select(clause.Associations).Delete(&user)

// 複数ユーザ削除時に それぞれのユーザのアカウントも削除します
db.Select("Account").Delete(&users)

Eager loading

Preload

Preloadを使うことで別のSQLを発行して関連レコードをeager loadingする小牛田できます。

type User struct {
  gorm.Model
  Username string
  Orders   []Order
}

type Order struct {
  gorm.Model
  UserID uint
  Price  float64
}

// Preload Orders when find users
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4);

db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one
// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to

JoinsによるPreloading

Preload はアソシエーションデータを別々のクエリでロードします。 Join Preload は内部結合を使用してアソシエーションデータをロードします。

db.Joins("Company").Joins("Manager").Joins("Account").First(&user, 1)
db.Joins("Company").Joins("Manager").Joins("Account").First(&user, "users.name = ?", "jinzhu")
db.Joins("Company").Joins("Manager").Joins("Account").Find(&users, "users.id IN ?", []int{1,2,3,4,5})

Joinsで条件を指定して結合

注意 Join Preload 1  1 関係にあるリレーションで動作します例えば has one, belongs to がそれにあたります

注意 Join Preload は、1 対 1 関係にあるリレーションで動作します。例えば has one, belongs to がそれにあたります。

Preload All

レコード作成/更新時の Select で指定するのと同様に、 Preload でも clause.Associations を指定することができます。全ての関連レコードを Preload する際にこれを使用することができます

type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company
  Role       Role
  Orders     []Order
}

db.Preload(clause.Associations).Find(&users)

条件つきのPreload

使い方はInline conditionと同じになります。

// Preload Orders with conditions
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');

db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');

PreloadのSqlをカスタマしず

db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
  return db.Order("orders.amount DESC")
}).Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC;

Nested Preloading

db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)

// Customize Preload conditions for `Orders`
// `Orders` の Preload をカスタマイズして、
// 条件に一致しない `Orders` の `OrderItems` を preloadしないようにする
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)

Context

シングルセッション

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

継続セッション

tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")

Hooks/CallBacksでのコンテキスト

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  ctx := tx.Statement.Context
  // ...
  return
}

エラーハンドリング

基本的にgormではFinisherMethodsを実行した後にはエラーチェックすべき。
gormではgorm.DBのErrorフィールドにエラーが格納されるのでその値をチェックします。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
  // ここでエラーハンドリング
}
```golang
if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {
  // ここでエラーハンドリング
}

エラーの種類をチェックするにはerrors.Isを使う

// RecordNotFound エラーが返却されたかチェックする
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

メソッドチェーン

gormでは基本的にメソッドチェーンで処理を記述していきます。
そしてチェーンしていくメソッドは3種類に分けられます。

  • Chain Method
  • Finisher Method
  • New Session Method

があります。

Chain MEthod

Chain Methodsは現在のStatementを変更したり、Clausesを追加するメソッドです。

Where, Select, Omit, Joins, Scopes, Preload, Raw (Raw はSQLを生成する他のメソッドとは共用不可)… などがあります。
(一覧)[https://github.com/go-gorm/gorm/blob/master/chainable_api.go]

FinisherMethod

Finisher Methodは登録されたコールバックを即時に実行するメソッドであり、この種類のメソッドが呼び出されるとSQLを生成して実行します。

Create, First, Find, Take, Save, Update, Delete, Scan, Row, Rows… などがあります。

(一覧)[https://github.com/go-gorm/gorm/blob/master/finisher_api.go]

New Session Mode

*gorm.DBが初期化された、あるいは、New Session Methodが実行された場合、 その後のメソッド呼び出しは、現在の Statement インスタンスを使用せずに新しいインスタンスを作成します。

GROMは Session, WithContext, Debug を New Session Method として定義しています。

メソッドチェーンの例

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db は新規に初期化された *gorm.DB であるため、 `New Session Mode` に該当します
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
// `Where("name = ?", "jinzhu")` が最初のメソッド呼び出しとなり、新規の `Statement` を作成します
// `Where("age = ?", 18)` は条件を追加した `Statement` を返却します
// `Find(&users)` はFinisher Methodであるため、登録されたクエリコールバックを呼び出し、以下のSQLを生成・実行します:
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18;

db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
// `Where("name = ?", "jinzhu2")` が最初のメソッド呼び出しとなり、これも新規の `Statement` を作成します
// `Where("age = ?", 20)` は条件を追加した `Statement` を返却します
// `Find(&users)` はFinisher Methodであるため、登録されたクエリコールバックを呼び出し、以下のSQLを生成・実行します:
// SELECT * FROM users WHERE name = 'jinzhu2' AND age = 20;

db.Find(&users)
// `Find(&users)` はFinisher Methodであり、これが `New Session Mode` の `*gorm.DB` の最初のメソッド呼び出しとなります。
// 新しい `Statement` を作成し、登録されたクエリコールバックを呼び出し、以下のSQLを生成・実行します:
// SELECT * FROM users;

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db は新規に初期化された *gorm.DB であるため、 `New Session Mode` に該当します
tx := db.Where("name = ?", "jinzhu")
// `Where("name = ?", "jinzhu")` が最初のメソッド呼び出しとなり、新規の `Statement` を作成し条件を追加します

tx.Where("age = ?", 18).Find(&users)
// `tx.Where("age = ?", 18)` は上記の `Statement` を再利用し, 同じ `Statement` に条件を追加します
// `Find(&users)` はFinisher Methodであるため、登録されたクエリコールバックを呼び出し、以下のSQLを生成・実行します:
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18

tx.Where("age = ?", 28).Find(&users)
// `tx.Where("age = ?", 18)` は上記の `Statement` を再利用し, 同じ `Statement` に条件を追加します
// `Find(&users)` はFinisher Methodであるため、登録されたクエリコールバックを呼び出し、以下のSQLを生成・実行します:
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18 AND age = 28;

安全なメソッドチェーンと安全なgoroutine

新規に初期化された *gorm.DB や New Session Method の後においては、メソッドを呼び出すと新しい Statement が生成されます。*gorm.DBを再利用する際は、 New Session Mode であることを確認する必要があります

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

// 新規に初期化された *gorm.DB であるため安全である
for i := 0; i < 100; i++ {
  go db.Where(...).First(&user)
}

tx := db.Where("name = ?", "jinzhu")
// Statementを使いまわしているため、安全ではない
for i := 0; i < 100; i++ {
  go tx.Where(...).First(&user)
}

ctx, _ := context.WithTimeout(context.Background(), time.Second)
ctxDB := db.WithContext(ctx)
// `New Session Method` の後であるため、安全である
for i := 0; i < 100; i++ {
  go ctxDB.Where(...).First(&user)
}

ctx, _ := context.WithTimeout(context.Background(), time.Second)
ctxDB := db.Where("name = ?", "jinzhu").WithContext(ctx)
//  `New Session Method` の後であるため、安全である
for i := 0; i < 100; i++ {
  go ctxDB.Where(...).First(&user) // `name = 'jinzhu'` が適用される
}

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
//  `New Session Method` の後であるため、安全である
for i := 0; i < 100; i++ {
  go tx.Where(...).First(&user) // `name = 'jinzhu'` が適用される
}

Session

GORMは Session メソッドを提供しています。これは 新しいセッションを作成するメソッドであり、設定可能な新しいセッションを作成することができます。

Dry Run

実行はせずに SQL の生成のみ行います。 生成されたSQLを前もって準備またはテストする際に使用することができます

PrepareStmt

PreparedStmt は任意の SQL を実行する際にプリペアードステートメントを作成し、後の呼び出しを高速化するためにそれをキャッシュします。

NewDB

NewDBオプションを指定せずに新しいDBオブジェクトを作成します

Skip Hooks

Hooks メソッドをスキップしたい場合は、 SkipHooks セッションモードを使用できます

DisableNestedTransaction

トランザクション内で Transaction メソッドを使用した場合、GORMはネストしたトランザクションをサポートするため、 SavePoint(savedPointName), RollbackTo(savedPointName) を使用します。 DisableNestedTransaction オプションを使用してそれを無効にすることができます。

AllowGlobalUpdate

GORMはデフォルトで全データのグローバルな更新/削除を許可しておらず、処理を行おうとした場合、ErrMissingWhereClause エラーを返します。 AllowGlobalUpdateオプションをtrueに設定することで、その処理を許可することができます。

FullSaveAssociations

GORMはレコードの作成・更新時にUpsertを使用して自動的にアソシエーションとその参照を保存します。 アソシエーションデータを更新したい場合は、 FullSaveAssociations モードを使用する必要があります。

Context

Context オプションを使用することで、以降のSQL操作を実行するための Context を設定できます。

Logger

Gormでは、 Logger オプションを使用してビルトインのロガーをカスタマイズできます。

NowFunc

NowFuncを指定すると、現在時刻を取得する関数を変更することができます。

Debug

Debug はセッションの Logger をデバッグモードに変更するためのショートカットメソッドです。定義は次の通りです

QueryFields

このオプションを使用すると、各フィールドを指定して選択します。

CreateBatchSize

デフォルトのバッチサイズを指定できます。

Hooks

Hooks は 作成/取得/更新/削除 処理の前後に呼び出される関数です。

指定のメソッドをモデルに対して定義すると、作成・更新・取得・削除時にそのメソッドが自動的に呼び出されます。 定義したメソッドが返した場合、GORMは以降の操作を中止し、トランザクションをロールバックします。

func(*gorm.DB) error が Hooks メソッドの型となります。

力尽きたいつか続きをする。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?