444
319

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.

GOのORMを分かりやすくまとめてみた【GORM公式ドキュメントの焼き回し】

Last updated at Posted at 2019-05-06

はじめに

GORMは公式ドキュメントがすごく良いのですが、途中から分かりづらかったり日本語訳が途切れたりしていたので、自分の理解向上のついでに構成を分かりやすくし全て日本語でまとめました。
よって、本記事は公式ドキュメントの焼き回しになります。

前提知識

  • ORMとは何かくらいは知っている
  • SQLの基本知識
  • Goの基本知識

インストール

$ go get -u github.com/jinzhu/gorm

DB接続

各種DBMSの接続方法

Open関数でDB接続します。DBMSの種類によって、引数の与え方が若干異なります。
また、DBドライバをラップしたパッケージをimportする必要があります。

MySQL

import _ "github.com/jinzhu/gorm/dialects/mysql"
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")

PostgreSQL

import _ "github.com/jinzhu/gorm/dialects/postgres"
db, err := gorm.Open("postgres", "host=myhost port=myport user=gorm dbname=gorm password=mypassword")

Sqlite3

import _ "github.com/jinzhu/gorm/dialects/sqlite"
db, err := gorm.Open("sqlite3", "/tmp/gorm.db")

SQL Server

import _ "github.com/jinzhu/gorm/dialects/mssql"
db, err := gorm.Open("mssql", "sqlserver://username:password@localhost:1433?database=dbname")

その他接続設定

// SetMaxIdleConnsはアイドル状態のコネクションプール内の最大数を設定します
db.DB().SetMaxIdleConns(10)

// SetMaxOpenConnsは接続済みのデータベースコネクションの最大数を設定します
db.DB().SetMaxOpenConns(100)

// SetConnMaxLifetimeは再利用され得る最長時間を設定します
db.DB().SetConnMaxLifetime(time.Hour)

モデル

モデルの基本

モデルはテーブルを構造体で表現したものです。
データ操作では、モデルに取得した情報を詰め込めんだり、更新したい情報をモデルに詰め込んで使用したりします。GORMの核となる部品です。
本記事では以下のUserモデルがたびたび具体例で扱われます。

type User struct {
  gorm.Model
  Name         string
  Age          sql.NullInt64
  Birthday     *time.Time
  Email        string  `gorm:"type:varchar(100);unique_index"`
  Role         string  `gorm:"size:255"` // フィールドサイズを255にセットします
  MemberNumber *string `gorm:"unique;not null"` // MemberNumberをuniqueかつnot nullにセットします
  Num          int     `gorm:"AUTO_INCREMENT"` // Numを自動インクリメントにセットします
  Address      string  `gorm:"index:addr"` // `addr`という名前のインデックスを作ります
  IgnoreMe     int     `gorm:"-"` // このフィールドは無視します
}

gorm.Model

gorm.ModelID, CreatedAt, UpdatedAt, DeletedAtをフィールドに持つ構造体です。

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

IDフィールドはGORMにおいて特別な意味を持ちます。全てのIDは自動で主キーとして扱われます。

CreatedAtフィールドはレコードが初めて作成された時に自動で設定されます。

db.Create(&user) // `CreatedAt`には現在時刻が設定されます

UpdatedAtフィールドはレコードが更新された時に自動で設定されます。

db.Save(&user) // `UpdatedAt`に現在時刻が設定されます

DeletedAtフィールドはレコードが削除された時に自動で設定されます。

db.Delete(&user) // `DeletedAt`に現在時刻が設定されます

Goでは構造体を入れ子で定義できるので、gorm.Modelを独自のモデルに組み込めば、これらのフィールドを自前で定義する必要はありません。

type User struct {
  gorm.Model
  Name string
}

もちろん、自前で定義してもよいです。

type User struct {
  ID   int
  Name string
}

タグ

GORM用のタグ(gorm:)を使って主キーやら制約やらを色々設定できます。
タグの一覧はこちらを参照ください。

また、複数のフィールドにprimary_keyを指定すると複合主キーになります。

type Product struct {
    ID           string `gorm:"primary_key"`
    LanguageCode string `gorm:"primary_key"`
    Code         string
    Name         string
}

モデル間の関係

Belongs To

Belongs Toは自分のテーブルが対象テーブルのレコードに所属する関係です。
下記は、ProfileUserに対してBelongs To(所属している)例です。
Profileにとって、Userはなくてはならない存在です。
UserIDを外部キーにすることでモデル間の紐付けをしています。

