Go大师课程系列将学习
这篇,学习如何使用PostgreSQL、Golang和Docker来设计一个简单的银行后端系统。分为以下三个部分
- dbdiagram.io
- pgsql
- golang-migrate
- 单元测试 我们将从使用dbdiagram.io学习数据库模式设计开始。
dbdiagram.io
首先,我们将生成SQL代码,在目标数据库中创建所设计的模式。接下来,我们将设计具有关键字段和数据类型的数据库模式,包括帐户、条目和转账表。我们还将设计内部银行转账的数据库模式。
sql
Table "users" {
"username" varchar [pk]
"role" varchar [not null, default: "depositor"]
"hashed_password" varchar [not null]
"full_name" varchar [not null]
"email" varchar [unique, not null]
"is_email_verified" bool [not null, default: false]
"password_changed_at" timestamptz [not null, default: "0001-01-01"]
"created_at" timestamptz [not null, default: `now()`]
}
Table "verify_emails" {
"id" bigserial [pk, increment]
"username" varchar [not null]
"email" varchar [not null]
"secret_code" varchar [not null]
"is_used" bool [not null, default: false]
"created_at" timestamptz [not null, default: `now()`]
"expired_at" timestamptz [not null, default: `now()+interval'15 minutes'`]
}
Table "accounts" {
"id" bigserial [pk, increment]
"owner" varchar [not null]
"balance" bigint [not null]
"currency" varchar [not null]
"created_at" timestamptz [not null, default: `now()`]
Indexes {
owner
(owner, currency) [unique]
}
}
Table "entries" {
"id" bigserial [pk, increment]
"account_id" bigint [not null]
"amount" bigint [not null, note: 'can be negative or positive']
"created_at" timestamptz [not null, default: `now()`]
Indexes {
account_id
}
}
Table "transfers" {
"id" bigserial [pk, increment]
"from_account_id" bigint [not null]
"to_account_id" bigint [not null]
"amount" bigint [not null, note: 'must be positive']
"created_at" timestamptz [not null, default: `now()`]
Indexes {
from_account_id
to_account_id
(from_account_id, to_account_id)
}
}
Table "sessions" {
"id" uuid [pk]
"username" varchar [not null]
"refresh_token" varchar [not null]
"user_agent" varchar [not null]
"client_ip" varchar [not null]
"is_blocked" boolean [not null, default: false]
"expires_at" timestamptz [not null]
"created_at" timestamptz [not null, default: `now()`]
}
Ref:"users"."username" < "verify_emails"."username"
Ref:"users"."username" < "accounts"."owner"
Ref:"accounts"."id" < "entries"."account_id"
Ref:"accounts"."id" < "transfers"."from_account_id"
Ref:"accounts"."id" < "transfers"."to_account_id"
Ref:"users"."username" < "sessions"."username"
为了提高数据库的性能和可维护性,我们将添加注释、定义自定义枚举类型,并向表添加索引。最后,我们将在dbdiagram.io中设计完整的数据库模式,并生成相应的SQL代码。
在设计数据库模式时,我们需要考虑各个表之间的关系,并确保数据的完整性和一致性。例如,我们可以为转账表添加外键约束,确保每笔转账都对应一个有效的账户。同时,我们还可以为常用的查询添加合适的索引,提高数据库的查询性能。
这块就不详细介绍了,大家可以使用摸索一番。其他的工具一样可以达到同样的效果,这里只是提供一种方案
PostgreSQL
使用 Docker 运行 PostgreSQL 容器并设置环境变量,需要使用 docker run
命令,并通过 -e
选项传递环境变量。
以下是具体的步骤:
- 拉取 PostgreSQL 镜像: 首先,需要使用
docker pull postgres:版本号
命令拉取 PostgreSQL 镜像,例如docker pull postgres:14
。 - 运行容器: 使用
docker run
命令运行容器,并设置环境变量。
例如,要设置用户名为 postgres
、密码为 password
,数据库名为 mydatabase
的环境变量,可以使用以下命令:
bash docker run --name mypostgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydatabase -d postgres:14
--name mypostgres
:为容器命名为mypostgres
-e POSTGRES_USER=postgres
:设置数据库用户名为postgres
-e POSTGRES_PASSWORD=password
:设置数据库密码为password
-e POSTGRES_DB=mydatabase
:设置数据库名称为mydatabase
-d
:在后台运行容器postgres:14
:使用postgres:14
镜像运行容器。
一些额外的注意事项: 可以根据需要设置其他环境变量,例如 POSTGRES_HOST
、POSTGRES_PORT
等。 为了安全性,最好不要将密码直接写在命令行中,可以使用环境变量文件或者 Docker Compose 来管理敏感信息。 您可以使用 docker exec -it mypostgres bash
命令进入容器,然后使用 psql
命令连接到数据库进行操作。
docker-compose
yaml
version: "3.9"
services:
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
ports:
- "5432:5432"
volumes:
- data-volume:/var/lib/postgresql/data
启动成功后
创建数据库与导入sql
我们创建一个名为simple_bank
的数据库,导入之前设计的sql
导入成功看到这几张表即可
golang-migrate
-
golang-migrate 库的安装和使用:
- 使用
brew install golang-migrate
命令安装库。 - 库提供了
create
、goto
、up
、down
等命令,分别用于创建迁移文件、迁移到特定版本、执行所有或部分迁移、回滚迁移。
- 使用
-
创建迁移文件:
- 使用
migrate create -seq <version> -dir <path> <name>
命令创建迁移文件。 - 每个迁移文件包含
up.sql
和down.sql
两个文件,分别用于执行迁移和回滚迁移。
- 使用
使用命令migrate create -ext sql -dir db/migration -seq init_schema
shell
migrate create -ext sql -dir db/migration -seq init_schema
/Users/Desktop/simplebank/db/migration/000001_init_schema.up.sql
/Users/Desktop/simplebank/db/migration/000001_init_schema.down.sql
-
编写迁移脚本:
up.sql
文件中包含用于修改数据库 schema 的 SQL 语句。down.sql
文件中包含用于回滚up.sql
中修改的 SQL 语句。
up 是创建表
down是回滚数据的sql
-
运行迁移:
- 使用
migrate -path <path> -database <url> up
命令执行迁移。 - 使用
migrate -path <path> -database <url> down
命令回滚迁移。
- 使用
shell
migrate -path db/migration -database "$(DB_URL)" -verbose up
sqlc
接下来将如何在 Golang 中使用 sqlc 库,通过 SQL 查询自动生成 CRUD 代码,以简化数据库操作。
CRUD 操作的概念:创建(Create)、读取(Read)、更新(Update)和删除(Delete)。 市面上开源的go orm 实现 CRUD 操作的几种方法:
- 使用标准库 database/sql 包:速度快,但需要手动映射 SQL 字段到变量,容易出错。
- 使用 Gorm 库:方便,但需要学习 Gorm 的查询语法,对于复杂查询可能难以使用。
- 使用 sqlx 库:速度接近标准库,使用方便,但代码仍较长,错误只在运行时发现。
- 使用 sqlc 库:速度快,使用简单,只需编写 SQL 查询,就能自动生成 Golang 代码。
sqlc 生成 CRUD 代码
安装 sqlc 并配置 sqlc.yaml 文件。
shell
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
shell
➜ ~ sqlc version
v1.26.0
在 query 文件夹中编写 SQL 查询,并使用注释指定生成函数签名。
sql
-- name: CreateAccount :one
INSERT INTO accounts (
owner,
balance,
currency
) VALUES (
$1, $2, $3
) RETURNING *;
-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;
-- name: GetAccountForUpdate :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1
FOR NO KEY UPDATE;
-- name: ListAccounts :many
SELECT * FROM accounts
WHERE owner = $1
ORDER BY id
LIMIT $2
OFFSET $3;
-- name: UpdateAccount :one
UPDATE accounts
SET balance = $2
WHERE id = $1
RETURNING *;
-- name: AddAccountBalance :one
UPDATE accounts
SET balance = balance + sqlc.arg(amount)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1;
运行 make sqlc 命令生成 Golang 代码。
shell
sqlc generate
以创建账户为例,使用 sqlc 生成的代码:
- models.go 文件包含了数据模型的定义。
- db.go 文件包含了 DBTX 接口,用于连接数据库。
- account.sql.go 文件包含了自动生成的 CRUD 函数
- querier.go 接口类型
单元测试
单元测试命名规
每个单元测试函数必须以 Test 前缀开头(首字母大写)并接收一个 testing.T 对象作为输入。 go的规范就是这样
go
package db
import (
"context"
"simplebank/util"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func createRandomAccount(t *testing.T) Account {
arg := CreateAccountParams{
Owner: "tracydzf",
Balance: util.RandomMoney(),
Currency: util.RandomCurrency(),
}
account, err := testStore.CreateAccount(context.Background(), arg)
require.NoError(t, err)
require.NotEmpty(t, account)
require.Equal(t, arg.Owner, account.Owner)
require.Equal(t, arg.Balance, account.Balance)
require.Equal(t, arg.Currency, account.Currency)
require.NotZero(t, account.ID)
require.NotZero(t, account.CreatedAt)
return account
}
func TestCreateAccount(t *testing.T) {
createRandomAccount(t)
}
数据库连接和 Queries 对象的设置
在 main_test.go 文件中定义一个全局变量 testQueries,包含一个 dbtx 属性,用于存储数据库连接或事务。
go
package db
import (
"context"
"log"
"os"
"simplebank/util"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
)
var testStore Store
func TestMain(m *testing.M) {
config, err := util.LoadConfig("../..")
if err != nil {
log.Fatal("cannot load config:", err)
}
connPool, err := pgxpool.New(context.Background(), config.DBSource)
if err != nil {
log.Fatal("cannot connect to db:", err)
}
testStore = NewStore(connPool)
os.Exit(m.Run())
}
随机数据生成器
使用 util/random.go 文件生成随机的测试数据,包括随机整数、随机字符串、随机姓名、随机金额和随机货币。
go
package util
import (
"fmt"
"math/rand"
"strings"
"time"
)
const alphabet = "abcdefghijklmnopqrstuvwxyz"
func init() {
rand.Seed(time.Now().UnixNano())
}
// RandomInt generates a random integer between min and max
func RandomInt(min, max int64) int64 {
return min + rand.Int63n(max-min+1)
}
// RandomString generates a random string of length n
func RandomString(n int) string {
var sb strings.Builder
k := len(alphabet)
for i := 0; i < n; i++ {
c := alphabet[rand.Intn(k)]
sb.WriteByte(c)
}
return sb.String()
}
// RandomOwner generates a random owner name
func RandomOwner() string {
return RandomString(6)
}
// RandomMoney generates a random amount of money
func RandomMoney() int64 {
return RandomInt(0, 1000)
}
// RandomCurrency generates a random currency code
func RandomCurrency() string {
currencies := []string{"USD", "EUR", "CAD"}
n := len(currencies)
return currencies[rand.Intn(n)]
}
// RandomEmail generates a random email
func RandomEmail() string {
return fmt.Sprintf("%s@email.com", RandomString(6))
}
测试函数的编写
每个测试函数都应该独立运行,不依赖于其他测试函数。