GORM チートシート

# 概要

Go言語用のORMライブラリGORMの使い方をまとめたチートシート。RESTful APIとgRPCの両方のパターンを含む。

## 目次

1. セットアップと接続
2. モデル定義
3. マイグレーション
4. CRUD操作
5. クエリ操作
6. アソシエーション（関連）
7. トランザクション
8. RESTful APIパターン
9. gRPCパターン
10. フック
11. 高度な機能
12. パフォーマンス最適化

## 1. セットアップと接続

### 基本的な接続

```go
import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

// MySQL
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// PostgreSQL
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

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

```

### ロギングの設定

```go
newLogger := logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    logger.Config{
        SlowThreshold: time.Second, // 遅いSQLを記録する閾値
        LogLevel:      logger.Info, // ログレベル (Silent, Error, Warn, Info)
        Colorful:      true,
    },
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: newLogger,
})

```

### コネクションプールの設定

```go
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10)    // アイドル接続の最大数
sqlDB.SetMaxOpenConns(100)   // オープン接続の最大数
sqlDB.SetConnMaxLifetime(time.Hour) // 接続の最大生存期間

```

## 2. モデル定義

### 基本的なモデル

```go
type User struct {
    gorm.Model         // ID, CreatedAt, UpdatedAt, DeletedAtフィールドを含む
    Name     string    `gorm:"size:255;not null" json:"name"`
    Email    string    `gorm:"uniqueIndex;not null" json:"email"`
    Age      int       `json:"age"`
    Birthday *time.Time `json:"birthday"`
}

```

### カスタムテーブル名・カラム名

```go
// テーブル名の指定
func (User) TableName() string {
    return "my_users"
}

// カスタムカラム名
type Product struct {
    gorm.Model
    Code  string `gorm:"column:product_code;size:50"`
    Price uint   `gorm:"column:product_price"`
}

```

### リレーションシップ

```go
// 一対多 (Has Many)
type User struct {
    gorm.Model
    Name    string
    Orders  []Order // ユーザーは複数の注文を持つ
}

type Order struct {
    gorm.Model
    UserID uint    // 外部キー
    User   User    // belongs toの関係
    Amount float64
}

// 多対多 (Many to Many)
type Student struct {
    gorm.Model
    Name     string
    Courses  []Course `gorm:"many2many:student_courses;"`
}

type Course struct {
    gorm.Model
    Name     string
    Students []Student `gorm:"many2many:student_courses;"`
}

// ポリモーフィック関連
type Comment struct {
    gorm.Model
    Content      string
    CommentableID   uint
    CommentableType string
}

```

## 3. マイグレーション

### 自動マイグレーション

```go
// 単一モデルのマイグレーション
db.AutoMigrate(&User{})

// 複数モデルのマイグレーション
db.AutoMigrate(&User{}, &Product{}, &Order{})

```

### 手動マイグレーション操作

```go
// テーブルの作成
db.Migrator().CreateTable(&User{})

// テーブルの削除
db.Migrator().DropTable(&User{})

// テーブルの存在チェック
exists := db.Migrator().HasTable(&User{})

// インデックスの追加
db.Migrator().CreateIndex(&User{}, "Name")

// インデックスの削除
db.Migrator().DropIndex(&User{}, "Name")

```

## 4. CRUD操作

### Create（作成）

```go
// 単一レコードの作成
user := User{Name: "John", Age: 30}
result := db.Create(&user)
// user.ID -> 自動生成されたID
// result.Error -> エラーがあれば
// result.RowsAffected -> 影響を受けた行数

// 特定のフィールドのみ作成
db.Select("Name", "Age").Create(&user)

// 特定のフィールドを除外して作成
db.Omit("Name").Create(&user)

// 一括作成
users := []User{{Name: "John"}, {Name: "Jane"}}
db.Create(&users)

// バッチ処理
db.CreateInBatches(users, 100) // 100レコードずつ処理

```

### Read（読み取り）

```go
// 主キーによる取得
var user User
db.First(&user, 10) // ID=10のユーザー
db.First(&user, "id = ?", 10) // 条件指定も可能

// 条件による取得
db.Where("name = ?", "John").First(&user)

// 複数レコードの取得
var users []User
db.Find(&users) // 全件取得
db.Where("age > ?", 20).Find(&users) // 条件付き取得

// ORDER BY
db.Order("age desc").Find(&users)

// LIMIT & OFFSET
db.Limit(10).Offset(5).Find(&users)

// 集計関数
var count int64
db.Model(&User{}).Where("age > ?", 20).Count(&count)

```

### Update（更新）

