
【告知】値の参照時の仕様変更のお知らせ
このたび2024年11月11日に値の参照に関する仕様変更を予定しておりますので詳細について報告いたします。
more
2023.03.13に公開 | 2023.11.16に更新
Querier運営
「Querier(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。
みなさんこんにちは。
今回はGoでAPIサーバーを作るときのDBコネクションの引き回しとトランザクションについて、実際の活用事例も含めて解説していきます。
ある程度レイヤー化されたGoのアプリケーションでは、しばしばDBコネクションをどのように引き回し、トランザクション処理をどこで行うかというのは常に議論されてきています。
今回はクエリアが行っている実装方法を紹介していきますので、ぜひ最後まで読んでいただけると幸いです。
.
├── account
│ ├── handler.go
│ ├── service.go
│ └── store.go
├── cmd
│ └── app
│ └── main.go
├── container
│ └── container.go
├── infra
│ └── store.go
├── infra_dep.go
├── infra_store.go
└── model
└── account.go
データベースやモデルに関するパッケージを一部抜粋したものになります。
それぞれのファイルの役割や、内部のコードに関しては下で解説していきます。
また、全体のパッケージ構成については今後詳しく紹介していきますので、そちらの記事をお待ち下さい。
model
パッケージ内にモデルを定義していきます。モデルパッケージ内には、モデルの構造体と、データアクセス層のインターフェースのみを定義します。
package model
type Account struct {
ID string `json:"id" db:"id"`
FirstName string `json:"firstName" db:"first_name"`
LastName string `json:"lastName" db:"last_name"`
Email string `json:"email" db:"email"`
CreatedAt time.Time `json:"-" db:"created_at"`
UpdatedAt time.Time `json:"-" db:"updated_at"`
}
type AccountStore interface {
GetByID(context.Context, string) (*Account, error)
Create(context.Context, *Account) error
}
次に、データアクセス層の共通処理と抽象化を行っていきます。
package root
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/xxx/xxx/model"
)
type ModelStore interface {
Account() model.AccountStore
}
type Store interface {
ModelStore
RunTransaction(func(tx Transaction) error) error
}
type Transaction interface {
ModelStore
}
type DB interface {
Query(string, ...interface{}) (*sql.Rows, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
Exec(string, ...interface{}) (sql.Result, error)
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
GetContext(context.Context, interface{}, string, ...interface{}) error
QueryxContext(context.Context, string, ...interface{}) (*sqlx.Rows, error)
SelectContext(context.Context, interface{}, string, ...interface{}) error
}
各インターフェースの役割は以下になります。
ModelStore
は各モデルて定義されたStoreを取得する関数を定義します。このインターフェース自体をどこかで使うわけではなく、下で定義する Store
と Transaction
にEmbedするためのものです。
Store
は ModelStore
と、RunTransaction
というトランザクションを実行する関数を定義します。
Transaction
は ModelStore
のみをEmbedします。
DB
には、お使いのORMやクエリビルダーに応じてデータアクセスのための関数を定義し、DBコネクションの抽象化を定義します。
インターフェースの定義が完了したら、infra/store.go
内では、infra_store.go
で定義した Store
と Transaction
インターフェースの実装をしていきます。
package infra
import (
"github.com/jmoiron/sqlx"
"github.com/xxx/xxx"
"github.com/xxx/xxx/account"
)
type store struct {
db *sqlx.DB
}
func NewStore(db *sqlx.DB) root.Store {
return &store{db}
}
func (s *store) RunTransaction(f func(root.Transaction) error) error {
tx, err := s.db.Beginx()
if err != nil {
return err
}
if err := f(&transaction{db: tx}); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *store) Account() model.AccountStore {
return account.NewStore(s.db)
}
type transaction struct {
db *sqlx.Tx
}
func (tx *transaction) Account() model.AccountStore {
return account.NewStore(tx.db)
}
account/store.go
内では、model
パッケージで定義した AccountStore
の実装をしていきます。
ここでは、infra_store.go
で定義した、DB
インターフェースを用いてデータベースへのリクエストを構築します。
package account
import (
"context"
"github.com/xxx/xxx"
"github.com/xxx/xxx/model"
)
type store struct {
db root.DB
}
func NewStore(db root.DB) model.AccountStore {
return &store{db}
}
func (s *store) Get(ctx context.Context, id string) (*model.Account, error) {
}
func (s *store) Create(ctx context.Context, m *model.Account) error {
}
Service層でStoreを呼び出す準備として、依存するStoreを初期化する Container
を定義していきます。infra_dep.go
内で、以下の依存の集約を定義します。今後、外部APIに依存するメール送信や決済機能などが入る際には、ここに定義を追加していきます。
package root
type Dependency struct {
Store Store
}
container/container.go
で、初期化する関数を定義します。
package container
type Container struct {
DB *sqlx.DB
}
func New(ctx context.Context) *Container {
db, err := mysql.New()
if err != nil {
logger.Logger.Fatal("", zap.Error(err))
}
return &Container{
DB: db,
}
}
func (c *Container) Dependency() *root.Dependency {
store := infra.NewStore(c.DB)
return &root.Dependency{
Store: store,
}
}
func (c *Container) Account() account.Service {
return account.NewService(c.Dependency())
}
AccountService
の構造体には Dependency
を持ち、Storeを取得します。
package account
type Service interface {
}
type service struct {
*root.Dependency
}
func NewService(d *root.Dependency) Service {
return &service{Dependency: d}
}
Serviceの関数内で、Storeを呼び出す際は、以下のように呼び出します。
// 純粋にStoreを呼び出す際
a, err := s.Store.Account().GetByID(ctx, "")
// Transactionを実行する際
if err = s.Store.RunTransaction(func(tx root.Transaction) error {
return tx.Account().Create(ctx, account)
}); err != nil {
return nil, err
}
今回は、クエリアが実践するGoアプリケーションでのDBコネクションの引き回しとトランザクション管理について解説していきました。GoのAPIを開発していてデータベース周りでの実装方法を模索している方がいればぜひ参考にしていただければと思います。
クエリアは社内のサーバーサイドはほぼ全てGoで開発されているので、今後も社内でのナレッジなどを発信していきます。
また、クエリアは様々なデータベース、APIと連携して、社内向けの管理画面やツールを構築できるローコードツールの『Querier』を開発しています。無料トライアルも実施しておりますので、もし気になる方がいればサイトを覗いてみてください。
Querier運営
「Querier(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。
このたび2024年11月11日に値の参照に関する仕様変更を予定しておりますので詳細について報告いたします。
more
データフローの通知設定機能・監査ログへのパラメータが追加されましたのでご紹介します。
more
データフローのアクションに永続化などに利用できるローカルストレージ機能を追加しました。
more
日本を健康に。多彩なフィットネスブランドを展開中。スタジオ付きの「JOYFIT」、24時間型の「JOYFIT24」、ヨガスタジオ「JOYFIT YOGA」、パーソナルジム「JOYFIT+」、家族向けの「FIT365」など、多彩なブランド展開で全国を席巻しているスポーツ事業。
more