🏃♀️
GolangでREST(その2)
前回は
全体の設計方針と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
}
次回は
アプリケーション全体をまとめます。
GitHubで編集を提案
Discussion