type User struct {
  gorm.Model
  Name string
}

// `Profile` belongs to `User`, `UserID` is the foreign key
type Profile struct {
  gorm.Model
  UserID int
  User   User
  Name   string
}

Has One

Has Oneは自分のテーブルが対象テーブルを1つ持っている関係です。
下記は、UserCreditCardを1つ持っている例です。
UserにとってCreditCardはなくてはならないという存在ではありません。(CreditCardを持っていないUserもありえる場合)
CreditCardのIDが外部キーになっています。

// User has one CreditCard, CreditCardID is the foreign key
type CreditCard struct {
  gorm.Model
  Number   string
  UserID   uint
}

type User struct {
  gorm.Model
  CreditCard   CreditCard
}

Has Many

Has Manyは自分のテーブルが対象テーブルを0以上(複数)持っている関係です。
下記は、UserCreditCardを複数持っている例です。スライスになっています。

// User has many CreditCards, UserID is the foreign key
type User struct {
  gorm.Model
  CreditCards []CreditCard
}

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

Many To Many

Many To Manyは自分のテーブルと対象のテーブルが複対複の関係です。
下記は、あるUserが複数のLanguageを話せ、複数のUserがあるLanguageを話す例です。

// User has and belongs to many languages, use `user_languages` as join table
type User struct {
  gorm.Model
  Languages         []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}

マイグレーション

自動マイグレーション

モデル定義に合わせた自動マイグレーション機能が用意されています。

db.AutoMigrate(&User{})

しかし、プロダクションでしっかり使うには不十分です。
理由としては、不足しているカラムやインデックスの生成はするが、カラムの削除まではやってくれないからです。
Goには他にマイグレーションライブラリが豊富に用意されていますので、そちらを利用しましょう。

スキーマ操作

テーブル系

テーブルの存在確認

// `User`モデルのテーブルが存在するかどうか確認します
db.HasTable(&User{})

// `usersテーブルが存在するかどうか確認します
db.HasTable("users")

テーブルの作成

// `User`モデルのテーブルを作成します
db.CreateTable(&User{})

// `users`テーブル作成時に、SQL文に`ENGINE=InnoDB`を付与します
db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{})

テーブルの削除

// `User`モデルのテーブルを削除します
db.DropTable(&User{})

// `users`テーブルを削除します
db.DropTable("users")

// `User`モデルのテーブルと`products`テーブルを削除します
db.DropTableIfExists(&User{}, "products")

カラム系

カラムの型変更

// `User`モデルのdescriptionカラムのデータ型を`text`に変更します
db.Model(&User{}).ModifyColumn("description", "text")

カラムの削除

// `User`モデルのdescriptionカラムを削除します
db.Model(&User{}).DropColumn("description")

インデックス系

インデックスの追加

// `name`カラムのインデックスを`idx_user_name`という名前で追加します
db.Model(&User{}).AddIndex("idx_user_name", "name")

// `name`,`age`のインデックスを`idx_user_name_age`という名前で追加します
db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age")

// ユニークインデックスを追加します
db.Model(&User{}).AddUniqueIndex("idx_user_name", "name")

// 複数カラムのユニークインデックスを追加します
db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age")

インデックスの削除

// インデックスを削除します
db.Model(&User{}).RemoveIndex("idx_user_name")

外部キー系

外部キーの追加

// 外部キーを追加します
// パラメータ1 : 外部キー
// パラメータ2 : 対象のテーブル(id)
// パラメータ3 : ONDELETE
// パラメータ4 : ONUPDATE
db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT")

外部キーの削除

db.Model(&User{}).RemoveForeignKey("city_id", "cities(id)")

テーブル名のルール

モデル名の複数形

モデルからマイグレーションする場合は、デフォルトでモデル名の複数形がテーブル名になります。

type User struct {} // `デフォルトのテーブル名は`users`です

複数形の設定をやめたい場合は以下で無効化できます。

// テーブル名の複数形化を無効化します。trueにすると`User`のテーブル名は`user`になります
db.SingularTable(true)

テーブル名を明示的に指定

CreateTable関数使用時にTable関数で明示的にテーブル名を指定することができます。

// User構造体の定義を使って`deleted_users`テーブルを作成します
db.Table("deleted_users").CreateTable(&User{})

テーブル名命名規則を指定

DefaultTableNameHandler関数でデフォルトのテーブル名を設定できます。
以下は、テーブル名の先頭にprefix_という文字列を付与する例です。

gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
    return "prefix_" + defaultTableName;
}

カラム名のルール

モデルからマイグレーションする場合は、デフォルトでカラム名はモデルのフィールド名のスネーク形式になります。

type User struct {
  ID        uint      // カラム名は`id`
  Name      string    // カラム名は`name`
  Birthday  time.Time // カラム名は`birthday`
  CreatedAt time.Time // カラム名は`created_at`
}

明示的にカラム名を設定する場合はタグで指定できます。

// カラム名の上書き
type Animal struct {
    AnimalId    int64     `gorm:"column:beast_id"`         // カラム名を`beast_id`に設定します
    Birthday    time.Time `gorm:"column:day_of_the_beast"` // カラム名を`day_of_the_beast`に設定します
    Age         int64     `gorm:"column:age_of_the_beast"` // カラム名を`age_of_the_beast`に設定します
}

データ操作(CRUD)

INSERT

Create関数でデータを挿入します。

var animal = Animal{Age: 99, Name: ""}
db.Create(&animal)
// INSERT INTO animals("age") values('99');

SELECT

様々な取得方法

取得方法が複数用意されています。
取得した情報は引数で与えたモデルに格納されます。

(取得方法①)全てのレコードを取得

Find関数で全レコードを取得します。

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

(取得方法②)単一値をカラム指定で取得(Select)

Select関数でカラムを指定します。
Table関数でテーブルを指定することもできます。

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

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

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

(取得方法③)複数値をカラム指定で取得(Pluck)

Pluck関数で任意のカラムのスライスを取得します。

var ages []int64
db.Model(&User{}).Pluck("age", &ages)

(取得方法④)最初のレコードを取得(ソート)

First関数で主キーでソートされた最初のレコードを一行取得します。
主キー以外でソートして一行取得したいのであれば、Order関数とTake関数を組み合わせる。

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

(取得方法⑤)最後のレコードを取得(ソート)

Last関数で主キーでソートされた最後のレコードを一行取得します。

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

(取得方法⑥)最初のレコードを取得(ソートなし)

Take関数でソートなしで最初のレコードを一行取得します。

db.Take(&user)
//// SELECT * FROM users LIMIT 1;

(取得方法⑦)プリロード

プリロードを利用すれば、1センテンスで複数のテーブルからデータを取得できます。
モデル間の関係を持っていることが前提になります。
関係を持っていると自動でプリロード対象になるため、プリロードしたくない場合は、gorm:"PRELOAD:false"をタグします。

type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company `gorm:"PRELOAD:false"` // not preloaded
  Role       Role                           // preloaded
}

最も単純なプリロードの例は下記です。

db.Preload("Orders").Find(&users)
//// SELECT * FROM users;
//// SELECT * FROM orders WHERE user_id IN (取得したusersのID);

プリロード対象に対して抽出条件を付ける場合は、下記のようになります。

db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
//// SELECT * FROM users;
//// SELECT * FROM orders WHERE user_id IN (取得したusersのID) 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 (取得したusersのID) AND state NOT IN ('cancelled');

3つプリロードする場合は、下記のようになります。

db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
//// SELECT * FROM users;
//// SELECT * FROM orders WHERE user_id IN (取得したusersのID); // has many
//// SELECT * FROM profiles WHERE user_id IN (取得したusersのID); // has one
//// SELECT * FROM roles WHERE id IN (取得したusersのロールID); // belongs to

WHERE

Where関数で条件を指定します。
プレースホルダを使えます。

// 条件に一致した最初のレコードを取得します
db.Where("name = ?", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name = 'jinzhu' limit 1;

// 条件に一致したすべてのレコードを取得します
db.Where("name = ?", "jinzhu").Find(&user)
//// SELECT * FROM users WHERE name = 'jinzhu';

// <>
db.Where("name <> ?", "jinzhu").Find(&user)

// IN
db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&user)
db.Where([]int64{20, 21, 22}).Find(&users)
//// SELECT * FROM users WHERE id IN (20, 21, 22);

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&user)

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&user)

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&user)

構造体やマップをWhereに指定すると、そのまま条件として扱われます。

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

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

Not

db.Not("name", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;

Or

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

ソート

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;

// ReOrder
db.Order("age desc").Find(&users1).Order("age", true).Find(&users2)
//// SELECT * FROM users ORDER BY age desc; (users1)
//// SELECT * FROM users ORDER BY age; (users2)

Limit

Limit関数で取得件数を指定します。

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)

Offset

Offset関数で取得レコードの先頭いくつをスキップするかを指定します。」

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

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

Count

Count関数で取得レコード数を取得します。

db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users)
//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count)

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

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

Group

Group関数で指定カラムでのグループ化します。

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

Having

Having関数でグループ化したものを条件判定します。

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("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)

FirstOrInit

FirstOrInit関数で、指定した条件でレコードが存在していた場合は最初のレコードを取得しモデルを初期化します。存在しなければその条件でモデルを初期化します。

// Found
db.Where(User{Name: "Jinzhu"}).FirstOrInit(&users)
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}

// Unfound
db.FirstOrInit(&user, User{Name: "non_existing"})
//// user -> User{Name: "non_existing"}

存在しなかった場合に、追加する情報を増やす場合はAttrs関数を併用します。

// Unfound
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&users)
//// SELECT * FROM USERS WHERE name = 'non_existing';
//// user -> User{Name: "non_existing", Age: 20}

db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&users)
//// SELECT * FROM USERS WHERE name = 'non_existing';
//// user -> User{Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&users)
//// SELECT * FROM USERS WHERE name = jinzhu';
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}

また、存在するしないに関わらずモデルを設定する場合はAssign関数を使用します。

// Unfound
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&users)
//// user -> User{Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&users)
//// SELECT * FROM USERS WHERE name = jinzhu';
//// user -> User{Id: 111, Name: "Jinzhu", Age: 30}

FirstOrCreate

FirstOrCreate関数で、指定した条件でレコードが存在していた場合は最初のレコードを取得しモデルを初期化します。存在しなければその条件でレコードを保存します。

// Found
db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&users)
//// user -> User{Id: 111, Name: "Jinzhu"}

// Unfound
db.FirstOrCreate(&users, User{Name: "non_existing"})
//// INSERT INTO "users" (name) VALUES ("non_existing");
//// user -> User{Id: 112, Name: "non_existing"}

存在しなかった場合に、追加する情報を増やす場合はAttrs関数を併用します。

// Unfound
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&users)
//// SELECT * FROM users WHERE name = 'non_existing';
//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
//// user -> User{Id: 112, Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&users)
//// SELECT * FROM users WHERE name = 'jinzhu';
//// user -> User{Id: 111, Name: "jinzhu", Age: 20}

また、存在するしないに関わらずレコードを挿入あるいは更新する場合はAssign関数を使用します。
いわゆるpostgresqlのUPSERTを処理したい場合はこれが該当します。

// Unfound
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&users)
//// SELECT * FROM users WHERE name = 'non_existing';
//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
//// user -> User{Id: 112, Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&users)
//// SELECT * FROM users WHERE name = 'jinzhu';
//// UPDATE users SET age=30 WHERE id = 111;
//// user -> User{Id: 111, Name: "jinzhu", Age: 30}

取得結果に対しての処理

RowとRows

取得結果は*sql.Row*sql.Rowsとして取得できます。

row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row)
row.Scan(&name, &age)

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

また、sql.Rowsをモデルに変換することもできます。

rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error)
defer rows.Close()

for rows.Next() {
  var user User
  // ScanRowsは1行をuserに変換します
  db.ScanRows(rows, &user)

  // 何らかの処理を行います
}

別のモデルに格納

例えば、Resultという適当なモデルを用意して、それに格納することも可能です。
Table関数は基本的に、モデルがテーブルに対応していない場合に使うようにすると良さそうです。

type Result struct {
    Name string
    Age  int
}

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

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

特定カラムのみ抽出

値が格納されているモデルから特定カラムを抽出するにはPluck関数を使用します。

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

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

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

UPDATE

全フィールドの更新

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;

ただし、構造体のフィールドにゼロ値が含まれているとUpdate処理は実行されないようですのでご注意ください。

Update with struct only works with none zero values,

特定のカラムのみを更新

UpdateあるいはUpdates関数で特定のカラムのみを更新します。

// nameカラムの値を"hello"に更新します
db.Model(&user).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 条件付き(active==trueならば)でnameカラムの値を"hello"に更新します
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;

// `map` で複数のフィールドを更新します(対象のフィールドのみ)
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
//// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// `struct` で複数のフィールドを更新します(空ではないフィールドのみ)
db.Model(&user).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

DELETE

論理削除

テーブルにDeletedAtフィールドが存在する場合に、Delete関数を実行すると自動で論理削除になります。

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

物理削除

