🏃‍♀️

GolangでREST(その2)

2023/04/25に公開

前回は

https://zenn.dev/robon/articles/af7b1b18427e50

全体の設計方針とDto、Entityといったデータ構造、DaoによるSQLの実行部分まで説明しました。

実装の続き

Service

Serviceは、Controllerが扱うDtoとDaoが扱うEntity間の対応を行います。また、Dtoを構成するEntityが複数種類ある場合は、それぞれのEntityに対応するDaoをそれぞれ呼び出します。

module/customers/service.go
package customers

import (
	"context"
	"database/sql"
)

// Service は、Contorller からの要求を Dao を使用して解決する。
type Service struct {
	dao *Dao
}

// NewService は、Service を生成する。
func NewService() *Service {
	return &Service{
		dao: &Dao{},
	}
}

// Create は、指定された Dto を登録する。
func (s *Service) Create(ctx context.Context, tx *sql.Tx, dto *Dto) (*Dto, error) {
	entity := NewEntity(dto)
	entity, err := s.dao.Insert(ctx, tx, entity)
	if err != nil {
		return nil, err
	}
	return NewDto(entity), nil
}

// Read は、指定されたキーの Dto を返す。
func (s *Service) Read(ctx context.Context, tx *sql.Tx, key *Key) (*Dto, error) {
	entity, err := s.dao.Select(ctx, tx, key)
	if err != nil {
		return nil, err
	}
	return NewDto(entity), nil
}

// Update は、指定された Dto を更新する。
func (s *Service) Update(ctx context.Context, tx *sql.Tx, dto *Dto) (*Dto, error) {
	entity := NewEntity(dto)
	entity, err := s.dao.Update(ctx, tx, entity)
	if err != nil {
		return nil, err
	}
	return NewDto(entity), nil
}

// Delete は、指定されたキーの Dto を削除する。
func (s *Service) Delete(ctx context.Context, tx *sql.Tx, key *Key) error {
	return s.dao.Delete(ctx, tx, key)
}

上位で作られたコンテキスト(context.Context)とトランザクション(sql.Tx)をDaoに渡すようにしました。

Contoroller

Controllerは、HTTPのリクエストとレスポンスを入出力として、これをGolangのデータ構造であるDtoに対応付けます。Controllerのメソッドがトランザクション境界となるように、メソッド内でトランザクションの開始と終了を行います。

module/customers/controller.go
package customers

import (
	"database/sql"
	"fmt"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/render"
	"github.com/take0a/go-rest-sample/utils"
)

// Controller は、REST呼び出しを処理する。
type Controller struct {
	db      *sql.DB
	service *Service
}

// NewController は、Controller を生成する。
func NewController(db *sql.DB) *Controller {
	return &Controller{
		db:      db,
		service: NewService(),
	}
}

// Get は、指定されたキーのリソースを返す。
func (c *Controller) Get(w http.ResponseWriter, r *http.Request) {
	param := chi.URLParam(r, "customerId")
	id, err := strconv.Atoi(param)
	if err != nil {
		utils.BadRequest(w, err, fmt.Sprintf("Invalid URL Parameter %s", param))
		return
	}

	ctx := r.Context()
	tx, err := c.db.BeginTx(ctx, nil)
	if err != nil {
		utils.ServerError(w, err, "BeginTx")
		return
	}
	defer tx.Rollback()

	dto, err := c.service.Read(ctx, tx, &Key{CustomerID: id})
	if err != nil {
		if err == sql.ErrNoRows {
			utils.NotFound(w, err, fmt.Sprintf("Invalid URL Parameter %s", param))
		} else {
			utils.ServerError(w, err, "service.Find")
		}
		return
	}

	render.JSON(w, r, dto)
	tx.Commit()
}

// Post は、指定されたリソースを保存する。
func (c *Controller) Post(w http.ResponseWriter, r *http.Request) {
	var dto Dto
	err := render.DecodeJSON(r.Body, &dto)
	if err != nil {
		utils.BadRequest(w, err, "render.DecodeJSON")
		return
	}

	ctx := r.Context()
	tx, err := c.db.BeginTx(ctx, nil)
	if err != nil {
		utils.ServerError(w, err, "BeginTx")
		return
	}
	defer tx.Rollback()

	res, err := c.service.Create(ctx, tx, &dto)
	if err != nil {
		if err == utils.ErrConflict {
			utils.Conflict(w, err, "serice.Insert")
		} else {
			utils.ServerError(w, err, "service.Insert")
		}
		return
	}

	render.JSON(w, r, res)
	tx.Commit()
}

// Put は、指定されたリソースを更新する。
func (c *Controller) Put(w http.ResponseWriter, r *http.Request) {
	param := chi.URLParam(r, "customerId")
	id, err := strconv.Atoi(param)
	if err != nil {
		utils.BadRequest(w, err, fmt.Sprintf("Invalid URL Parameter %s", param))
		return
	}

	var dto Dto
	err = render.DecodeJSON(r.Body, &dto)
	if err != nil {
		utils.BadRequest(w, err, "render.DecodeJSON")
		return
	}
	if id != dto.Key().CustomerID {
		utils.BadRequest(w, err, fmt.Sprintf("Unmatch URL Parameter %s", param))
		return
	}

	ctx := r.Context()
	tx, err := c.db.BeginTx(ctx, nil)
	if err != nil {
		utils.ServerError(w, err, "BeginTx")
		return
	}
	defer tx.Rollback()

	res, err := c.service.Update(ctx, tx, &dto)
	if err != nil {
		utils.ServerError(w, err, "service.Insert")
		return
	}

	render.JSON(w, r, res)
	tx.Commit()
}

// Delete は、指定されたキーのリソースを削除する。
func (c *Controller) Delete(w http.ResponseWriter, r *http.Request) {
	param := chi.URLParam(r, "customerId")
	id, err := strconv.Atoi(param)
	if err != nil {
		utils.BadRequest(w, err, fmt.Sprintf("Invalid URL Parameter %s", param))
		return
	}

	ctx := r.Context()
	tx, err := c.db.BeginTx(ctx, nil)
	if err != nil {
		utils.ServerError(w, err, "BeginTx")
		return
	}
	defer tx.Rollback()

	err = c.service.Delete(ctx, tx, &Key{CustomerID: id})
	if err == sql.ErrNoRows {
		utils.NotFound(w, err, fmt.Sprintf("Invalid URL Parameter %s", param))
		return
	}
	if err != nil {
		utils.ServerError(w, err, "service.Delete")
		return
	}
	w.WriteHeader(http.StatusOK)
	tx.Commit()
}

utilsは、エラー発生時のログ出力とエラーのステータスコードを設定しているだけです。最後に全体のリポジトリを公開しますので、気になる方はそちらを参照してください。

Router

Routerは、RESTリソース毎の実装の頂点になります。といっても、go-chiのRouterにContorollerのメソッドを紐づけているだけです。

module/customers/router.go
package customers

import (
	"database/sql"

	"github.com/go-chi/chi/v5"
)

// NewRouter は、このモジュールのルーターを生成する。
func NewRouter(db *sql.DB) chi.Router {
	r := chi.NewRouter()
	c := NewController(db)

	r.Get("/{customerId}", c.Get)
	r.Post("/", c.Post)
	r.Put("/{customerId}", c.Put)
	r.Delete("/{customerId}", c.Delete)

	return r
}

次回は

アプリケーション全体をまとめます。

https://zenn.dev/robon/articles/af0dd9608ff4e7

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion