概要
Go言語用のORMライブラリGORMの使い方をまとめたチートシート。RESTful APIとgRPCの両方のパターンを含む。
目次
- セットアップと接続
- モデル定義
- マイグレーション
- CRUD操作
- クエリ操作
- アソシエーション(関連)
- トランザクション
- RESTful APIパターン
- gRPCパターン
- フック
- 高度な機能
- パフォーマンス最適化
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")
}