1
1

Goの「wire」と「sqlboiler」でレイヤードアーキテクチャを実装してみる

Last updated at Posted at 2024-08-18

0. はじめに

wireを勉強中なので、レイヤードアーキテクチャーでの使い方を記事として書こうと思います。
今回の記事では、goのバックエンドコンテナ、mysql,adminerを利用しており、docker-composeファイルで管理していました。
またwiresqlboilerの基本的なセットアップについてもすでに行った状態で記事をはじめます。

ディレクトリ構造

|   .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.gowire.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にリクエストを送り、データベースに保存できたことを確認しました。

apireq1.png
adminer.png

9. 最後に

読んでいただきありがとうございます。この記事では、レイヤードアーキテクチャの中で、WireとSQLBoilerを使ってシンプルなAPIを作成しました。Wireを使用することで、依存関係が複雑になっても管理を簡単にできることを学びました。もし間違いありましたら、ぜひお知らせください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1