```go
// 単一レコードの更新
db.First(&user)
user.Name = "新しい名前"
db.Save(&user) // 全フィールドを更新

// 特定フィールドのみ更新
db.Model(&user).Updates(User{Name: "新しい名前", Age: 32})
// または
db.Model(&user).Updates(map[string]interface{}{"name": "新しい名前", "age": 32})

// 単一カラム更新
db.Model(&user).Update("name", "新しい名前")

// 条件付き一括更新
db.Model(&User{}).Where("age > ?", 20).Update("name", "新しい名前")

```

### Delete（削除）

```go
// 単一レコード削除
db.Delete(&user) // gorm.Modelを使用している場合、ソフトデリート

// 主キーによる削除
db.Delete(&User{}, 10) // ID=10のユーザーを削除
db.Delete(&User{}, []int{1, 2, 3}) // ID=1,2,3のユーザーを削除

// 条件付き削除
db.Where("age < ?", 20).Delete(&User{})

// 完全削除 (物理削除)
db.Unscoped().Delete(&user)

```

## 5. クエリ操作

### 条件クエリ

```go
// 基本的な条件
db.Where("name = ?", "John").Find(&users)
db.Where("name <> ?", "John").Find(&users)
db.Where("age >= ? AND gender = ?", 20, "M").Find(&users)

// IN句
db.Where("name IN ?", []string{"John", "Jane"}).Find(&users)

// LIKE句
db.Where("name LIKE ?", "%J%").Find(&users)

// AND条件
db.Where("name = ?", "John").Where("age > ?", 20).Find(&users)

// OR条件
db.Where("name = ?", "John").Or("name = ?", "Jane").Find(&users)

// 構造体による条件
db.Where(&User{Name: "John", Age: 20}).Find(&users)
// SELECT * FROM users WHERE name = "John" AND age = 20

// マップによる条件
db.Where(map[string]interface{}{"name": "John", "age": 20}).Find(&users)

```

### 複雑なクエリとサブクエリ

```go
// ジョイン
db.Joins("JOIN orders ON orders.user_id = users.id").Find(&users)

// サブクエリ
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)

// サブクエリをFROM句で
subQuery := db.Table("users").Select("name")
db.Table("(?)", subQuery).Find(&users)

// Raw SQL
db.Raw("SELECT * FROM users WHERE name = ?", "John").Scan(&users)

```

### スコープ（再利用可能なクエリ）

```go
// スコープ定義
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
    return db.Where("amount > ?", 1000)
}

func OrderStatus(status string) func (db *gorm.DB) *gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
        return db.Where("status = ?", status)
    }
}

// スコープ使用
db.Scopes(AmountGreaterThan1000, OrderStatus("paid")).Find(&orders)

```

## 6. アソシエーション（関連）

### Preload（事前読込）

```go
// 単一の関連をプリロード
db.Preload("Orders").Find(&users)

// ネストした関連をプリロード
db.Preload("Orders.Items").Find(&users)

// 複数の関連をプリロード
db.Preload("Orders").Preload("Profile").Find(&users)

// 条件付きプリロード
db.Preload("Orders", "state = ?", "paid").Find(&users)

```

### 関連の操作

```go
// 関連の追加
db.Model(&user).Association("Orders").Append(&newOrder)

// 関連の置き換え
db.Model(&user).Association("Orders").Replace(&newOrders)

// 関連の削除
db.Model(&user).Association("Orders").Delete(&orderToDelete)

// 関連のクリア
db.Model(&user).Association("Orders").Clear()

// 関連のカウント
count := db.Model(&user).Association("Orders").Count()

```

## 7. トランザクション

### 基本的なトランザクション

```go
// 手動トランザクション
tx := db.Begin()

// ロールバックしてエラーを返す関数
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

// トランザクション内の操作
if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}

if err := tx.Create(&order).Error; err != nil {
    tx.Rollback()
    return err
}

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

```

### 宣言的トランザクション

```go
// 関数でラップされたトランザクション
err := db.Transaction(func(tx *gorm.DB) error {
    // トランザクション内の操作
    if err := tx.Create(&user).Error; err != nil {
        // エラー発生時は自動ロールバック
        return err
    }

    if err := tx.Create(&order).Error; err != nil {
        return err
    }

    // nilを返すと自動コミット
    return nil
})

```

### ネストしたトランザクション

```go
db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user)

    // ネストしたトランザクション
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&order)
        return nil
    })

    return nil
})

```

## 8. RESTful APIパターン

### Ginフレームワークとの統合

```go
func GetUsers(c *gin.Context) {
    var users []User
    result := db.Find(&users)

    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        return
    }

    c.JSON(http.StatusOK, users)
}

func GetUser(c *gin.Context) {
    id := c.Param("id")
    var user User

    result := db.First(&user, id)
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        } else {
            c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        }
        return
    }

    c.JSON(http.StatusOK, user)
}

func CreateUser(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    result := db.Create(&user)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        return
    }

    c.JSON(http.StatusCreated, user)
}

// ルーティング設定
router := gin.Default()
router.GET("/users", GetUsers)
router.GET("/users/:id", GetUser)
router.POST("/users", CreateUser)

```