テーブルにDeletedAtフィールドが存在する場合に物理削除したい場合は、Unscoped().Delete関数を利用します。

db.Unscoped().Delete(&order)
//// DELETE FROM orders WHERE id=10;

また、テーブルにDeletedAtフィールドが存在しない場合に、Delete関数を実行すると物理削除になります。

db.Delete(&email)
//// DELETE from emails where id=10;

削除失敗

たとえ、条件にあうレコードが見つからずに、削除に失敗したとしてもエラーにはなりません。
削除失敗したときにエラーとしたい場合は、 RowsAffected を活用します。
サンプルコードとissueはこちらです。
https://github.com/jinzhu/gorm/issues/1380#issuecomment-600342388

素のSQLを実行する

SELECTはRaw関数、その他はExec関数で素のSQLを引数に渡して実行します。

type Result struct {
    Name string
    Age  int
}
var result Result
db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)

db.Exec("DROP TABLE users;")
db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now(), []int64{11,22,33})

素のSQLも実行結果は*sql.Row*sql.Rowsとして取得できます。

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

副問い合わせ

db.Where("amount > ?", DB.Table("orders").Select("AVG(amount)").Where("state = ?", "paid").QueryExpr()).Find(&orders)
// SELECT * FROM "orders"  WHERE "orders"."deleted_at" IS NULL AND (amount > (SELECT AVG(amount) FROM "orders"  WHERE (state = 'paid')))

テーブルの結合

Joins関数で結合するテーブルと条件を指定します。
結合結果はRowsでもモデルでもどちらにでも格納可能です。

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)

以下は3テーブル結合する例です。

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

トランザクション

トランザクション中の即時メソッド

デフォルトではデータの整合性を保つために、即時メソッドとトランザクションの関係は1:1になっています。
1つのトランザクション内で複数の即時メソッドを実行するための操作は以下です。

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

// データベース操作をトランザクション内で行います(ここからは'db'でなく'tx'を使います)
tx.Create(...)

// ...

// エラーが起きた場合はトランザクションをロールバックします
tx.Rollback()

// もしくはトランザクションをコミットします
tx.Commit()

具体例:

func CreateAnimals(db *gorm.DB) error {
  // Note the use of tx as the database handle once you are within a transaction
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()

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

  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
     tx.Rollback()
     return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
     tx.Rollback()
     return err
  }

  return tx.Commit().Error
}

トランザクションヘルパー

このようなヘルパー関数を用意してあげて、このヘルパー関数内でDBアクセスする処理を書いてあげるのも良いでしょう。

// how to use
// err := common.Transact(func(tx *gorm.DB) (err error) {
//   // DBアクセスを伴うビジネスロジック
//   return
// })
// if err != nil {
//   // エラーハンドリング
// }
func Transact(txFunc func(*gorm.DB) error) (err error) {
	if db == nil {
		log.Error("must set DB Connection")
		panic("must set DB Connection")
	}
	tx := db.Begin()
	if err = tx.Error; err != nil {
		return
	}
	defer func() {
		if p := recover(); p != nil {
			log.Error(p)
			if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
				log.Error(rollbackErr)
			}
			panic(p)
		} else if err != nil {
			log.Warn(err)
			if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
				log.Error(rollbackErr)
				panic(rollbackErr)
			}
		} else {
			err = tx.Commit().Error
		}
	}()
	err = txFunc(tx)
	return err
}

フック

フックとは、各トランザクション中で自動で実行されるメソッドです。
モデルごとにフックを定義できます。

INSERT

トランザクション内の流れ

  1. トランザクションの開始
  2. BeforeSave
  3. BeforeCreate
  4. 関連の保存前
  5. CreatedAtUpdatedAtのタイムスタンプ更新
  6. データの保存処理
  7. デフォルト値か空値のフィールドの再ロード
  8. 関連の保存後
  9. AfterCreate
  10. AfterSave
  11. トランザクションのコミットもしくはロールバック

フック例

func (u *User) BeforeSave() (err error) {
    if u.IsValid() {
        err = errors.New("不正な値を保存できません")
    }
    return
}

func (user *User) BeforeCreate(scope *gorm.Scope) error {
  scope.SetColumn("ID", uuid.New())
  return nil
}

func (u *User) AfterCreate(scope *gorm.Scope) (err error) {
    if u.ID == 1 {
    scope.DB().Model(u).Update("role", "admin")
  }
    return
}

SELECT

トランザクション内の流れ

  1. データベースからのデータロード
  2. プリロード(eager loading)
  3. AfterFind

