0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GORM チートシート

Posted at

概要

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

目次

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

1. セットアップと接続

基本的な接続

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

ロギングの設定

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

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

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

2. モデル定義

基本的なモデル

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"`
}

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

// テーブル名の指定
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"`
}

リレーションシップ

// 一対多 (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. マイグレーション

自動マイグレーション

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

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

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

// テーブルの作成
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(作成)

// 単一レコードの作成
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(読み取り)

// 主キーによる取得
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(更新)

// 単一レコードの更新
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(削除)

// 単一レコード削除
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. クエリ操作

条件クエリ

// 基本的な条件
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)

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

// ジョイン
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)

スコープ(再利用可能なクエリ)

// スコープ定義
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(事前読込)

// 単一の関連をプリロード
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)

関連の操作

// 関連の追加
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. トランザクション

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

// 手動トランザクション
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

宣言的トランザクション

// 関数でラップされたトランザクション
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
})

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

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フレームワークとの統合

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)

ページネーションの実装

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

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

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定義

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実装

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実装

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. フック

モデルフック

// 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. 高度な機能

スコープ設定

// グローバルスコープ
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)

データベース固有の機能

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

コンテキスト

// コンテキスト付きクエリ
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. パフォーマンス最適化

インデックスの活用

// インデックス定義
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")

バッチ処理

// バッチ挿入
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{})

クエリの最適化

// 必要なフィールドだけ選択
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)

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

sqlDB, err := db.DB()

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

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

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

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

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

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

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?