### ページネーションの実装

```go
func GetUsers(c *gin.Context) {
    // クエリパラメータの取得
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
    offset := (page - 1) * limit

    var users []User
    var count int64

    // 総件数の取得
    db.Model(&User{}).Count(&count)

    // ページネーション付きデータ取得
    result := db.Offset(offset).Limit(limit).Find(&users)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": users,
        "total": count,
        "page": page,
        "limit": limit,
        "pages": int(math.Ceil(float64(count) / float64(limit))),
    })
}

```

### トランザクションを用いた複雑な操作

```go
func CreateOrderWithItems(c *gin.Context) {
    // リクエストデータのバインド
    var input struct {
        Order Order       `json:"order"`
        Items []OrderItem `json:"items"`
    }

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // トランザクション開始
    err := db.Transaction(func(tx *gorm.DB) error {
        // 注文の作成
        if err := tx.Create(&input.Order).Error; err != nil {
            return err
        }

        // 注文項目の作成
        for i := range input.Items {
            input.Items[i].OrderID = input.Order.ID
            if err := tx.Create(&input.Items[i]).Error; err != nil {
                return err
            }
        }

        // 在庫の更新など追加処理...

        return nil
    })

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "order": input.Order,
        "items": input.Items,
    })
}

```

## 9. gRPCパターン

### Proto定義

```protobuf
syntax = "proto3";
package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc UpdateUser(UpdateUserRequest) returns (User);
  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}

message GetUserRequest {
  int64 id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 limit = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  int64 total = 2;
}

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  google.protobuf.Timestamp created_at = 5;
  google.protobuf.Timestamp updated_at = 6;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message UpdateUserRequest {
  int64 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
}

message DeleteUserRequest {
  int64 id = 1;
}

message DeleteUserResponse {
  bool success = 1;
}

```

### gRPC実装

```go
type UserServer struct {
    db *gorm.DB
    pb.UnimplementedUserServiceServer
}

func NewUserServer(db *gorm.DB) *UserServer {
    return &UserServer{db: db}
}

func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    var user User
    result := s.db.First(&user, req.Id)
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            return nil, status.Errorf(codes.NotFound, "User not found")
        }
        return nil, status.Errorf(codes.Internal, "Failed to get user: %v", result.Error)
    }

    return convertUserToProto(&user), nil
}

func (s *UserServer) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
    offset := (req.Page - 1) * req.Limit
    var users []User
    var count int64

    s.db.Model(&User{}).Count(&count)

    result := s.db.Offset(int(offset)).Limit(int(req.Limit)).Find(&users)
    if result.Error != nil {
        return nil, status.Errorf(codes.Internal, "Failed to list users: %v", result.Error)
    }

    protoUsers := make([]*pb.User, len(users))
    for i, user := range users {
        protoUsers[i] = convertUserToProto(&user)
    }

    return &pb.ListUsersResponse{
        Users: protoUsers,
        Total: count,
    }, nil
}

func (s *UserServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
    user := User{
        Name:  req.Name,
        Email: req.Email,
        Age:   int(req.Age),
    }

    result := s.db.Create(&user)
    if result.Error != nil {
        return nil, status.Errorf(codes.Internal, "Failed to create user: %v", result.Error)
    }

    return convertUserToProto(&user), nil
}

// 変換ヘルパー関数
func convertUserToProto(user *User) *pb.User {
    createdAt := timestamppb.New(user.CreatedAt)
    updatedAt := timestamppb.New(user.UpdatedAt)

    return &pb.User{
        Id:        int64(user.ID),
        Name:      user.Name,
        Email:     user.Email,
        Age:       int32(user.Age),
        CreatedAt: createdAt,
        UpdatedAt: updatedAt,
    }
}

// サーバーの起動
func StartGRPCServer(db *gorm.DB) {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, NewUserServer(db))

    log.Println("Starting gRPC server on :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

```

### トランザクションを用いたgRPC実装

```go
func (s *UserServer) CreateOrderWithItems(ctx context.Context, req *pb.CreateOrderRequest) (*pb.OrderResponse, error) {
    var order Order
    var items []OrderItem

    // protoからモデルへの変換
    order = convertProtoToOrder(req.Order)
    for _, item := range req.Items {
        items = append(items, convertProtoToOrderItem(item))
    }

    // トランザクション
    err := s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(&order).Error; err != nil {
            return err
        }

        for i := range items {
            items[i].OrderID = order.ID
            if err := tx.Create(&items[i]).Error; err != nil {
                return err
            }
        }

        return nil
    })

    if err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to create order: %v", err)
    }

    // レスポンス生成
    protoOrder := convertOrderToProto(&order)
    protoItems := make([]*pb.OrderItem, len(items))
    for i, item := range items {
        protoItems[i] = convertOrderItemToProto(&item)
    }

    return &pb.OrderResponse{
        Order: protoOrder,
        Items: protoItems,
    }, nil
}

```

