【Golang】使用Goa✖️golang-migration✖️gorm实现CRUD API

前言

在之前的文章中,我們已經完成了Goa專案的環境設置。在這篇文章中,我們將使用golang-migration和gorm在goa專案中實現CRUD API的功能。

1. 使用 Docker Compose 启动 PostgreSQL 容器
2. 使用 golang-migration 创建数据库模式
3. 使用 gorm 创建实体、连接数据库并实现 CRUD 操作
4. 进行操作确认

适合只想看源代码的人。

 

我在Goa的GitHub上有一些示例项目,可以参考项目结构和编码等方面,下面是链接。

 

项目结构

  /api
  /model
   /database
    /repository 
     product.go【DBのCRUD処理】
    /entity
     product.go【Entity定義】
    connection.go【DB接続処理】
  /svc
     /design 
       product.go【デザイン定義】
       /request
         product.go【リクエストパラメータ】
       /response
         product.go【レスポンスパラメータ】
  /gen   ← goa gen で生成される。手でいじらない。
  /cmd   ← goa example で生成される。
    /inventory_system
      main.go【アプリ起動時に実行されるmain関数あり】
  inventory.go ← goa example で生成される。

 /db/migrations【golang-migration用ファイルを格納】
    000001_create_product_table.up.sql
    000001_create_product_table.down.sql
    000002_add_product.up.sql
    000002_add_product.down.sql

 docker-compose.yml
 .env【godotenvでアプリ起動時に読み込む/docker-compose up時に読み込む】
 .env_template ※ .envはGitHubにあげずこれをあげる。利用する時は「.env」にリネーム

使用docker compose up命令来启动PostgreSQL容器。

将用于容器启动的YAML和.env文件放置在任意目录中,使用docker compose up -d命令启动。

docker-compose.yml 文件

version: "3.8"
services:
  dbms:
    image: postgres:latest
    restart: always
    environment:
      TZ: ${OS_TIMEZONE}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d postgres -U postgres"]
      interval: 10s
      timeout: 10s
      retries: 5
    ports:
      - ${POSTGRES_PORT}:5432

在将.env上传到存储库时,请将其改名为”.env.template”并上传。

OS_TIMEZONE=Asia/Tokyo
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5435

② 使用golang-migration工具创建数据库模式。

我会按照官方的入门指南来使用golang-migration,因为我对其中的细节一无所知。

 

迁移CLI安装

 

# Mac
brew install golang-migrate
# インストール確認
% migrate -version
v4.15.2

 

创建迁移文件

通过执行提供的迁移命令行界面(CLI)命令,可以生成用于迁移的SQL文件。

产品表

migrate create -ext sql -dir db/migrations -seq create_product_table

执行上述命令后,将创建两个文件up和down。

000001_创建产品表.up.sql
000001_创建产品表.down.sql

・因为内部是空的,所以需要对它们分别进行定义。

000001_create_product_table.up.sql的意思是创建一个名为”product”的数据库表。

CREATE TABLE IF NOT EXISTS product(
   product_id serial PRIMARY KEY,
   product_name VARCHAR (50) UNIQUE NOT NULL,
   product_description VARCHAR(500),
   product_min_stock INTEGER
);

创建产品表的down脚本。

DROP TABLE IF EXISTS product;

產品資料

migrate create -ext sql -dir db/migrations -seq add_product

使用相同的命令,在数据库中创建一个SQL文件,就像product表一样。

000002_新增产品.up.sql
000002_新增产品.down.sql

由于内容为空,所以需要对其进行相应的定义。

000002_add_product.up.sql
000002_新增产品.上升.sql

INSERT INTO product 
(product_name, product_description, product_min_stock)
VALUES
('製品A', '製品Aの説明', '10'),
('製品B', '製品Bの説明', '20'),
('製品C', '製品Cの説明', '30');

000002_add_product.down.sql 的中文释义只需要提供一个选项 :

000002_add_product.down.sql – 添加产品.down.sql

DELETE FROM product;

移民执行(向上)

# フォーマット
migrate -database ${POSTGRESQL_URL} -path db/migrations up
# 実行コマンド
migrate -database "postgres://postgres:postgres@localhost:5435/postgres?sslmode=disable" -path db/migrations up

执行结果

