Create CRUD apps with Go with Mysql, GORM, Echo, Clean Architecture

Introduction

In this article, it is Clean Architecture. Use Mysql for DB, ʻEcho for framework, and GORMfor ORMapper. We will create an API withCR (U) D function`.

What to make

API with Create, Read, (Update), Delete functions Only Update is not implemented yet, so please add it yourself!

Target audience

Those who want to create a simple API after completing the Go environment construction

Technology used

Technology type
DB Mysql
ORM GORM
Framework Echo

table of contents

What is Clean Architecture?

When it comes to Clean Architecture, the following figure is very famous.

clean.jpeg

The purpose of Clean Architecture is ** separation of concerns ** What you need to be aware of to achieve this is ** dependency of each layer **.

Separation of concerns improves code readability and makes the design resistant to change. Please refer to Reference article for the advantages of this area and details of Clean Architecture.

In the figure above, the arrows are pointing from the outside to the inside of the circle, which is the ** dependent orientation **. It is possible to depend from the outside to the inside, but not from the inside to the outside.

In other words, ** things declared inside can be called from the outside **, but things declared outside ** cannot be called from the inside **.

In this article, I will introduce the code **, paying attention to the direction of dependence.

Regarding each function

The endpoints of each function are as follows

POST: /create GET: /users DELETE: /delete/:id

Directory structure

echoSample
│
│── src
│    ├── domain 
│    │    └── user.go
│    ├── infrastructure
│    │    ├── router.go
│    │    └── sqlHandler.go
│    ├── interfaces
│    │    ├── api
│    │        ├── context.go
│    │        └── user_controller.go
│    │    └── database
│    │        ├── sql_handler.go
│    │        └── user_repository.go
│    ├── usecase
│    │    ├── user_interactor.go
│    │    └── user_repository.go
│    ├── server.go
│        
└── docker-compose.yml 

Clean Arhictecture layer and directory structure this time You can compare as follows.

Directory name layer
domain Entities
usecase Use Cases
interface Controllers Presenters
infrastructure External Interfaces

domain Entity is defined in the domain layer. Since it is in the center, it can be called from any layer. entitys.png

src/domain/user.go


package domain

type User struct {
	ID   int    `json:"id" gorm:"primary_key"`
	Name string `json:"name"`
}

This time, create ʻUser` with ID and Name in the column and set id as the primary key.

About json:" id "gorm:" primary_key "

json:" id " has json mapping gorm:" primary_key " tags the model with gorm. Of course, in addition to primary_key, you can do the same as defining a table with sql, such as not null unique`` default.

reference GORM Declearing Model

infrastructure

Outermost ** Infrastructure ** I will write the part where the application is related to the outside. In this example, ** Connection with DB ** and ** Router ** are defined here. framework.png

Since it is the outermost layer, you can call it without being aware of any layer.

src/infrastucture/sqlhandler.go


package infrastructure

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"echoSample/src/interfaces/database"
)

type SqlHandler struct {
	db *gorm.DB
}

func NewSqlHandler() database.SqlHandler {
	dsn := "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err.Error)
	}
	sqlHandler := new(SqlHandler)
	sqlHandler.db = db
	return sqlHandler
}

func (handler *SqlHandler) Create(obj interface{}) {
	handler.db.Create(obj)
}

func (handler *SqlHandler) FindAll(obj interface{}) {
	handler.db.Find(obj)
}

func (handler *SqlHandler) DeleteById(obj interface{}, id string) {
	handler.db.Delete(obj, id)
}

The official document was helpful around the DB connection.

gorm.io

Next is routing. This time I'm using the web framework Echo. I also referred to the official document. It defines API Method and path. echo.labstack

src/infrastructure/router.go


package infrastructure

import (
	controllers "echoSample/src/interfaces/api"
	"net/http"

	"github.com/labstack/echo"
)

func Init() {
	// Echo instance
	e := echo.New()
	userController := controllers.NewUserController(NewSqlHandler())

	e.GET("/users", func(c echo.Context) error {
		users := userController.GetUser() 
		c.Bind(&users) 
		return c.JSON(http.StatusOK, users)
	})

	e.POST("/create", func(c echo.Context) error {
		userController.Create(c)
		return c.String(http.StatusOK, "created")
	})

	e.DELETE("/delete/:id", func(c echo.Context) error {
		id := c.Param("id")
		userController.Delete(id)
		return c.String(http.StatusOK, "deleted")
	})

	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}


interfaces

Layers of Controllers Presenters.

From here on, you need to be ** aware of dependencies **.

framework-controller.png

There is no problem with calling ** interface layer ** to ** domain layer ** and ** usecase layer **.

You cannot call the ** infrastructure layer **, so define the interface instead of calling it directly. (It's complicated, but it's the interface of sqlHandler defined in the inrastrucure layer)

src/interfaces/api/user_controller.go


package controllers

import (
	"echoSample/src/domain"
	"echoSample/src/interfaces/database"
	"echoSample/src/usecase"

	"github.com/labstack/echo"
)

type UserController struct {
	Interactor usecase.UserInteractor
}

