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("%s@email.com", RandomString(6))
}

测试函数的编写

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

相关推荐
.生产的驴1 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲1 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
hanglove_lucky2 小时前
本地摄像头视频流在html中打开
前端·后端·html
liyinuo20173 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范
皓木.3 小时前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端
代码中の快捷键4 小时前
java开发面试有2年经验
java·开发语言·面试
i7i8i9com4 小时前
java 1.8+springboot文件上传+vue3+ts+antdv
java·spring boot·后端
秋意钟4 小时前
Spring框架处理时间类型格式
java·后端·spring
我叫啥都行4 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
Stark、4 小时前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端