0. はじめに
wireを勉強中なので、レイヤードアーキテクチャーでの使い方を記事として書こうと思います。
今回の記事では、goのバックエンドコンテナ、mysql,adminerを利用しており、docker-composeファイルで管理していました。
またwire
とsqlboiler
の基本的なセットアップについてもすでに行った状態で記事をはじめます。
ディレクトリ構造
| .gitignore
| docker-compose.yml
| README.md
|
+---backend
| | .air.toml
| | Dockerfile
| | go.mod
| | go.sum
| | main.go
| | sqlboiler.toml
| |
| +---domain
| | +---entity
| | | book.go
| | |
| | \---repository
| | book.go
| |
| +---infrastructure
| | \---repositoryImpl
| | book.go
| |
| +---interface
| | \---handler
| | book.go
| | router.go
| |
| +---mysql
| | db.go
| |
| +---sqlboiler (auto generated)
| | boil_queries.go
| | boil_table_names.go
| | boil_types.go
| | books.go
| | mysql_upsert.go
| |
| |
| +---usecase
| | book_repository.go
| |
| \---wire
| wire.go
| wire_gen.go
|
\---initdb
init.sql
1. initdb/init.sql
docker-compose.yamlのdocker-entrypoint-initdb.dで初期化のsqlを自動で読み込まれるように設定しました。
CREATE TABLE books (
id INT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
2. sqlboilerファイルの生成
sqlboiler mysql
を実行します
sqlboiler.toml
で設定した場所に生成されます
3. NewDBの定義
インフラ層で呼ばれるデータベースとの接続用の関数を定義します。
/mysql/db.go
package mysql
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type DBConfig struct {
User string
Password string
Host string
Port int
DBName string
}
func NewDB() (*sql.DB, error) {
cfg := DBConfig{
User: "sample",
Password: "sample",
Host: "sample",
Port: 3306,
DBName: "sample",
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
4. ドメイン層、ユースケース層、インフラ層、インターフェイス層の実装
/domain/entity/book.go
package entity
type Book struct {
Id int
Name string
}
/domain/repository/book.go
package repository
import "main/domain/entity"
type BookRepository interface {
Save(book *entity.Book) error
}
/usecase/book.go
package usecase
import (
"main/domain/entity"
"main/domain/repository"
)
type BookUsecase interface {
Save(book *entity.Book) error
}
type bookUsecaseImpl struct {
bookRepository repository.BookRepository
}
func NewBookUsecaseImpl(br repository.BookRepository) BookUsecase {
return &bookUsecaseImpl{bookRepository: br}
}
func (bu *bookUsecaseImpl) Save(book *entity.Book) error {
if err := bu.bookRepository.Save(book); err != nil {
return err
}
return nil
}
/infrastructure/repositoryImpl/book.go
package repositoryImpl
import (
"context"
"database/sql"
"main/domain/entity"
"main/domain/repository"
"main/sqlboiler"
"github.com/volatiletech/sqlboiler/v4/boil"
)
type bookRepositoryImpl struct {
db *sql.DB
}
func NewBookRepositoryImpl(db *sql.DB) repository.BookRepository {
return &bookRepositoryImpl{db: db}
}
func (br *bookRepositoryImpl) Save(book *entity.Book) error {
bookModel := &sqlboiler.Book{
ID: book.Id,
Name: book.Name,
}
err := bookModel.Insert(context.Background(), br.db, boil.Infer())
if err != nil {
return err
}
return nil
}
/interface/handler/book.go
/book
で分けることのできるAPIはこのハンドラーで管理します。
package handler
import (
"main/domain/entity"
"main/usecase"
"net/http"
"github.com/labstack/echo/v4"
)
type BookHandler struct {
bookUsecase usecase.BookUsecase
}
func (bh *BookHandler) RegisterRoutes(e *echo.Echo) {
e.POST("/book", bh.SaveBook)
}
func NewBookHandler(bu usecase.BookUsecase) *BookHandler {
return &BookHandler{bookUsecase: bu}
}
func (bh *BookHandler) SaveBook(c echo.Context) error {
book := new(entity.Book)
if err := c.Bind(book); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := bh.bookUsecase.Save(book); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, book)
}
/interface/handler/router.go
複数APIで使う場合もあるため、それぞれのハンドラーをまとめるrouter.go
も作成します。
package handler
import (
"github.com/labstack/echo/v4"
)
func RegisterRoutes(e *echo.Echo, bookHandler *BookHandler) {
bookHandler.RegisterRoutes(e)
}
func NewEchoInstance(bookHandler *BookHandler) *echo.Echo {
e := echo.New()
RegisterRoutes(e, bookHandler)
return e
}
5. wire.goの実装
DIを管理するwire.go
を実装します。 router.go
でechoのインスタンスまで返しているのでwire.go
ではここまで含めます。
//go:build wireinject
は最終的なビルド時に含まれないように書いておくそうです。
//go:build wireinject
package wire
import (
"main/infrastructure/repositoryImpl"
"main/interface/handler"
"main/mysql"
"main/usecase"
"github.com/google/wire"
"github.com/labstack/echo/v4"
)
func InitializeEcho() (*echo.Echo, error) {
wire.Build(
mysql.NewDB,
repositoryImpl.NewBookRepositoryImpl,
usecase.NewBookUsecaseImpl,
handler.NewBookHandler,
handler.NewEchoInstance,
)
return nil, nil
}
6. wire_gen.goの生成
wire_gen.go
はwire.go
から自動生成されるファイルです。
wire
を/wire
で実行してください。
wire_gen.go
が生成されます。
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package wire
import (
"github.com/labstack/echo/v4"
"main/infrastructure/repositoryImpl"
"main/interface/handler"
"main/mysql"
"main/usecase"
)
// Injectors from wire.go:
func InitializeEcho() (*echo.Echo, error) {
db, err := mysql.NewDB()
if err != nil {
return nil, err
}
bookRepository := repositoryImpl.NewBookRepositoryImpl(db)
bookUsecase := usecase.NewBookUsecaseImpl(bookRepository)
bookHandler := handler.NewBookHandler(bookUsecase)
echoEcho := handler.NewEchoInstance(bookHandler)
return echoEcho, nil
}
7. main.go
main.goではwire_gen.goのInitializeEcho()を呼び出すだけです。
package main
import (
"log"
"main/wire"
)
func main() {
e, err := wire.InitializeEcho()
if err != nil {
log.Fatal(err)
}
e.Logger.Fatal(e.Start(":8000"))
}
8. 確認
APIにリクエストを送り、データベースに保存できたことを確認しました。
9. 最後に
読んでいただきありがとうございます。この記事では、レイヤードアーキテクチャの中で、WireとSQLBoilerを使ってシンプルなAPIを作成しました。Wireを使用することで、依存関係が複雑になっても管理を簡単にできることを学びました。もし間違いありましたら、ぜひお知らせください。