Gin基礎 #6 データベース連携 (GORM)
前回の記事ではミドルウェアを扱いました。ここまで来ると、リクエストを受け取り、検証し、レスポンスし、共通処理を挟む基礎が整います。今回の記事はその上にデータベースをつなぎます。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の構造体をテーブルに対応させます。構造体名の複数形がテーブル名になります(User → users)。
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の中心となるメソッドをまず整理します。
db.Create(&user) // 作成
db.First(&user, id) // 主キーで1件取得
db.Find(&users) // 複数件取得
db.Save(&user) // 更新 (全体保存)
db.Model(&user).Updates(...) // 更新 (一部フィールド)
db.Delete(&user, id) // 削除取得結果がないとき、First は gorm.ErrRecordNotFound エラーを返します。このエラーを捕まえて404でレスポンスするパターンを、以下で使います。
Ginハンドラと組み合わせる #
では、これらのメソッドをGinハンドラに付けます。ハンドラが *gorm.DB にアクセスする必要があるので、クロージャで db を閉じ込めてハンドラを作る方式を使います。
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 でレスポンスします。
取得ハンドラです。
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でレスポンスします。一覧取得はもっとシンプルです。
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 を各ハンドラに渡す形です。
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つのファイルに集めたコードをレイヤー別に分け、設定を分離してシリーズを締めくくります。