Gin基礎 #6 データベース連携 (GORM)

読了 5分

前回の記事ではミドルウェアを扱いました。ここまで来ると、リクエストを受け取り、検証し、レスポンスし、共通処理を挟む基礎が整います。今回の記事はその上にデータベースをつなぎます。GORMで実際のデータを保存するCRUD APIを作ります。

  • #1 はじめてのサーバー
  • #2 ルーティングとハンドラ
  • #3 リクエストバインディングと検証
  • #4 レスポンス処理 — JSON、ステータスコード、エラー
  • #5 ミドルウェア
  • #6 データベース連携 (GORM) ← 今回の記事
  • #7 プロジェクト構成とミニREST API

GORMとは何ですか? #

GORM はGoで最も広く使われているORMです。SQLを直接書く代わりに、Goの構造体とメソッドでデータベースを扱えるようにしてくれます。SQLを直接扱う方式は Go実践 #4 DB連携 で扱ったので、2つのアプローチを比べると違いがはっきりします。

今回の記事はインストールが簡単なSQLiteを例にします。PostgreSQLやMySQLもドライバを変えるだけで、同じコードがほぼそのまま動作します。

インストール
go get gorm.io/gorm
go get gorm.io/driver/sqlite

データベース接続 #

gorm.Open で接続を開きます。返ってきた *gorm.DB が、以後すべてのクエリの出発点です。

接続
import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func setupDB() *gorm.DB {
	db, err := gorm.Open(sqlite.Open("app.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("DB接続失敗:", err)
	}
	return db
}

接続失敗はサーバーが正常に動作できない状況なので、log.Fatal で即座に終了します。

モデル定義 #

GORMはGoの構造体をテーブルに対応させます。構造体名の複数形がテーブル名になります(Userusers)。

モデル
type User struct {
	ID        uint      `json:"id" gorm:"primaryKey"`
	Name      string    `json:"name"`
	Email     string    `json:"email" gorm:"uniqueIndex"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}
  • gorm:"primaryKey" — 主キー指定。ID という名前ならGORMが自動で主キーと認識しますが、明示しても問題ありません
  • gorm:"uniqueIndex" — ユニークインデックス、メールの重複をDBレベルで遮断します
  • CreatedAt / UpdatedAt — GORMが作成、更新時刻を自動で埋めてくれる、約束されたフィールド名

json タグと gorm タグを一緒に付けている点に注目してください。同じ構造体がAPIレスポンスのシリアライズ(#4)とDBマッピングを兼ねます。

マイグレーション — AutoMigrate #

AutoMigrate はモデルに合わせてテーブルを作ったりカラムを追加したりします。開発段階でスキーマを素早く合わせるときに便利です。

マイグレーション
db.AutoMigrate(&User{})

本番環境では通常、別のマイグレーションツールでスキーマを管理しますが、入門段階では AutoMigrate で十分です。

CRUDの基本動作 #

GORMの中心となるメソッドをまず整理します。

CRUDメソッド
db.Create(&user)              // 作成
db.First(&user, id)           // 主キーで1件取得
db.Find(&users)               // 複数件取得
db.Save(&user)                // 更新 (全体保存)
db.Model(&user).Updates(...)  // 更新 (一部フィールド)
db.Delete(&user, id)          // 削除

取得結果がないとき、Firstgorm.ErrRecordNotFound エラーを返します。このエラーを捕まえて404でレスポンスするパターンを、以下で使います。

Ginハンドラと組み合わせる #

では、これらのメソッドをGinハンドラに付けます。ハンドラが *gorm.DB にアクセスする必要があるので、クロージャで db を閉じ込めてハンドラを作る方式を使います。

作成 — POST /users
func createUser(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var req struct {
			Name  string `json:"name" binding:"required"`
			Email string `json:"email" binding:"required,email"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		user := User{Name: req.Name, Email: req.Email}
		if err := db.Create(&user).Error; err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失敗"})
			return
		}

		c.JSON(http.StatusCreated, user)
	}
}

GORMメソッドは *gorm.DB を返し、その中の .Error フィールドで結果を確認します。作成に成功すると user.ID が埋められ、#4で見たとおり 201 Created でレスポンスします。

取得ハンドラです。

取得 — GET /users/:id
func getUser(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")

		var user User
		if err := db.First(&user, id).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
				return
			}
			c.JSON(http.StatusInternalServerError, gin.H{"error": "取得失敗"})
			return
		}

		c.JSON(http.StatusOK, user)
	}
}

errors.Is で「レコードなし」とそれ以外のDBエラーを区別し、それぞれ404と500でレスポンスします。一覧取得はもっとシンプルです。

一覧 — GET /users
func listUsers(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var users []User
		if err := db.Find(&users).Error; err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "取得失敗"})
			return
		}
		c.JSON(http.StatusOK, users)
	}
}

更新と削除も同じ型です。

更新・削除
func updateUser(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var user User
		if err := db.First(&user, c.Param("id")).Error; err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
			return
		}

		var req struct {
			Name string `json:"name"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		db.Model(&user).Update("name", req.Name)
		c.JSON(http.StatusOK, user)
	}
}

func deleteUser(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		if err := db.Delete(&User{}, c.Param("id")).Error; err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "削除失敗"})
			return
		}
		c.Status(http.StatusNoContent)
	}
}

ルートに接続する #

クロージャで作ったハンドラをルートに登録します。db を各ハンドラに渡す形です。

main.go
func main() {
	db := setupDB()
	db.AutoMigrate(&User{})

	r := gin.Default()

	users := r.Group("/users")
	{
		users.POST("", createUser(db))
		users.GET("", listUsers(db))
		users.GET("/:id", getUser(db))
		users.PUT("/:id", updateUser(db))
		users.DELETE("/:id", deleteUser(db))
	}

	r.Run()
}

これでユーザーを作成して取得する、完全なCRUD APIが動作します。

動作確認
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"カーティス","email":"a@b.com"}'

curl http://localhost:8080/users

関係マッピングは別途 #

ユーザーが複数の投稿を持つ1:Nのような関係マッピングはGORMの中心テーマですが、量が多いのでここでは扱いません。関係モデリングと関連データの取得は Go言語 GORM 1:N関係 の記事で別に整理したので、続けて読むとよいでしょう。

まとめ #

今回の記事で整理した内容です。

  • GORMはGoの構造体でDBを扱うORM、ドライバを変えるだけで別のDBへ移植可能
  • gorm.Open で接続し *gorm.DB でクエリ、AutoMigrate で開発段階のスキーマ管理
  • モデルは gorm タグで主キー、インデックスを指定、CreatedAt/UpdatedAt は自動管理
  • CRUDは Create/First/Find/Updates/Delete、結果は .Error で確認
  • ハンドラはクロージャで db を閉じ込めて作り、errors.Is で「なし」を404に分岐
  • 関係マッピングは別のGORM記事に続く

次の記事(#7 プロジェクト構成とミニREST API)では、これまで1つのファイルに集めたコードをレイヤー別に分け、設定を分離してシリーズを締めくくります。

X