func NewUserController(sqlHandler database.SqlHandler) *UserController {
	return &UserController{
		Interactor: usecase.UserInteractor{
			UserRepository: &database.UserRepository{
				SqlHandler: sqlHandler,
			},
		},
	}
}

func (controller *UserController) Create(c echo.Context) {
	u := domain.User{}
	c.Bind(&u)
	controller.Interactor.Add(u)
	createdUsers := controller.Interactor.GetInfo()
	c.JSON(201, createdUsers)
	return
}

func (controller *UserController) GetUser() []domain.User {
	res := controller.Interactor.GetInfo()
	return res
}

func (controller *UserController) Delete(id string) {
	controller.Interactor.Delete(id)
}

There is no problem because the controller calls from the usecase layer and domain layer.

src/interfaces/api/context.go


package controllers

type Context interface {
	Param(string) string
	Bind(interface{}) error
	Status(int)
	JSON(int, interface{})
}

database related

src/interfaces/database/user_repository.go


package database
package database

import (
	"echoSample/src/domain"
)

type UserRepository struct {
	SqlHandler
}

func (db *UserRepository) Store(u domain.User) {
	db.Create(&u)
}

func (db *UserRepository) Select() []domain.User {
	user := []domain.User{}
	db.FindAll(&user)
	return user
}
func (db *UserRepository) Delete(id string) {
	user := []domain.User{}
	db.DeleteById(&user, id)
}

In the repository, we call sqlHandler, but instead of calling things in the infrastructure layer directly It is called through the ** sqlhandler interface ** defined in the same hierarchy.

This is called the ** Dependency Inversion Principle **.

src/interfaces/db/sql_handler.go


package database

type SqlHandler interface {
	Create(object interface{})
	FindAll(object interface{})
	DeleteById(object interface{}, id string)
}

Now you can call the process of sql_handler.

usecase Last but not least, the usecase layer.

controller-usecase.png

src/usecase/user_interactor.go


package usecase

import "echoSample/src/domain"

type UserInteractor struct {
	UserRepository UserRepository
}

func (interactor *UserInteractor) Add(u domain.User) {
	interactor.UserRepository.Store(u)
}

func (interactor *UserInteractor) GetInfo() []domain.User {
	return interactor.UserRepository.Select()
}

func (interactor *UserInteractor) Delete(id string) {
	interactor.UserRepository.Delete(id)
}

Again, we need to apply the Dependency Inversion Principle as before.

So define user_repository.go.

src/usecase/user_repository.go


package usecase

import (
	"echoSample/src/domain"
)

type UserRepository interface {
	Store(domain.User)
	Select() []domain.User
	Delete(id string)
}

This completes the implementation.

After that, start mysql with docker-compose.yml, start the server, and it should work.

docker-compose.yml


version: "3.6"
services:
  db:
    image: mysql:5.7
    container_name: go_sample
    volumes:
      #mysql settings
      - ./mysql/conf:/etc/mysql/conf.d
      - ./mysql/data:/var/lib/mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: go_sample
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: root
      TZ: "Asia/Tokyo"

src/server.go


package main

import (
	"echoSample/src/domain"
	"echoSample/src/infrastructure"

	"github.com/labstack/echo/v4"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var (
	db  *gorm.DB
	err error
	dsn = "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
)

func main() {
	dbinit()
	infrastructure.Init()
	e := echo.New()
	e.Logger.Fatal(e.Start(":1323"))
}

func dbinit() {
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
	}
	db.Migrator().CreateTable(domain.User{})
}


Start mysql with the following command

docker-compose up -d

Server startup

go run server.go

Operation check

POST: /create

curl --location --request POST 'localhost:1323/create' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"J.Y Park"
}'

curl --location --request POST 'localhost:1323/create' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"Eron Mask"
}'


docker-compose

created%                                                                                                                                                                                                               

GET: /users

curl --location --request GET 'localhost:1323/users'
[{"id":1,"name":"J.Y Park"},{"id":2,"name":"Eron Mask"}]

DELETE: /delete/:id

curl --location --request DELETE 'localhost:1323/delete/1'
deleted

get and check

{"id":2,"name":"Eron Mask"}]

Summary

Not only reading the article about Clean Architecture, but actually making a simple API and checking the operation will deepen your understanding.

However, my honest impression was that I couldn't experience the appeal of Clean Architecture on the scale of a CRUD app.

Clean Architecture not only improves the readability and productivity of the code, but also has the property of being ** resistant to change **, so I would like to add various things to the product I made this time and realize the goodness. ...!

Reference article

The most easy-to-understand Clean Architecture in the world Clean Architecture (Translation) Try building API Server with Clean Architecture Building Clean Architecture API with Golang --Echo and GORM

Recommended Posts

Create CRUD apps with Go with Mysql, GORM, Echo, Clean Architecture
Create a development environment for Go + MySQL + nginx with Docker (docker-compose)
Go language server sample code with Clean Architecture-like configuration (using echo, wire)
GO gorm DB creation INSERT JSONUnmarshal Mysql