【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)
迁移实施(下行)
使用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)
数据库连接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函数执行之前被自动调用的函数。它用于执行必要的初始化操作,例如设置变量或配置程序。它在全局上下文中被初始化。