J'ai essayé de créer un serveur API avec Go (Echo) x MySQL x Docker x Clean Architecture

introduction

Comme le titre l'indique, j'ai créé un serveur API avec Go x MySQL x Docker. au début,

・ Créez une API REST simple avec Go ・ Mettre en place un environnement de développement avec Docker

Je le faisais dans le but de, mais quand je me demandais quoi faire avec la structure de répertoire et que je recherchais diverses choses, j'ai trouvé de nombreux articles qui fabriquent des serveurs API avec une architecture Go × clean, alors j'ai décidé d'essayer une architecture propre. Je l'ai essayé.

Donc cet article est

・ Je souhaite écrire une API dans Go ・ Je souhaite créer un environnement de développement avec Docker ・ A cette époque, j'aimerais adopter une architecture épurée

Recommandé pour ceux qui ont une sensation de température.

Au contraire, cela peut ne pas être très utile pour ceux qui souhaitent étudier une architecture solide et propre. ..

De plus, il convient de noter ** Echo ** dans le framework (https://echo.labstack.com/) ** Gorm ** (https://github.com/go-gorm/gorm) pour accéder à la base de données en utilisant.

Cliquez ici pour le référentiel ↓

Préparation

Environnement de développement

J'ai construit un environnement de développement avec Docker.

Dockerfile Référence officielle Dockerfile est une bonne référence pour savoir comment écrire un Dockerfile basique.

Maintenant, regardons-les rapidement dans l'ordre.

Le premier est Go.

docker/api/Dockerfile


FROM golang:1.14

#utiliser le module go
ENV GO111MODULE=on 

#Spécifiez le répertoire pour exécuter l'application
WORKDIR /go/src/github.com/Le0tk0k/go-rest-api

#Allez dans le répertoire ci-dessus.mod et c'est parti.Copier la somme
COPY go.mod go.sum ./
#S'il n'y a pas de changement dans le fichier ci-dessus, le cache peut être utilisé
RUN go mod download

COPY . .
RUN go build .

RUN go get github.com/pilu/fresh

EXPOSE 8080

#Démarrez le serveur avec la nouvelle commande
CMD ["fresh"]

Activation du rechargement à chaud sur github.com/pilu/fresh. À part cela, il n'y a rien de spécial à ce sujet.

référence ・ Utilisation du téléchargement de go mod pour accélérer les versions de Golang DockerGo v1.11 + Docker + fresh pour créer un environnement de développement de rechargement à chaud et avoir une vie de langage Go agréable

Vient ensuite MySQL.

docker/mysql/Dockerfile


FROM mysql

EXPOSE 3306

#Copiez le fichier de configuration MySQL dans l'image
COPY ./docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf

CMD ["mysqld"]

Le fichier de configuration MySQL ressemble à ceci: Puisque la méthode d'authentification au moment de la connexion a changé dans MySQL 8.0 ou version ultérieure, il semble que la description sur la deuxième ligne soit nécessaire. En outre, le code de caractère a été modifié pour éviter les caractères déformés.

docker/mysql/my.cnf


[mysqld]
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4

[client]
default-character-set=utf8mb4

Si vous placez le SQL et le script d'initialisation dans le répertoire /docker-entrypoint-initdb.d/, les données peuvent être initialisées automatiquement lorsque l'image est démarrée pour la première fois.

Alors, créons une table.

docker/mysql/db/init.sql


CREATE DATABASE IF NOT EXISTS go_rest_api;
USE go_rest_api;

CREATE TABLE IF NOT EXISTS users (
  id          INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
  name        VARCHAR(256) NOT NULL,
  age         INT NOT NULL,
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  updated_at  TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

référence

docker-compose.yml Vous pouvez également consulter Compose File / Official Reference.

docker-compose.yml


version: '3'
services: 
  db:
    build: 
      #Spécifiez le chemin du répertoire où se trouve le Dockerfile
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    ports: 
      #Spécifiez le port public
      - "3306:3306"
    volumes: 
      #Initialiser les données au démarrage
      - ./docker/mysql/db:/docker-entrypoint-initdb.d
      #persistance mysql
      - ./docker/mysql/db/mysql_data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: go_rest_api
      MYSQL_USER: user
      MYSQL_PASSWORD: password

  api:
    build:
      context: .
      dockerfile: ./docker/api/Dockerfile
    volumes: 
      - ./:/go/src/github.com/Le0tk0k/go-rest-api
    ports: 
      - "8080:8080"
    depends_on:
      #db commence en premier
      - db

Puisque mysql est rendu persistant, les données précédentes resteront même si le conteneur est redémarré.

De plus, en spécifiant depend_on pour api, db démarrera en premier, puis api démarrera. Cependant, depend_on spécifie uniquement l'ordre de démarrage et n'attend pas qu'il soit terminé, une autre mesure est donc requise. Les contre-mesures sont présentées dans la section mise en œuvre.

référence

Une architecture propre

Clean Architecture est une architecture proposée dans The Clean Architecture.

Suis-je une architecture en premier lieu? Quoi ça? C'était dans un état, mais après avoir lu l'article suivant, je pense avoir compris environ 1 mm de surnageant. Il existe de nombreux autres articles, alors n'hésitez pas à les consulter!

Référence (il existe également des articles autres que l'architecture propre)

la mise en oeuvre

Structure du répertoire

La structure des répertoires est la suivante.


├── docker
│   ├── api
│   │   └── Dockerfile
│   └── mysql
│       ├── Dockerfile
│       ├── db
│       │   ├── init.sql
│       │   └── mysql_data
│       └── my.cnf
├── docker-compose.yml
├── domain
│   └── user.go
├── infrastructure
│   ├── router.go
│   └── sqlhandler.go
├── interfaces
│   ├── controllers
│   │   ├── context.go
│   │   ├── error.go
│   │   └── user_controller.go
│   └── database
│       ├── sql_handler.go
│       └── user_repository.go
|── usecase
│   ├── user_interactor.go
│   └── user_repository.go
├── server.go
├── go.mod
├── go.sum

domaine → Couche d'entités infrastructure → Couche Frameworks & Drivers interfaces → Couche d'interface usecase → Couche de cas d'utilisation

Cela correspond à.

Domaine (couche d'entités)

La couche la plus interne, indépendante de quoi que ce soit. Nous définirons le modèle utilisateur.

Pour json:" - ", si vous écrivez-, il ne sera pas affiché.

domain/user.go


package domain

import "time"

type User struct {
	ID        int       `gorm:"primary_key" json:"id"`
	Name      string    `json:"name"`
	Age       int       `json:"age"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type Users []User

Interfaces / base de données, infrastructure (couche d'interface, couche Frameworks et pilote)

Ensuite, nous allons implémenter la zone de base de données. La connexion à la base de données étant une connexion externe, définissez-la dans la couche Infrastructure la plus externe.

infrastructure/sqlhandler.go


package infrastructure

import (
	"fmt"
	"time"

	"github.com/Le0tk0k/go-rest-api/interfaces/database"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

type SqlHandler struct {
	Conn *gorm.DB
}

func NewMySqlDb() database.SqlHandler {

	connectionString := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
		"user",
		"password",
		"db",
		"3306",
		"go_rest_api",
	)

	conn, err := open(connectionString, 30)
	if err != nil {
		panic(err)
	}

    //Vérifiez si vous pouvez vous connecter
	err = conn.DB().Ping()
	if err != nil {
		panic(err)
	}

    //Détails du journal de sortie
	conn.LogMode(true)
    //Configurer le moteur DB
	conn.Set("gorm:table_options", "ENGINE=InnoDB")

	sqlHandler := new(SqlHandler)
	sqlHandler.Conn = conn

	return sqlHandler
}

//Assurez-vous que le conteneur d'API démarre après avoir confirmé le démarrage de MySQL
func open(path string, count uint) (*gorm.DB, error) {
	db, err := gorm.Open("mysql", path)
	if err != nil {
		if count == 0 {
			return nil, fmt.Errorf("Retry count over")
		}
		time.Sleep(time.Second)
		count--
		return open(path, count)
	}
	return db, nil
}

func (handler *SqlHandler) Find(out interface{}, where ...interface{}) *gorm.DB {
	return handler.Conn.Find(out, where...)
}

func (handler *SqlHandler) Create(value interface{}) *gorm.DB {
	return handler.Conn.Create(value)
}

func (handler *SqlHandler) Save(value interface{}) *gorm.DB {
	return handler.Conn.Save(value)
}

func (handler *SqlHandler) Delete(value interface{}) *gorm.DB {
	return handler.Conn.Delete(value)
}

Dans opan (), le conteneur api est configuré pour démarrer après avoir attendu le démarrage du conteneur db. Cette fois, j'ai implémenté cette fonctionnalité dans le code Go, mais est-il préférable de l'implémenter dans un script shell? Je ne connais pas la meilleure pratique ... référence

De plus, la couche infrastructure n'est qu'une connexion db et le traitement proprement dit est implémenté dans la couche interfaces / base de données.

interfaces/database/user_repository.go


package database

import (
	"github.com/Le0tk0k/go-rest-api/domain"
)

type UserRepository struct {
	SqlHandler
}

func (userRepository *UserRepository) FindByID(id int) (user domain.User, err error) {
	if err = userRepository.Find(&user, id).Error; err != nil {
		return
	}
	return
}

func (userRepository *UserRepository) Store(u domain.User) (user domain.User, err error) {
	if err = userRepository.Create(&u).Error; err != nil {
		return
	}
	user = u
	return
}

func (userRepository *UserRepository) Update(u domain.User) (user domain.User, err error) {
	if err = userRepository.Save(&u).Error; err != nil {
		return
	}
	user = u
	return
}

func (userRepository *UserRepository) DeleteByID(user domain.User) (err error) {
	if err = userRepository.Delete(&user).Error; err != nil {
		return
	}
	return
}

func (userRepository *UserRepository) FindAll() (users domain.Users, err error) {
	if err = userRepository.Find(&users).Error; err != nil {
		return
	}
	return
}

J'intègre un SqlHandler dans le UserRepository. J'importe la couche de domaine, qui est la couche la plus interne, donc les dépendances sont préservées.

Cependant, puisque SqlHandler est défini dans la couche Infrastructure la plus externe, les dépendances sont dirigées de l'intérieur vers l'extérieur et les règles peuvent ne pas être suivies. Cependant, le SqlHandler appelé ici n'est pas une structure définie dans la couche Infrastructure, mais une interface définie dans la couche interface / base de données, qui est la même couche. Ceci est appelé ** DIP (Dependency Reversal Principle) **.

Cela signifie que dans interfaces / database / user_repository.go, si vous appelez le processus défini dans la couche Infrastructure, la dépendance changera de l'intérieur vers l'extérieur et vous ne pourrez pas protéger la dépendance. Afin d'éviter cela, définissons l'interaction avec DB dans la même couche avec interface et appelons-la. Avec cela, bien que le traitement soit réellement effectué dans la couche Infrastructure, la dépendance est protégée car les interfaces / database / user_repository.go appelle l'interface SqlHandler.

Et dans Go, quand ** type T a toutes les méthodes définies dans l'interface I, cela signifie que l'interface I est implémentée **, donc l'interface SqlHandler des interfaces / databases / sql_hander dans la structure Infrastructure.SqlHandler. Définit la méthode définie dans.

interfaces/database/sql_handler.go


package database

import "github.com/jinzhu/gorm"

type SqlHandler interface {
	Find(interface{}, ...interface{}) *gorm.DB
	Create(interface{}) *gorm.DB
	Save(interface{}) *gorm.DB
	Delete(interface{}) *gorm.DB
}

Cas d'utilisation (couche de cas d'utilisation)

La couche Usecase est chargée de recevoir les informations de la couche interfaces / base de données et de les transmettre à la couche interfaces / contrôleur.

usecase/user_interactor.go


package usecase

import "github.com/Le0tk0k/go-rest-api/domain"

type UserInteractor struct {
	UserRepository UserRepository
}

func (interactor *UserInteractor) UserById(id int) (user domain.User, err error) {
	user, err = interactor.UserRepository.FindByID(id)
	return
}

func (interactor *UserInteractor) Users() (users domain.Users, err error) {
	users, err = interactor.UserRepository.FindAll()
	return
}

func (interactor *UserInteractor) Add(u domain.User) (user domain.User, err error) {
	user, err = interactor.UserRepository.Store(u)
	return
}

func (interactor *UserInteractor) Update(u domain.User) (user domain.User, err error) {
	user, err = interactor.UserRepository.Update(u)
	return
}

func (interactor *UserInteractor) DeleteById(user domain.User) (err error) {
	err = interactor.UserRepository.DeleteByID(user)
	return
}

UserInteractor appelle UserRepository, mais si vous l'appelez depuis la couche Interfase, cela dépendra de l'intérieur → de l'extérieur et vous ne pourrez pas protéger la dépendance, alors créez user_repository.go dans la même couche et implémentez l'interface * Protégez les dépendances en utilisant * DIP (Dependency Reversal Principle) **.

usecase/user_repository.go


package usecase

import "github.com/Le0tk0k/go-rest-api/domain"

type UserRepository interface {
	FindByID(id int) (domain.User, error)
	Store(domain.User) (domain.User, error)
	Update(domain.User) (domain.User, error)
	DeleteByID(domain.User) error
	FindAll() (domain.Users, error)
}

Interfaces / contrôleurs, infrastructure (couche d'interface, couche Frameworks et pilote)

Ensuite, nous mettrons en œuvre le contrôleur et le routeur.

interfaces/controllers/user_controller.go


package controllers

import (
	"strconv"

	"github.com/Le0tk0k/go-rest-api/domain"
	"github.com/Le0tk0k/go-rest-api/interfaces/database"
	"github.com/Le0tk0k/go-rest-api/usecase"
)

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) CreateUser(c Context) (err error) {
	u := domain.User{}
	c.Bind(&u)
	user, err := controller.Interactor.Add(u)

	if err != nil {
		c.JSON(500, NewError(err))
		return
	}
	c.JSON(201, user)
	return
}

func (controller *UserController) GetUsers(c Context) (err error) {
	users, err := controller.Interactor.Users()

	if err != nil {
		c.JSON(500, NewError(err))
		return
	}
	c.JSON(200, users)
	return
}

func (controller *UserController) GetUser(c Context) (err error) {
	id, _ := strconv.Atoi(c.Param("id"))
	user, err := controller.Interactor.UserById(id)

	if err != nil {
		c.JSON(500, NewError(err))
		return
	}
	c.JSON(200, user)
	return
}

func (controller *UserController) UpdateUser(c Context) (err error) {
	id, _ := strconv.Atoi(c.Param("id"))
	u := domain.User{ID: id}
	c.Bind(&u)

	user, err := controller.Interactor.Update(u)

	if err != nil {
		c.JSON(500, NewError(err))
		return
	}
	c.JSON(201, user)
	return
}

func (controller *UserController) DeleteUser(c Context) (err error) {
	id, _ := strconv.Atoi(c.Param("id"))
	user := domain.User{ID: id}

	err = controller.Interactor.DeleteById(user)
	if err != nil {
		c.JSON(500, NewError(err))
		return
	}
	c.JSON(200, user)
	return
}

Puisque echo utilise echo.Context, définissez l'interface de la méthode utilisée cette fois.

interfaces/controllers/context.go


package controllers

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

interfaces/controllers/error.go


package controllers

type Error struct {
	Message string
}

func NewError(err error) *Error {
	return &Error{
		Message: err.Error(),
	}
}

Étant donné que le routage utilise l'écho (à l'aide d'un package externe), implémentez-le dans la couche Infrastructure. Le type echo.Context peut être passé en argument dans uesrController. Fonction (c) car echo.Context satisfait l'interface de contexte d'interfaces / controllers / context.go. (Autrement dit, le type echo.Context a toutes les méthodes de l'interface de contexte.)

infrastructure/router.go


package infrastructure

import (
	"github.com/Le0tk0k/go-rest-api/interfaces/controllers"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func Init() {
	e := echo.New()

	userController := controllers.NewUserController(NewMySqlDb())

    //Sortie de journaux basés sur les demandes, tels que les journaux d'accès
	e.Use(middleware.Logger())
    //Si vous paniquez accidentellement quelque part dans votre application, le serveur se rétablira afin de pouvoir renvoyer une réponse d'erreur sans le laisser tomber.
	e.Use(middleware.Recover())

	e.GET("/users", func(c echo.Context) error { return userController.GetUsers(c) })
	e.GET("/users/:id", func(c echo.Context) error { return userController.GetUser(c) })
	e.POST("/users", func(c echo.Context) error { return userController.CreateUser(c) })
	e.PUT("/users/:id", func(c echo.Context) error { return userController.UpdateUser(c) })
	e.DELETE("/users/:id", func(c echo.Context) error { return userController.DeleteUser(c) })

	e.Logger.Fatal(e.Start(":8080"))
}

Enfin, appelez-le depuis server.go.

server.go


package main

import "github.com/Le0tk0k/go-rest-api/infrastructure"

func main() {
	infrastructure.Init()
}

Exécution

Passons maintenant à l'API terminée. Cette fois, j'aimerais utiliser ** Postman **. (https://www.postman.com/downloads/)

Tout d'abord, définissez Content-Type of Headers sur application / json. スクリーンショット 2020-09-19 16.09.22.png

GetUsers Essayez d'obtenir tous les utilisateurs. Envoyez une requête GET à / users. スクリーンショット 2020-09-19 16.14.07.png

Getuser Essayez d'obtenir n'importe quel utilisateur. Envoyez une requête GET à / users /: id. スクリーンショット 2020-09-19 16.16.59.png

CreateUser Commençons par créer un utilisateur. Envoyez une requête POST à / users. スクリーンショット 2020-09-19 16.11.47.png

UpdateUser Essayez de mettre à jour n'importe quel utilisateur. Envoyez une requête PUT à / users /: id. スクリーンショット 2020-09-19 16.18.02.png

DeleteUser Essayez de supprimer n'importe quel utilisateur. Envoyez une demande DELETE à / users /: id. スクリーンショット 2020-09-19 16.19.14.png

Lorsque je le vérifie avec GET, il est supprimé fermement. スクリーンショット 2020-09-19 16.19.24.png

Ça marche bien!

enfin

J'aurais dû juste mettre en place un environnement de développement avec Docker et créer une API REST, mais c'était difficile d'étudier l'architecture propre, mais je pense que c'était une excellente opportunité de connaître l'architecture. Au contraire, avec une petite API comme celle-ci, cela semble compliqué, mais j'avais l'impression que ce serait bien si c'était à grande échelle. (~~ En fait, je ne comprends pas grand-chose ~~) J'aimerais à nouveau étudier le design!

Recommended Posts

J'ai essayé de créer un serveur API avec Go (Echo) x MySQL x Docker x Clean Architecture
J'ai essayé de créer un environnement de serveur UML Plant avec Docker
Environnement Build Rails (API) x MySQL x Nuxt.js avec Docker
J'ai essayé de lier le chat avec le serveur de Minecraft avec l'API Discord
J'ai essayé de créer un environnement de développement http2 avec Eclipse + Tomcat
J'ai essayé de vérifier AdoptOpenJDK 11 (11.0.2) avec l'image Docker
[Rails] Comment créer un environnement avec Docker
Comment créer un environnement [TypeScript + Vue + Express + MySQL] avec Docker ~ Vue ~
J'ai démarré MySQL 5.7 avec docker-compose et j'ai essayé de me connecter
J'ai essayé de dessiner une animation avec l'API Blazor + canvas
Rails6 J'ai essayé d'introduire Docker dans une application existante
Comment créer un environnement [TypeScript + Vue + Express + MySQL] avec Docker ~ Express ~
J'ai essayé de construire l'environnement petit à petit en utilisant docker
J'ai essayé de créer un environnement de WSL2 + Docker + VSCode
J'ai essayé BIND avec Docker
J'ai essayé de créer un environnement de développement padrino avec Docker
02. J'ai créé une API pour me connecter de Spring Boot à MySQL (My Batis)
J'ai essayé de me connecter à MySQL en utilisant le modèle JDBC avec Spring MVC
Comment créer un environnement [TypeScript + Vue + Express + MySQL] avec Docker ~ MySQL ~
J'ai essayé de créer une application Android avec MVC maintenant (Java)
J'ai essayé de vérifier le fonctionnement du serveur gRPC avec grpcurl
Comment créer un environnement Rails + Vue + MySQL avec Docker [dernière version 2020/09]
Comment créer un environnement [TypeScript + Vue + Express + MySQL] avec Docker ~ Sequelize ~
J'ai essayé d'interagir avec Java
Mettre à jour MySQL de 5.7 à 8.0 avec Docker
Créez un environnement de "développement d'API + vérification d'API à l'aide de Swagger UI" avec Docker
J'ai essayé de créer une API Web qui se connecte à DB avec Quarkus
Comment créer un environnement de développement Ruby on Rails avec Docker (Rails 6.x)
Comment créer un environnement de développement Ruby on Rails avec Docker (Rails 5.x)
Après tout, je voulais prévisualiser le contenu de mysql avec Docker ...
J'ai essayé de démarrer avec Web Assembly
J'ai essayé d'utiliser Scalar DL avec Docker
Déployer sur heroku avec Docker (Rails 6, MySQL)
J'ai créé un serveur écologique avec scala
Créer un serveur proxy d'authentification à l'aide de Docker
J'ai essayé de construire AdoptOpenjdk 11 sur CentOS 7
Qu'est-ce que Docker? J'ai essayé de résumer
Implémentez un CRUD simple avec Go + MySQL + Docker
Créer un environnement avec Docker sur AWS
Comment créer un environnement Rails 6 avec Docker
J'ai essayé de créer un portefeuille avec AWS, Docker, CircleCI, Laravel [avec lien de référence]
J'ai créé une application d'apprentissage automatique avec Dash (+ Docker) part3 ~ Practice ~
[Première construction d'environnement] J'ai essayé de créer un environnement Rails6 + MySQL8.0 + Docker sur Windows 10.
[Java] J'ai essayé de me connecter en utilisant le pool de connexion avec Servlet (tomcat) & MySQL & Java
[Résolution d'erreur] Se produit lors de la tentative de création d'un environnement pour le printemps avec docker
J'ai essayé de faire une authentification de base avec Java
J'ai essayé de gérer la configuration des jambes de force avec Coggle
J'ai essayé de gérer les informations de connexion avec JMX
J'ai essayé de construire AdoptOpenJDK 8 (ajout: Amazon Corretto 8 également)
Comment créer une API avec GraphQL et Rails