Querier

GoのAPIサーバーでDBコネクションの引き回しとトランザクション管理をスマートにやる方法

2023.03.13に公開 | 2023.11.16に更新

Querier運営

@querier_io@querierinc

「Querier(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。

管理画面の構築もWeb上で完結
エンジニアのためのローコードツール

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インターフェース

ModelStore は各モデルて定義されたStoreを取得する関数を定義します。このインターフェース自体をどこかで使うわけではなく、下で定義する StoreTransaction にEmbedするためのものです。

Storeインターフェース

StoreModelStore と、RunTransaction というトランザクションを実行する関数を定義します。

Transactionインターフェース

TransactionModelStore のみをEmbedします。

DBインターフェース

DB には、お使いのORMやクエリビルダーに応じてデータアクセスのための関数を定義し、DBコネクションの抽象化を定義します。

インターフェースの定義が完了したら、infra/store.go 内では、infra_store.go で定義した StoreTransaction インターフェースの実装をしていきます。

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)
}

Storeの実装を行う

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())
}

ServiceでStoreを呼び出す

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(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。

最新の記事

2〜3ヶ月と見積もっていた開発期間を、クエリアを導入することでわずか1週間に短縮できました

2012年5月創業のフルカイテン株式会社。 「在庫をフル回転させる」をコンセプトに、機械学習を駆使したSaaS『FULL KAITEN』を提供し、在庫問題の解決に取り組む。

more

管理画面の構築もWeb上で完結
エンジニアのためのローコードツール

Querierについて詳しく見る