1/u create_product_table (42.524774ms)
2/u add_product (81.520087ms)
image.png

迁移实施(下行)

使用up命令创建的表格/数据需要使用down命令撤销。

% migrate -database "postgres://postgres:postgres@localhost:5435/postgres?sslmode=disable" -path db/migrations down

执行结果 (shí jié guǒ)

Are you sure you want to apply all down migrations? [y/N]
y
Applying all down migrations
2/d add_product (39.820023ms)
1/d create_product_table (73.951284ms)
image.png

数据库连接URL格式。

 

用gorm创建实体、实现数据库连接和CRUD操作。

Golang的ORM的流行度在以下代码库中有统计。目前(2022/05/02),我们将使用第二名的gorm。

 

数据库连接

安装必要的依赖

go get gorm.io/gorm
go get gorm.io/driver/postgres

创建一个名为 `inventory-system/api/model/database/connection.go` 的文件。

package database

import (
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var Db *gorm.DB
var err error

// アプリ起動時にこの関数を呼び出す。
func SetupDb() {
	var host = os.Getenv("DB_HOST")
	var user = os.Getenv("DB_USER")
	var password = os.Getenv("DB_PASSWORD")
	var name = os.Getenv("DB_NAME")
	var port = os.Getenv("DB_PORT")
	var timezone = os.Getenv("OS_TIMEZONE")

	dsn := "host=" + host + " user=" + user + " password=" + password + " dbname=" + name + " port=" + port + " sslmode=disable TimeZone=" + timezone
	Db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("FAILED TO CONNECT TO DB")
	} else {
		log.Println("CONNECTED TO DB")
	}
}

根目录下的.env文件

DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=postgres
DB_PORT=5435
OS_TIMEZONE=Asia/Tokyo

※全球变量的注意事项

 

关于由gorm.Open()返回的DB对象

DB对象的真实身份是Golang标准库database/sql的DB结构。

一个数据库对象可以同时被多个goroutine利用和连接池支持,所以在整个应用中只需要在一个地方使用gorm.Open()来获取数据库对象即可。

返回的数据库对多个 goroutine 的并发使用是安全的,并且维护着自己的空闲连接池。因此,只需要调用一次 Open 函数。关闭数据库的操作很少需要。

 

Entity的含義

建立inventory-system/api/model/database/entity/product.go。

package entity

type Product struct {
	ProductID          uint `gorm:"primaryKey"`
	ProductName        string
	ProductDescription string
	ProductMinStock    int32
}

对CRUD的实现

创建请求和响应

在定义Design之前,先创建Create和Update请求参数,以及Find响应参数。

创建 inventory-system/api/svc/design/request/product.go。

package request

import (
	. "goa.design/goa/v3/dsl"
)

var CreateProductPayload = Type("CreateProductPayload", func() {
	Field(1, "productName", String)
	Field(2, "productDescription", String)
	Field(3, "productMinStock", Int32)
	Required("productName")
})

var UpdateProductPayload = Type("UpdateProductPayload", func() {
	Field(1, "productId", Int)
	Field(2, "productName", String)
	Field(3, "productDescription", String)
	Field(4, "productMinStock", Int32)
	Required("productId")
})

创建一个 inventory-system/api/svc/design/response/product.go 文件。

package response

import (
	. "goa.design/goa/v3/dsl"
)

var FindProductResult = ResultType("FindProductResult", func() {
	Field(1, "productId", Int)
	Field(2, "productName", String)
	Field(3, "productDescription", String)
	Field(4, "productMinStock", Int32)
})

设计出来

创建 inventory-system/api/svc/design/product.go 文件。
定义对于 products 的 CRUD 设计。
注册 /products POST
更新 /products/{productId} PUT
获取 /products/{productId} GET
删除 /products/{productId} DELETE

package design

import (
	"inventory-system/api/svc/design/request"
	"inventory-system/api/svc/design/response"

	. "goa.design/goa/v3/dsl"
)

// APIサーバ定義
var _ = API("inventory-system", func() {
    // API の説明(タイトルと説明)
    Title("Inteventory System Service")
    Description("Service for inventory")

    // ホスト情報
    Server("inventory-system", func() {
        Host("localhost", func() {
            URI("http://localhost:8008") // HTTP REST API
            URI("grpc://localhost:8088") // gRPC
        })
    })
})

