Using SQLBoiler and Wire in a Layered Architecture with Go
iekderaka
Posted on August 18, 2024
0. To start with
I wrote this article as I implemented a layered architecture while learning how to use Wire
.
Before this task, the Docker Compose file has been set up with the Go backend container, MySQL, and Adminer.
Additionally, the setup for SQLBoiler and Wire has also been completed.
This is the directory structure.
| .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
By configuring docker-entrypoint-initdb.d in the docker-compose.yaml file, tables are set to be created automatically.
CREATE TABLE books (
id INT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
2. create sqlboiler files
run sqlboiler mysql
The files are generated in the location specified in sqlboiler.toml
.
3. implement NewDB
I implement the database connection method to be called in the infrastructure layer.
/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. implement domain, usecase, infrastructure and interface
/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
For APIs categorized under /book, they will be implemented in this handler.
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
I create router.go with the expectation of generating multiple APIs.
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. implement wire.go
I implement wire.go to manage the implemented files with dependency injection. Since router.go returns e
, this part will also be included in wire.go.
It looks like the //go:build wireinject
directive ensures that the file is included only during the code generation phase with Wire and is excluded from the final build.
//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. create wire_gen.go
wire_gen.go is an automatically generated file based on the dependencies specified in wire.go.
run wire
in the /wire
.
wire.go
will be generated like this.
// 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
In the main.go
file, it simply calls the InitializeEcho() function from wire_gen.go
.
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. Confirmation
I was able to confirm that the data was successfully saved to the database after sending a request via the API.
9. In conclusion
Thank you for reading. In this post, I created a simple API using Wire and SQLBoiler within a layered architecture. I also learned how Wire can simplify managing dependencies, even as they become more complex. If you notice any mistakes, please feel free to let me know.
Posted on August 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.