## 10. フック

### モデルフック

```go
// BeforeSave - レコード保存前の処理
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
    if len(u.Password) > 0 {
        // パスワードハッシュ化など
        hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        if err != nil {
            return err
        }
        u.Password = string(hashedPassword)
    }
    return
}

// AfterFind - レコード検索後の処理
func (u *User) AfterFind(tx *gorm.DB) (err error) {
    // 例: 関連データの取得など
    return
}

// BeforeDelete - レコード削除前の処理
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    // 例: 関連レコードの削除チェックなど
    if u.IsAdmin {
        return errors.New("admin user cannot be deleted")
    }
    return
}

```

### 利用可能なフック

```
// 作成操作のフック
BeforeSave
BeforeCreate
AfterCreate
AfterSave

// 更新操作のフック
BeforeSave
BeforeUpdate
AfterUpdate
AfterSave

// 削除操作のフック
BeforeDelete
AfterDelete

// クエリ操作のフック
AfterFind

```

## 11. 高度な機能

### スコープ設定

```go
// グローバルスコープ
func SetupGlobalScopes(db *gorm.DB) {
    // 削除済みを含むモデルでも常に非削除のみ取得するスコープ
    db.Callback().Query().Before("gorm:query").Register("apply_global_scopes", func(db *gorm.DB) {
        if db.Statement.Schema != nil && db.Statement.Schema.ModelType.Name() == "User" {
            db.Where("active = ?", true)
        }
    })
}

// デフォルトスコープ
type User struct {
    gorm.Model
    Name   string
    Active bool
}

// デフォルトスコープの設定
func (u *User) BeforeFind(tx *gorm.DB) error {
    tx.Where("active = ?", true)
    return nil
}

// デフォルトスコープの解除
db.Unscoped().Find(&users)

```

### データベース固有の機能

```go
// MySQL: ロック
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)

// PostgreSQL: 競合時の処理
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&user)

// 複合主キー
type Tag struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string `gorm:"primaryKey"`
    Value string
}

```

### コンテキスト

```go
// コンテキスト付きクエリ
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

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

// トランザクションでのコンテキスト使用
db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
    return tx.Create(&user).Error
})

```

## 12. パフォーマンス最適化

### インデックスの活用

```go
// インデックス定義
type User struct {
    gorm.Model
    Name  string `gorm:"index"`
    Email string `gorm:"uniqueIndex"`
    // 複合インデックス
    Address string
    Phone   string `gorm:"index:idx_address_phone,unique"`
}

// インデックスの追加
db.Migrator().CreateIndex(&User{}, "Name")
db.Migrator().CreateIndex(&User{}, "idx_users_address_phone")

```

### バッチ処理

```go
// バッチ挿入
db.CreateInBatches(users, 100) // 100件ずつ挿入

// バッチ更新
db.Model(&User{}).Where("role = ?", "admin").Updates(map[string]interface{}{"admin_flag": true})

// バッチ削除
db.Where("created_at < ?", time.Now().Add(-30*24*time.Hour)).Delete(&LogEntry{})

```

### クエリの最適化

```go
// 必要なフィールドだけ選択
db.Select("id", "name").Find(&users)

// 遅延読み込み
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("orders.created_at DESC").Limit(5)
}).Find(&users)

// ジョインの最適化
db.Joins("LEFT JOIN orders ON orders.user_id = users.id").
   Select("users.*, orders.id as order_id").
   Find(&results)

```

### コネクションプールの調整

```go
sqlDB, err := db.DB()

// アイドル接続の最大数 (デフォルト: 2)
sqlDB.SetMaxIdleConns(10)

// オープン接続の最大数 (デフォルト: 無制限)
sqlDB.SetMaxOpenConns(100)

// 接続の最大生存期間
sqlDB.SetConnMaxLifetime(time.Hour)

// 接続の最大アイドル時間
sqlDB.SetConnMaxIdleTime(time.Minute * 30)

```

## マイグレーションチートシート

### 最小限のデータベースセットアップ

```go
package main

import (
    "log"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"

    "yourapp/models"
)

func main() {
    dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }

    // モデルのマイグレーション
    err = db.AutoMigrate(
        &models.User{},
        &models.Product{},
        &models.Order{},
        // その他のモデル
    )
    if err != nil {
        log.Fatalf("Failed to migrate database: %v", err)
    }

    log.Println("Database migration completed")
}

```