Go大师课程(一): 安装使用pgsql并使用golang-migrate 实现数据库 schema 迁移、sqlc 实现CRUD

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 选项传递环境变量。

以下是具体的步骤:

  1. 拉取 PostgreSQL 镜像: 首先,需要使用 docker pull postgres:版本号 命令拉取 PostgreSQL 镜像,例如 docker pull postgres:14
  2. 运行容器: 使用 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_HOSTPOSTGRES_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 命令安装库。
    • 库提供了 creategotoupdown 等命令,分别用于创建迁移文件、迁移到特定版本、执行所有或部分迁移、回滚迁移。
  • 创建迁移文件:

    • 使用 migrate create -seq <version> -dir <path> <name> 命令创建迁移文件。
    • 每个迁移文件包含 up.sqldown.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("%[email protected]", RandomString(6))
}

测试函数的编写

每个测试函数都应该独立运行,不依赖于其他测试函数。

相关推荐
丘山子40 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
CopyLower1 小时前
在 Spring Boot 中实现 WebSockets
spring boot·后端·iphone
天天扭码2 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑2 小时前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
追逐时光者3 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
天天扭码3 小时前
深入讲解Javascript中的常用数组操作函数
前端·javascript·面试
方圆想当图灵3 小时前
深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
后端·maven
豌豆花下猫3 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai
渭雨轻尘_学习计算机ing3 小时前
二叉树的最大宽度计算
算法·面试