データベース
この章では、Go-Sail でデータベースとやりとりする方法を紹介します。
はじめに
ほとんどすべての現代的な Web アプリケーションはデータベースとやりとりします。Go-Sail ではデータベース操作層エンジンとして GORM を採用しており、さまざまな種類のデータベースに簡単に接続できます。
現在、Go-Sail では以下のデータベースに対する組み込み設定が用意されており、呼び出し側は API の簡単な文法で、内部の詳細を気にせずにアクセスできます。
- MySQL / MariaDB
- PostgreSQL
- SQLite
- SQL Server
- Clickhouse
前章ではログイン画面を作成しましたが、ユーザー名とパスワードは固定値でハードコーディングされていました。ここからはデータベースとの連携を始めていきます。
以下のコード例は MySQL データベースを使っています。
モデルの宣言
package main
type User struct {
Username string `gorm:"column:username;type:varchar(100);NOT NULL;comment:username"`
Password string `gorm:"column:password;type:varchar(1024);NOT NULL;comment:password"`
}
func (User) TableName() string {
return "users"
}
設定
Go-Sail の動作は設定駆動です。データベースと接続するためには、まず接続情報を設定する必要があります。クイックスタートで書いたコードを振り返ると、空の設定を宣言していましたが、これを必要に応じて埋めていきます。
package main
import (
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if loginRequest.Username != "go-sail" || loginRequest.Password != "password" {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, nil).Launch()
}
サービス起動時、設定でデータベースの有効化とドライバー名(ここでは MySQL)が指定されているため、Go-Sail はこの設定にしたがってデータベースコンポーネントを初期化し、グローバルに利用できるようにします。
SQL クエリの実行
データベースとの接続に成功し、データベースコンポーネントが準備できたら、APIっぽい文法で操作(クエリ など)を行うことができます。
上記例を用いると、ハードコードせずに、データベース上の user テーブルをクエリし、対象ユーザーが存在するか確認し、さらに入力されたパスワードとDB上のパスワードを比較します。
package main
import (
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
type User struct {
Username string `gorm:"column:username;type:varchar(100);NOT NULL;comment:username"`
Password string `gorm:"column:password;type:varchar(1024);NOT NULL;comment:password"`
}
func (User) TableName() string {
return "users"
}
var (
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない場合
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
// パスワード不一致
if loginRequest.Password != user.Password {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, nil).Launch()
}
読み書き接続の分離
上記の設定例から分かるように、Read(読み込み)とWrite(書き込み)の2つの接続を設定しています。これはリードライト分離(読取書込分離)シナリオのためで、たとえば定期的にユーザー数を集計し外部(Slackなど)に通知するような読み取りだけの処理や、ユーザー登録時のように読んで・条件を満たせば書き込む場合などに応じて使い分けます。
これは典型的な OLAP と OLTP のシナリオで、Web アプリケーションではよくあります。もしこうした要件や分離設計が不要で、単一DBのみを使用している場合(今回のサンプル)には、ReadとWriteの設定を同じにして構いません。
データベースリード関数の例です。GetDBR() メソッドを使えます。
package main
import (
"github.com/keepchen/go-sail/v3/sail"
)
...
sail.GetDBR().Where(...).First(...)
...
書き込み関数の例として、GetDBW() メソッドを使えます。
package main
import (
"github.com/keepchen/go-sail/v3/sail"
)
...
sail.GetDBW().Where(...).Updates(...)
...
データベース操作のシンタックスシュガーは GORM 由来であり、GORM と全く同じです。さらに詳細な例はGORM公式ドキュメント を参照ください。
データベーストランザクション
データの一貫性を確保するために、トランザクションは非常に重要です。Go-Sail では GORM が提供するトランザクション処理メカニズムを簡単に利用できます。
package main
import (
"github.com/keepchen/go-sail/v3/sail"
)
...
sail.GetDBW().Transaction(func(tx *gorm.DB) error {
// ユーザー存在確認
err := tx.Where(&User{Username: "..."}).First(&user).Error
if err != nil {
return err
}
if len(user.Username) != 0 {
return nil
}
// 存在しなければ作成
return tx.Create(&User{Username: "...", Password: "..."}).Error
})
...
データ一貫性を確保するため、Transaction() の中では必ず tx を使用してください。そうしないと思わぬ問題が発生します。
また、たとえ Read/Write 設定が同一でも、Go-Sail で提供されている GetDBW() キーワードを使ってトランザクションアクセスすることを強く推奨します。
タイムアウト制御
実業務では、さまざまな要因で処理が長時間に及ぶことがありえます。その場合、処理時間を適切に制限できるよう、タイムアウト設定が必要です。たとえば「DB操作は5秒を超えたらキャンセルする」といった形です。
package main
import (
"context"
"github.com/keepchen/go-sail/v3/sail"
)
...
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
sail.GetDBW().
WithContext(ctx).
Transaction(func(tx *gorm.DB) error {
// ユーザー存在確認
err := tx.Where(&User{Username: "..."}).First(&user).Error
if err != nil {
return err
}
if len(user.Username) != 0 {
return nil
}
// 存在しなければ作成
return tx.Create(&User{Username: "...", Password: "..."}).Error
})
...
タイムゾーン設定
基準時刻(時刻の一貫性)は、特に分散システムやグローバルなデータセンターなど複雑な環境において非常に重要です。もしサービスが異なるタイムゾーンで実行され、「現在時刻」取得の動作を統一したい場合は、データベース設定内で NowFunc を指定する必要があります。
グローバルアプリケーションを例として、すべてのユーザーへ同じタイムスタンプ表示をしたい場合、そのための時間関数・タイムゾーンを統一し、出力・変換時のデータの正しさを担保します。
例として、データベースのタイムゾーンを UTC に設定する場合です。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if loginRequest.Username != "go-sail" || loginRequest.Password != "password" {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, nil).Launch()
}
ロギング
ロギングはトラブルシューティングのための重要な方法のひとつであり、特定のイベントやデータを記録するのにも役立ちます。たとえば、スロークエリの記録や SQL 実行エラーなどです。
Go-Sail エコシステムにおけるデータベースのロギングは、グローバルのロギングコンポーネントと共通の、uber/zap ログライブラリに基づいています。より使いやすくするには、以下のような設定を行います。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if loginRequest.Username != "go-sail" || loginRequest.Password != "password" {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, nil).Launch()
}
各フィールドの説明
-
Level(レベル)
ログレベルを指定します。現在、次のレベルがサポートされています(低 → 高):- silent
- info
- warn
- error
-
SlowThreshold(スロースレッショルド)
スロー(遅い)ログ記録閾値(ミリ秒)。この時間を超える SQL は記録されます。 -
SkipCallerLookup
呼び出し元(コールチェーン)を出力しないかどうか。 -
IgnoreRecordNotFoundError
デフォルトでは、GORM は検索でレコードがなければRecordNotFoundErrorエラーを返します。trueに設定するとこのエラーを無視できます。 -
Colorful
ログ出力をカラーにするか。通常はfalse推奨です。
マイグレーション
通常はデータベースのテーブル構造を自動的に移行・同期する必要があります。たとえば上記サンプルでは最初DBに user テーブルがなければ、サービス起動時にテーブルを自動作成し、その後利用できるようにします。
一部のチームや会社には専門の DBA がいてデータベース管理を担当している場合もありますし、監査等の理由でプログラムからの自動マイグレーションが不要な場合もあります。
その場合は、GORM の AutoMigrate 関数が利用できます。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if loginRequest.Username != "go-sail" || loginRequest.Password != "password" {
sail.Response(c).Failure("ログイン失敗、ユーザー名またはパスワードが一致しません!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
sail.GetDBW().Create(&User{Username:"go-sail", password:"password"})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}