Using SQLBoiler and Wire in a Layered Architecture with Go



This content originally appeared on DEV Community and was authored by iekderaka

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
|   |           rooter.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 created on /sqlboiler

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. Confirmation

I was able to confirm that the data was successfully saved to the database after sending a request via the API.

API request

db

8. 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.


This content originally appeared on DEV Community and was authored by iekderaka