Skip to content
markdown
---
title: "数据库"
description: "Goyave 专为关系型数据库设计,并采用了强大的 Gorm ORM。"
---

# 数据库

[[toc]]

## 简介

Goyave 专为关系型数据库设计,并采用了强大的 [Gorm ORM](https://gorm.io/)。

框架负责管理数据库连接,这些连接是长期存活的。当服务器关闭时,数据库连接会自动关闭。因此您无需在应用中操心创建、关闭或刷新数据库连接的问题。

数据库连接池会在使用 `goyave.New()` 创建服务器后立即可用,可通过 `server.DB()` 访问。该连接池也会分配给所有**组件**使用。

## 配置

开始使用数据库只需少量代码。但您需要修改以下[配置](/getting-started/configuration.html#database-category)选项:
- `database.connection`
- `database.host`
- `database.port`
- `database.name`
- `database.username`
- `database.password`
- `database.options`

::: tip
`database.options` 表示 DSN 中的附加连接选项。例如,使用 MySQL 时应设置 `parseTime=true` 选项以确保正确处理 `time.Time`。可用选项因驱动程序而异,可在各自文档中找到。

此项可为空。
:::

### DSN 选项

本节为每种支持的驱动程序提供 `database.options` 配置项的示例值。

#### MySQL

charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true&loc=Local


查看 [MySQL 选项](https://github.com/go-sql-driver/mysql#parameters)的更多信息。

#### PostgreSQL

sslmode=disable application_name=goyave


查看 [PostgreSQL 选项](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)的更多信息。

#### SQLite

cache=shared&mode=memory


:::info
此驱动程序仅需配置 `database.name` 项。
:::

查看 [SQLite 选项](https://github.com/mattn/go-sqlite3#connection-string)的更多信息。

#### MSSQL

encrypt=disable


查看 [MSSQL 选项](https://github.com/denisenkom/go-mssqldb#connection-parameters-and-dsn)的更多信息。

#### Clickhouse

dial_timeout=10s&read_timeout=20s


查看 [Clickhouse 选项](https://github.com/ClickHouse/clickhouse-go?tab=readme-ov-file#dsn)的更多信息。

#### Bigquery

Bigquery 没有 DSN 选项。

:::warning 重要
必须禁用预处理语句才能使此驱动程序正常工作。可通过将配置项 `database.config.prepareStmt` 设置为 `false` 实现。
:::

查看 [Bigquery 驱动](https://github.com/go-gorm/bigquery)的更多信息。

### 驱动程序

框架开箱即支持以下 sql 驱动程序(在 `database.connection` 配置项中定义):
- `none`(*禁用数据库功能*)
- `mysql`
- `postgres`
- `sqlite3`
- `mssql`
- `clickhouse`
- `bigquery`

为建立数据库连接,Gorm 需要导入数据库驱动。在 `main.go` 中添加以下导入:

```go
import _ "goyave.dev/goyave/v5/database/dialect/mysql"
import _ "goyave.dev/goyave/v5/database/dialect/postgres"
import _ "goyave.dev/goyave/v5/database/dialect/sqlite"
import _ "goyave.dev/goyave/v5/database/dialect/mssql"
import _ "goyave.dev/goyave/v5/database/dialect/clickhouse"
import _ "goyave.dev/goyave/v5/database/dialect/bigquery"

INFO

请注释或移除不需要的导入。


您可以注册更多方言给 Gorm。首先实现或导入方言,然后告知 Goyave 如何构建该方言的连接字符串:

go
import (
  "goyave.dev/goyave/v5/database"
  "my-project/database/mydriver"
)

func init() {
  database.RegisterDialect("my-driver", "{username}:{password}@({host}:{port})/{name}?{options}", mydriver.Open)
}

模板格式接受以下占位符,它们会自动替换为相应的配置项:

  • {username}
  • {password}
  • {host}
  • {port}
  • {name}
  • {options}

无法覆盖已存在的方言。

超时设置

默认会注册超时插件。该插件使用 database.defaultReadQueryTimeoutdatabase.defaultWriteQueryTimeout(时间单位为毫秒)自动为查询上下文添加超时。超时范围涵盖完整的 Gorm 操作:超时在最先执行的回调Before("*"))中开始计时。这意味着诸如 BeforeCreateAfterSave 等钩子函数也会计入执行时间。使用事务时,超时是每个操作的超时,而非整个事务的完成时间。

如果查询上下文已有截止时间或超时设置,插件不会覆盖它。

可通过将读取或写入超时(或两者)设置为 0 来禁用。

DANGER

超时插件不支持 Raw().Scan() 操作,因为插件会在 Gorm 内部调用 rows.Next() 之前取消上下文,导致错误。由于 Gorm 的实现方式,此问题无法在插件中解决。

如需为此类操作设置超时,需手动处理:

go
users := []*model.User{}

ctx, cancel := context.WithTimeout(ctx, time.Millisecond*500)
defer cancel()
db := r.DB.WithContext(ctx).Raw("SELECT * FROM users").Scan(&users)

超时插件支持 Exec() 操作。

分页

database.Paginator 是帮助您分页记录的工具。该结构包含自动获取的分页信息(当前页码、最大页数、记录总数)。

示例

go
// database/repository/user.go
package repository

import (
	"context"

	"gorm.io/gorm"
	"goyave.dev/goyave/v5/database"
	"goyave.dev/goyave/v5/util/session"
	"my-project/database/model"
)

type User struct {
	DB *gorm.DB
}

func (r *User) Paginate(ctx context.Context, page int, pageSize int) (*database.Paginator[*model.User], error) {
	users := []*model.User{}

	db := session.DB(ctx, r.DB)
	paginator := database.NewPaginator(db, page, pageSize, &users)
	err := paginator.Find()
	return paginator, err
}

调用 paginator.Find() 时,会在事务中执行两个查询:

  • 获取页面信息(总记录数和最大页数)并自动更新结构字段。
  • 执行实际查询获取记录。传递给 NewPaginator() 的目标切片也会自动更新。

WARNING

请不要忘记在从服务返回分页器前将其记录转换为 DTO。可使用 typeutil.MustConvert() 轻松转换分页器:

go
import (
	"context"

	"goyave.dev/goyave/v5/database"
	"goyave.dev/goyave/v5/util/typeutil"
	"my-project/dto"
)
//...
paginator, err := repository.Paginate(ctx, page, pageSize)
if err != nil {
	return nil, err
}
// 转换前 paginator 类型为 `*database.Paginator[*model.User]`
return typeutil.MustConvert[*database.PaginatorDTO[*dto.User]](paginator), nil

WARNING

与数据库偏移量不同,page 索引从 1 开始而非 0。


您可以在创建分页器前向 SQL 查询添加子句。这对分页搜索结果特别有用。条件将同时应用于总记录数查询和实际查询。

完整示例:

go
import (
	"context"

	"goyave.dev/goyave/v5/database"
	"goyave.dev/goyave/v5/util/sqlutil"
	"my-project/database/model"
)

func (r *User) Paginate(ctx context.Context, page int, pageSize int, search string) (*database.Paginator[*model.User], error) {
	users := []*model.User{}

	db := r.DB
	if search != "" {
		db = db.Where("email", "%"+sqlutil.EscapeLike(search)+"%")
	}

	paginator := database.NewPaginator(db, page, pageSize, &users)
	err := paginator.Find()
	return paginator, err
}

TIP

查看过滤器库了解基于查询参数的强大动态过滤和分页功能。

分页原始查询

对于特殊用例,您可能需要对原始查询的结果进行分页,而非使用自动生成的查询。原始查询不应包含 LIMITOFFSET 子句,它们会根据给定的 pagepageSize 自动添加在末尾。

计数查询应返回单个数字(例如 COUNT(*))。

go
func (r *User) Paginate(ctx context.Context, page int, pageSize int) (*database.Paginator[*model.User], error) {
	users := []*model.User{}

	paginator := database.NewPaginator(r.DB, page, pageSize, &users)
	paginator.Raw(
		"SELECT * FROM users WHERE id = ?",
		[]any{123}, // 原始查询的参数
		"SELECT COUNT(*) FROM users WHERE id = ?",
		[]any{123}, // 原始计数查询的参数
	)
	err := paginator.Find()
	return paginator, err
}

WARNING

如果使用原始分页,会执行 Scan() 操作。因此超时插件将不起作用。如需为这些查询设置超时,请确保向分页器提供带有超时上下文的数据库实例:

go
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*500)
defer cancel()
db := r.DB.WithContext(ctx)
paginator := database.NewPaginator(db, page, pageSize, &users)

设置 SSL/TLS

MySQL

如需让数据库连接使用 TLS 配置,创建 database/tls.go。在此文件中创建 init() 函数来加载证书和密钥。

请不要忘记在 main.go 中空白导入数据库包:import _ "myproject/database"。最后,对于名为 "custom" 的配置,在 database.options 配置项末尾添加 &tls=custom

go
package database

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"

    "github.com/go-sql-driver/mysql"
)

func init() {
    rootCertPool := x509.NewCertPool()
    pem, err := ioutil.ReadFile("/path/ca-cert.pem")
    if err != nil {
        panic(err)
    }
    if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
        panic("Failed to append PEM.")
    }
    clientCert := make([]tls.Certificate, 0, 1)
    certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
    if err != nil {
        panic(err)
    }
    clientCert = append(clientCert, certs)
    mysql.RegisterTLSConfig("custom", &tls.Config{
        RootCAs:      rootCertPool,
        Certificates: clientCert,
    })
}

参考

PostgreSQL

对于 PostgreSQL,只需向 database.options 配置项添加几个选项。

sslmode=verify-full sslrootcert=root.crt sslkey=client.key sslcert=client.crt

root.crtclient.keyclient.crt 替换为相应文件的路径。

参考

MSSQL

参阅驱动程序文档

迁移

不建议使用 Gorm 的自动迁移。Goyave 鼓励开发者使用可在多个开发者和生产服务器间轻松同步的版本化模式。框架未直接提供此类工具,因为这超出了其范围。但已有许多优秀工具可用于此目的,例如 dbmate