// サービス定義
var _ = Service("inventory", func() {
    Description("The inventory service")

    // Create
    Method("create", func() {
        Payload(request.CreateProductPayload)
        Result(String)
        HTTP(func() {
            POST("/products")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Update
    Method("update", func() {
        Payload(request.UpdateProductPayload)
        Result(String)
        HTTP(func() {
            PUT("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Find
    Method("find", func() {
        Payload(func(){
            Field(1, "productId", Int)
            Required("productId")
        })
        Result(response.FindProductResult)
        HTTP(func() {
            GET("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Delete
    Method("delete", func() {
        Payload(func(){
            Field(1, "productId", Int)
            Required("productId")
        })
        Result(String)
        HTTP(func() {
            DELETE("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })

})

从设计到编码实现

根据之前的工程创建的设计,使用goa生成模板文件。

// genフォルダ生成
goa gen inventory-system/api/svc/design

// cmdフォルダ + inventory.go生成
goa example inventory-system/api/svc/design

inventory.go这个文件的大致内容如下。

package inventorysystem

import (
	"context"
	"inventory-system/api/model/database/repository"
	inventory "inventory-system/gen/inventory"
	"log"
)

// inventory service example implementation.
// The example methods log the requests and return zero values.
type inventorysrvc struct {
	logger *log.Logger
}

// NewInventory returns the inventory service implementation.
func NewInventory(logger *log.Logger) inventory.Service {
	return &inventorysrvc{logger}
}

// Create implements create.
func (s *inventorysrvc) Create(ctx context.Context, p *inventory.CreateProductPayload) (res string, err error) {
	s.logger.Print("inventory.create")
	return
}

// Update implements update.
func (s *inventorysrvc) Update(ctx context.Context, p *inventory.UpdateProductPayload) (res string, err error) {
	s.logger.Print("inventory.update")
	return
}

// Find implements find.
func (s *inventorysrvc) Find(ctx context.Context, p *inventory.FindPayload) (res *inventory.Findproductresult, err error) {
	s.logger.Print("inventory.find")
	return
}

// Delete implements delete.
func (s *inventorysrvc) Delete(ctx context.Context, p *inventory.DeletePayload) (res string, err error) {
	s.logger.Print("inventory.delete")
	return
}

开发人员将根据已确定的API的请求/响应状态生成相应的文件。开发人员将根据确定的接口规范,在这个模板文件中实现逻辑。

请在cmd/inventory_system/main.go中添加①DB连接处理和②.env读取处理。

只需要一个选项吗?请执行以下任务:在由goa example命令生成的cmd/inventory_system/main.go文件中的main函数是在应用启动时执行的函数。
为了在应用启动时执行①数据库连接处理和②读取.env文件的处理,需要将这些处理添加到main函数中。

数据库连接处理

package main

import (
    "inventory-system/api/model/database"
    // その他import割愛
)

func main() {
    // ①DB接続処理
    database.SetupDb()
}

读取.env文件的处理

使用godotenv来读取根目录下的.env文件。

 

安装

go get github.com/joho/godotenv

将以下内容追加到主函数中

package main

import (
    "github.com/joho/godotenv"
    // その他import割愛
)

func main() {
    // ②.envロード処理
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	} else {
		log.Println("LOAD .env file")
	}
}

最后的结果是这样的。

package main

import (
    "inventory-system/api/model/database"
	"github.com/joho/godotenv"
)

func main() {
	// Load .env file
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	} else {
		log.Println("LOAD .env file")
	}
	// Setup Database
	database.SetupDb()

}

创建代码仓库

创建 inventory-system/api/model/database/repository/product.go。

package repository

import (
	"inventory-system/api/model/database"
	"inventory-system/api/model/database/entity"
	inventory "inventory-system/gen/inventory"
	"log"
)

func SaveProduct(p *inventory.CreateProductPayload) {
	product := entity.Product{
		ProductName:        p.ProductName,
		ProductDescription: *p.ProductDescription,
		ProductMinStock:    *p.ProductMinStock,
	}
	database.Db.Create(&product)
}

func UpdateProduct(p *inventory.UpdateProductPayload) {
	// find
	var product entity.Product
	if err := database.Db.Where("product_id = ?", p.ProductID).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	// update
	product.ProductName = *p.ProductName
	product.ProductDescription = *p.ProductDescription
	product.ProductMinStock = *p.ProductMinStock
	database.Db.Save(&product)
}

func FindProduct(id int) entity.Product {
	var product entity.Product
	if err := database.Db.Where("product_id = ?", id).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	return product
}

func DeleteProduct(id int) {
	// find
	var product entity.Product
	if err := database.GetDb().Where("product_id = ?", id).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	// delete
	database.Db.Delete(&product)
}

修改inventory.go(原型文件)。

修改代码,从每个端点调用存储库提供的CRUD操作。

package inventorysystem

import (
	"context"
	"inventory-system/api/model/database/repository"
	inventory "inventory-system/gen/inventory"
	"log"
)

// inventory service example implementation.
// The example methods log the requests and return zero values.
type inventorysrvc struct {
	logger *log.Logger
}

// NewInventory returns the inventory service implementation.
func NewInventory(logger *log.Logger) inventory.Service {
	return &inventorysrvc{logger}
}

// Create implements create.
func (s *inventorysrvc) Create(ctx context.Context, p *inventory.CreateProductPayload) (res string, err error) {
	repository.SaveProduct(p)
	s.logger.Print("inventory.create")
	return
}

// Update implements update.
func (s *inventorysrvc) Update(ctx context.Context, p *inventory.UpdateProductPayload) (res string, err error) {
	repository.UpdateProduct(p)
	s.logger.Print("inventory.update")
	return
}

// Find implements find.
func (s *inventorysrvc) Find(ctx context.Context, p *inventory.FindPayload) (res *inventory.Findproductresult, err error) {
	var product = repository.FindProduct(p.ProductID)
	res = &inventory.Findproductresult{
		ProductID:          &p.ProductID,
		ProductName:        &product.ProductName,
		ProductDescription: &product.ProductDescription,
		ProductMinStock:    &product.ProductMinStock,
	}
	s.logger.Print("inventory.find")
	return
}

// Delete implements delete.
func (s *inventorysrvc) Delete(ctx context.Context, p *inventory.DeletePayload) (res string, err error) {
	repository.DeleteProduct(p.ProductID)
	s.logger.Print("inventory.delete")
	return
}

请参考以下对gorm的使用方法。

 

确认动作

一切就绪,开始构建并尝试执行。

// 実行ファイル(inventory_system)を生成
go build ./cmd/inventory_system

// 実行
./inventory_system

开始行动!

% go build ./cmd/inventory_system
% ./inventory_system 
2022/05/05 16:01:49 LOAD .env file
2022/05/05 16:01:49 CONNECTED TO DB
[inventorysystem] 16:01:49 HTTP "Create" mounted on POST /products
[inventorysystem] 16:01:49 HTTP "Update" mounted on PUT /products/{productId}
[inventorysystem] 16:01:49 HTTP "Find" mounted on GET /products/{productId}
[inventorysystem] 16:01:49 HTTP "Delete" mounted on DELETE /products/{productId}
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Create
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Update
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Find
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Delete
[inventorysystem] 16:01:49 HTTP server listening on "localhost:8008"
[inventorysystem] 16:01:49 gRPC server listening on "localhost:8088"

在应用程序启动时可以确认.env文件读取处理正在运行。
在应用程序启动时可以确认数据库连接处理正在运行。

在中国,使用以下curl命令进行操作确认。

创建

curl -X POST -H "Content-Type: application/json" -d '{"productName": "createTest","productDescription": "createTest","productMinStock": 111}' localhost:8008/products

更新

curl -X PUT -H "Content-Type: application/json" -d '{"productName": "updateTest","productDescription": "updateTest","productMinStock": 99911}' localhost:8008/products/1

找到

curl localhost:8008/products/1

删除

curl -X DELETE localhost:8008/products/1

我自己用便签

init函数和main函数的区别

以下是在Golang中的主要函数和init函数的主要目的。主函数是程序的入口点,当程序运行时,它将首先被执行。init函数是在main函数执行之前被自动调用的函数。它用于执行必要的初始化操作,例如设置变量或配置程序。它在全局上下文中被初始化。

广告
将在 10 秒后关闭
bannerAds