Using SQLBoiler and Wire in a Layered Architecture with Go

iekderaka - Aug 18 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

4. implement domain, usecase, infrastructure and interface

/domain/entity/book.go

package entity

type Book struct {
    Id int
    Name string
}


Enter fullscreen mode Exit fullscreen mode

/domain/repository/book.go

package repository

import "main/domain/entity"

type BookRepository interface {
    Save(book *entity.Book) error
}

Enter fullscreen mode Exit fullscreen mode

/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
}

Enter fullscreen mode Exit fullscreen mode

/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
}

Enter fullscreen mode Exit fullscreen mode

/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)
}

Enter fullscreen mode Exit fullscreen mode

/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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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"))
}

Enter fullscreen mode Exit fullscreen mode

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

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.

.
Terabox Video Player