フック例

unc (u *User) AfterFind() (err error) {
  if u.MemberShip == "" {
    u.MemberShip = "user"
  }
    return
}

UPDATE

トランザクション内の流れ

  1. トランザクションの開始
  2. BeforeSave
  3. BeforeUpdate
  4. データの更新処理
  5. UpdatedAtのタイムスタンプ更新
  6. モデルの持つ情報を保存
  7. 関連の保存後
  8. AfterUpdate
  9. AfterSave
  10. トランザクションのコミットもしくはロールバック

フック例

func (user *User) BeforeSave(scope *gorm.Scope) (err error) {
  if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
    scope.SetColumn("EncryptedPassword", pw)
  }
}

func (u *User) BeforeUpdate() (err error) {
    if u.readonly() {
        err = errors.New("読み出し専用ユーザーです")
    }
    return
}

// Updating data in same transaction
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
  }
    return
}

DELETE

トランザクション内の流れ

  1. トランザクションの開始
  2. BeforeDelete
  3. データの削除処理
  4. AfterDelete
  5. トランザクションのコミットもしくはロールバック

フック例

// 同一トランザクション内でデータを更新します
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
  }
    return
}

メソッドチェーン

メソッドチェーンは条件を鎖つなぎで指定する方法です。
条件だけなので、即時メソッド(Create, First, Find, Take, Save, UpdateXXX, Delete, Scan, Row, Rows… 等のCRUD操作をするメソッド)が実行されるまで実行されません。

db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable")

// 新規リレーションを作成します
tx := db.Where("name = ?", "jinzhu")

// さらにフィルタを追加します
if someCondition {
    tx = tx.Where("age = ?", 20)
} else {
    tx = tx.Where("age = ?", 30)
}

if yetAnotherCondition {
    tx = tx.Where("active = ?", 1)
}

また、条件を関数化し、Scopes関数でメソッドチェーンにすることも可能です。

func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
    return db.Where("amount > ?", 1000)
}

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCod(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
        return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status)
    }
}

db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// クレジットカードの注文かつ1000件以上の注文を取得します

db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// CODによる注文かつ1000件以上の注文を取得します

db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 支払い済みで発送済みの注文かつ1000件以上の注文を取得します

エラーハンドリング

即時メソッドを使う際はエラーハンドリングをすべきです。

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

GORMはレコードが見つからなかった時だけRecordNotFoundという特別なエラーを返します。
より親切なエラーハンドリングができます。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; gorm.IsRecordNotFoundError(err) {
  // レコードが見つかりません
}

ただし、sliceで結果を受け取る時は、IsRecordNotFoundErrorは必ずfalseになるため要注意です!
【Go】gorm のRecordNotFoundメソッドがsliceで結果を受け取ると必ずfalseになる
この時は、 if len(スライス型のモデル) == 0 で中身が空かどうかを判定するなどしましょう。

個人的には、エラー種類はもっと増やして欲しいですね。少なくともUKエラーは欲しいところ。
今の所は、そういうのはRDBのエラーメッセージを文字列解析したりする必要があります。
https://github.com/jinzhu/gorm/issues/2903
例えばUKであれば、とりあえずこんな感じの関数用意しておけば対応できます。

func IsDuplicateKeyError(err error) bool {
	switch e := err.(type) {
	case nil:
		return false
	case *pq.Error:
		if e.Code == uniqueViolation {
			return true
		}
		return false
	default:
		return false
	}
}

if IsDuplicateKeyError(err) {
// 省略  
}

ここら辺は、近々v2がでるみたいなので、期待ですねー。

ログ

デフォルトモードではエラーが起きた場合のみ出力します。

// ロガーを有効にすると、詳細なログを表示します
db.LogMode(true)

// ロガーを無効化すると、エラーさえも出力しなくなります
db.LogMode(false)

// 1回だけ操作をデバッグしてこの操作中の詳細なログのみ出力します
db.Debug().Where("name = ?", "jinzhu").First(&User{})

まとめ

  • GORMはMySQL/PostgreSQL/sqlite/SQLServerに対応している
  • モデル(構造体)を中心にデータ操作を行う
  • gorm用のタグでフィールドに制約を付けられる
  • GORMのマイグレーションは基本的に使わない
  • データ操作時のGORMの関数の呼び出し順が実際の解析と同じ順で分かりやすい
  • ORMの中では遅いほうだけど多機能
444
319
1

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
444
319

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?