一个后台系统如何同时支持 MySQL、PostgreSQL、SQLite、SQL Server?从 ShiyuAdmin 的 GORM 多数据库适配说起

很多后台管理系统刚开始开发的时候,数据库通常是写死的。

比如:

go 复制代码
dsn := "root:123456@tcp(127.0.0.1:3306)/admin"
db, err := gorm.Open(mysql.Open(dsn))

项目自己用当然没有问题。

但是一旦准备把项目开源,问题马上就来了。

有人习惯 MySQL,有人公司的基础设施全部是 PostgreSQL;有人只是想把项目下载下来跑个 Demo,希望直接使用 SQLite;还有一些企业项目历史上就是 SQL Server。

这时候你会发现:

真正通用的后台管理系统,数据库不能写死。

我最近在整理自己的开源后台管理系统 ShiyuAdmin 时,就专门做了这一层设计。

项目地址:

text 复制代码
https://github.com/Rodert/ShiyuAdmin

ShiyuAdmin 后端采用 Go + Gin + GORM,目前连接层已经支持:

text 复制代码
PostgreSQL
MySQL
SQLite
SQL Server

今天不讲 CRUD,我们专门聊聊:

一个 Go 后台管理系统,应该怎么设计自己的多数据库适配层?


一、为什么不能在业务代码里判断数据库?

假设我们最开始只支持 MySQL。

代码可能是:

go 复制代码
func InitDatabase() (*gorm.DB, error) {
	dsn := "root:123456@tcp(localhost:3306)/admin?charset=utf8mb4&parseTime=True&loc=Local"

	return gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

后来有人说:

text 复制代码
我们公司不用 MySQL,可以支持 PostgreSQL 吗?

于是你改成:

go 复制代码
if dbType == "mysql" {
	return gorm.Open(mysql.Open(mysqlDSN))
}

if dbType == "postgres" {
	return gorm.Open(postgres.Open(postgresDSN))
}

再后来:

text 复制代码
能不能支持 SQLite?

继续加:

go 复制代码
if dbType == "sqlite" {
	return gorm.Open(sqlite.Open(path))
}

再来一个 SQL Server。

慢慢地你就会发现,数据库初始化代码开始散落在整个项目里。

最糟糕的情况是业务层甚至会出现:

go 复制代码
if dbType == "mysql" {
	// MySQL 查询
} else if dbType == "postgres" {
	// PostgreSQL 查询
}

这种代码一旦出现,数据库适配基本就失控了。

更合理的结构应该是:

text 复制代码
业务层
  ↓
Service
  ↓
Repository
  ↓
GORM
  ↓
Database Dialector
  ↓
MySQL / PostgreSQL / SQLite / SQL Server

业务层根本不应该关心下面到底是哪一种数据库。


二、ShiyuAdmin 是怎么做的?

ShiyuAdmin 把数据库连接逻辑单独放到了:

text 复制代码
backend/shiyu-admin-backend/pkg/database/database.go

核心思想非常简单:

go 复制代码
func Connect(cfg *config.Config) (*gorm.DB, error) {
	var dialector gorm.Dialector

	switch cfg.Database.Driver {

	case "postgres", "postgresql":
		dialector = postgres.Open(...)

	case "mysql":
		dialector = mysql.Open(...)

	case "sqlite":
		dialector = sqlite.Open(...)

	case "sqlserver", "mssql":
		dialector = sqlserver.Open(...)

	default:
		return nil, errors.Errorf(
			"unsupported database driver: %s",
			cfg.Database.Driver,
		)
	}

	return gorm.Open(dialector, &gorm.Config{})
}

这里有一个非常重要的设计:

go 复制代码
var dialector gorm.Dialector

无论底层是什么数据库,最后全部被转换成:

go 复制代码
gorm.Dialector

然后统一:

go 复制代码
gorm.Open(dialector, gcfg)

也就是说,对系统上层来说:

text 复制代码
MySQL
PostgreSQL
SQLite
SQL Server

最后全部变成:

go 复制代码
*gorm.DB

这就是 ORM 带来的一个重要价值。


三、GORM Dialector 到底是什么?

很多刚接触 GORM 的开发者可能只写过:

go 复制代码
gorm.Open(mysql.Open(dsn))

但真正值得注意的是:

go 复制代码
mysql.Open(dsn)

返回的是一个:

go 复制代码
gorm.Dialector

可以简单理解成:

数据库方言适配器。

不同数据库之间有大量差异:

text 复制代码
连接协议不同
SQL 方言不同
分页语法不同
字段类型不同
自增策略不同
索引实现不同
RETURNING 行为不同
DDL 语法不同

GORM 在中间帮我们屏蔽了一部分差异。

于是上层可以统一使用:

go 复制代码
db.Create(&user)

db.First(&user)

db.Where("username = ?", username).First(&user)

db.Model(&user).Updates(data)

db.Delete(&user)

而不用写:

go 复制代码
switch databaseType {
case "mysql":
	...
case "postgres":
	...
}

整个应用层会干净很多。


四、第一步:把数据库配置抽象出来

ShiyuAdmin 没有直接在数据库代码里写用户名和密码,而是统一定义配置结构。

大致可以抽象为:

go 复制代码
type Config struct {
	Database struct {
		Driver   string
		Host     string
		Port     int
		Username string
		Password string
		Database string
		SSLMode  string
		Timezone string
	}
}

于是数据库层根本不用知道:

text 复制代码
配置来自 YAML?
环境变量?
Docker?
Kubernetes Secret?
Render?
Railway?

它只关心:

go 复制代码
cfg.Database.Driver
cfg.Database.Host
cfg.Database.Port
cfg.Database.Username
cfg.Database.Password

这种设计其实比"支持几种数据库"更加重要。

因为它完成了:

text 复制代码
配置系统
        ↓
数据库连接层
        ↓
ORM
        ↓
业务系统

之间的解耦。


五、MySQL 的 DSN 怎么构建?

ShiyuAdmin 中 MySQL 的连接逻辑大致是:

go 复制代码
dsn := fmt.Sprintf(
	"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
	cfg.Database.Username,
	cfg.Database.Password,
	cfg.Database.Host,
	cfg.Database.Port,
	cfg.Database.Database,
)

dialector = mysql.Open(dsn)

最终可能生成:

text 复制代码
root:123456@tcp(127.0.0.1:3306)/shiyu_admin?charset=utf8mb4&parseTime=True&loc=Local

这里几个参数比较重要。


charset=utf8mb4

推荐:

text 复制代码
utf8mb4

而不是:

text 复制代码
utf8

因为 MySQL 中传统的 utf8 并不是真正完整的 UTF-8。

比如 Emoji:

text 复制代码
😀
🚀
🔥

可能就会出现问题。

现代项目基本建议直接:

text 复制代码
utf8mb4

parseTime=True

Go 读取数据库中的:

text 复制代码
DATETIME
TIMESTAMP

字段时,通常希望直接映射为:

go 复制代码
time.Time

所以一般都会开启:

text 复制代码
parseTime=True

loc=Local

控制时间解析时使用的时区。

实际生产环境还需要结合:

text 复制代码
数据库服务器时区
Docker 时区
Go 服务时区
业务时区

统一规划。

否则非常容易出现:

text 复制代码
数据库显示 10:00
Go 读取出来 02:00
前端又显示 10:00

这种经典的八小时时区问题。


六、PostgreSQL 的 DSN 又完全不同

PostgreSQL 的连接格式通常是:

go 复制代码
dsn := fmt.Sprintf(
	"host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s",
	cfg.Database.Host,
	cfg.Database.Username,
	cfg.Database.Password,
	cfg.Database.Database,
	cfg.Database.Port,
	cfg.Database.SSLMode,
	cfg.Database.Timezone,
)

dialector = postgres.Open(dsn)

例如:

text 复制代码
host=127.0.0.1
user=postgres
password=123456
dbname=shiyu_admin
port=5432
sslmode=disable
TimeZone=Asia/Shanghai

可以看到:

MySQL 和 PostgreSQL 连 DSN 格式都完全不一样。

所以千万不要试图在 Service 层处理这些事情。

它们应该全部封装在:

text 复制代码
database.Connect()

内部。


七、SQLite 是另一种完全不同的思路

SQLite 最有意思。

MySQL 是:

text 复制代码
客户端
  ↓
TCP
  ↓
MySQL Server

PostgreSQL:

text 复制代码
客户端
  ↓
TCP
  ↓
PostgreSQL Server

SQLite 则可以直接:

text 复制代码
Go Application
    ↓
xxx.db 文件

所以连接 SQLite 非常简单:

go 复制代码
dialector = sqlite.Open(cfg.Database.Database)

配置甚至可以只有:

yaml 复制代码
database:
  driver: sqlite
  database: shiyu-admin.db

这对于开源项目特别有价值。

因为很多人体验一个后台项目的时候,并不想:

text 复制代码
安装 MySQL
创建数据库
创建用户
设置密码
修改配置
导入 SQL

他只是想:

bash 复制代码
git clone xxx

go run .

然后看看项目长什么样。

SQLite 非常适合承担:

text 复制代码
开发环境
Demo 环境
单机环境
自动化测试
快速体验

这几个场景。


八、SQL Server 为什么建议使用 URL 构造 DSN?

SQL Server 的连接字符串处理稍微麻烦一点。

尤其当密码中包含:

text 复制代码
@
#
:
/
?
&

这些特殊字符的时候,如果直接字符串拼接,很容易出问题。

所以 ShiyuAdmin 这里采用的是:

go 复制代码
dsnURL := &url.URL{
	Scheme: "sqlserver",
	User: url.UserPassword(
		cfg.Database.Username,
		cfg.Database.Password,
	),
	Host: net.JoinHostPort(
		cfg.Database.Host,
		fmt.Sprintf("%d", cfg.Database.Port),
	),
}

然后设置数据库名:

go 复制代码
query := dsnURL.Query()

query.Set(
	"database",
	cfg.Database.Database,
)

dsnURL.RawQuery = query.Encode()

最后:

go 复制代码
dialector = sqlserver.Open(
	dsnURL.String(),
)

这比:

go 复制代码
fmt.Sprintf(
	"sqlserver://%s:%s@%s:%d",
	username,
	password,
	host,
	port,
)

更加稳妥。

特别是数据库密码来自用户配置时。


九、真正关键的一步:统一返回 *gorm.DB

前面的所有判断,最终都会走到这里:

go 复制代码
db, err := gorm.Open(
	dialector,
	&gorm.Config{},
)

然后返回:

go 复制代码
*gorm.DB

这意味着下面的 Repository:

go 复制代码
type UserRepository struct {
	db *gorm.DB
}

完全不需要知道使用什么数据库。

例如:

go 复制代码
func NewUserRepository(
	db *gorm.DB,
) *UserRepository {

	return &UserRepository{
		db: db,
	}
}

查询用户:

go 复制代码
func (r *UserRepository) FindByUsername(
	username string,
) (*User, error) {

	var user User

	err := r.db.
		Where("username = ?", username).
		First(&user).
		Error

	if err != nil {
		return nil, err
	}

	return &user, nil
}

这段代码既可以跑:

text 复制代码
MySQL

也可以跑:

text 复制代码
PostgreSQL

甚至可以跑:

text 复制代码
SQLite
SQL Server

这才是真正的数据库解耦。


十、多数据库支持的核心不是 switch

看到这里,有些人可能觉得:

不就是写了一个 switch 吗?

实际上不是。

真正重要的是:

text 复制代码
所有数据库差异

被限制在一个非常小的边界内。

理想情况下只有:

text 复制代码
pkg/database

知道当前数据库是什么。

而:

text 复制代码
API
Service
Repository
Domain

最好完全不知道。

也就是说系统形成:

text 复制代码
┌─────────────────────┐
│      HTTP API       │
├─────────────────────┤
│      Service        │
├─────────────────────┤
│     Repository      │
├─────────────────────┤
│       GORM          │
├─────────────────────┤
│  Database Adapter   │
├─────────────────────┤
│ MySQL PostgreSQL... │
└─────────────────────┘

如果以后新增一种数据库,我们希望修改的是:

text 复制代码
Database Adapter

而不是把整个系统改一遍。


十一、ShiyuAdmin 启动时是怎么使用数据库连接的?

ShiyuAdmin 服务启动以后,会先执行:

go 复制代码
db, err = database.Connect(cfg)

得到统一的:

go 复制代码
*gorm.DB

然后继续执行数据库初始化逻辑。

例如:

go 复制代码
bootstrap.AutoMigrate(db)

再初始化:

go 复制代码
repoDB.NewUserRepository(db)
repoDB.NewRoleRepository(db)
repoDB.NewMenuRepository(db)
repoDB.NewDeptRepository(db)

可以看到。

后面的代码根本不在乎:

text 复制代码
database.Connect()

内部到底创建了什么数据库。

这就是一个比较典型的:

text 复制代码
依赖向上抽象

十二、AutoMigrate 为什么也很重要?

ShiyuAdmin 目前使用:

go 复制代码
db.AutoMigrate(...)

统一创建核心模型。

类似:

go 复制代码
func AutoMigrate(db *gorm.DB) error {
	return db.AutoMigrate(
		&User{},
		&Role{},
		&Menu{},
		&Dept{},
		&UserRole{},
		&RoleMenu{},
	)
}

对于后台管理系统来说,这种方式开发效率非常高。

定义:

go 复制代码
type User struct {
	ID       uint
	Username string
	Nickname string
}

然后:

go 复制代码
db.AutoMigrate(&User{})

GORM 会调用不同 Dialector,根据数据库生成对应的建表 SQL。


十三、但是 AutoMigrate 并不是万能的

这里必须提醒一下。

如果项目规模变大,只靠:

go 复制代码
AutoMigrate

是不够的。

因为生产数据库迁移涉及:

text 复制代码
字段增加
字段删除
字段重命名
索引调整
唯一约束
历史数据迁移
数据修复
回滚
灰度发布

比如:

text 复制代码
users.nickname

准备从:

text 复制代码
VARCHAR(50)

改成:

text 复制代码
VARCHAR(200)

可能问题不大。

但是如果你要:

text 复制代码
删除字段
拆表
合表
重新设计索引

就不能简单依赖 AutoMigrate。

更成熟的项目通常会使用:

text 复制代码
golang-migrate
Atlas
Goose
Flyway
Liquibase

这样的数据库 Migration 工具。


十四、连接成功还不够,还要设置连接池

ShiyuAdmin 获取 GORM 对应的底层:

go 复制代码
*sql.DB

之后,还会配置数据库连接池。

例如:

go 复制代码
sqlDB, err := db.DB()

if err != nil {
	return nil, err
}

然后:

go 复制代码
sqlDB.SetMaxOpenConns(50)

sqlDB.SetMaxIdleConns(10)

sqlDB.SetConnMaxLifetime(
	30 * time.Minute,
)

这几个参数非常值得讲。


十五、SetMaxOpenConns 是干什么的?

go 复制代码
sqlDB.SetMaxOpenConns(50)

表示:

text 复制代码
数据库最大打开连接数 = 50

很多人会觉得:

连接越多性能越高。

其实完全不是。

如果:

text 复制代码
10 个 Go 实例

每个:

text 复制代码
100 个连接

那理论最大就是:

text 复制代码
1000 个数据库连接

数据库可能直接被拖死。

所以连接池大小应该结合:

text 复制代码
数据库 max_connections
应用实例数量
请求量
SQL 执行时间
CPU
磁盘 IO

综合考虑。


十六、SetMaxIdleConns 又是什么?

go 复制代码
sqlDB.SetMaxIdleConns(10)

意思是最多保留:

text 复制代码
10 个空闲连接

如果完全不保留空闲连接:

每次请求可能都要:

text 复制代码
TCP 建连
认证
握手
创建 Session
执行 SQL
关闭连接

成本非常高。

连接池的价值就是:

text 复制代码
把已经创建好的连接重复使用。

十七、连接为什么还要设置生命周期?

go 复制代码
sqlDB.SetConnMaxLifetime(
	30 * time.Minute,
)

很多人第一次看到会疑惑:

连接不是一直复用最好吗?

并不是。

因为长期连接可能遇到:

text 复制代码
数据库主动断开
NAT 超时
负载均衡连接过期
数据库重启
网络设备清理 Session

所以通常会主动给连接设置一个生命周期。

例如:

text 复制代码
30 分钟

到期重新创建。


十八、多数据库真正难的地方其实是 SQL

做到:

text 复制代码
数据库能连接

其实只是第一步。

真正麻烦的是:

text 复制代码
SQL 是否兼容。

比如获取当前时间。

MySQL:

sql 复制代码
SELECT NOW();

PostgreSQL:

sql 复制代码
SELECT NOW();

看起来一样。

但很多 SQL 并没有这么幸运。

比如字符串拼接。

MySQL 常见:

sql 复制代码
SELECT CONCAT(first_name, last_name);

PostgreSQL 可以:

sql 复制代码
SELECT first_name || last_name;

SQL Server 又可能有自己的处理方式。

再比如:

text 复制代码
LIMIT
OFFSET
TOP
IDENTITY
AUTO_INCREMENT
SERIAL
RETURNING

不同数据库都可能存在差异。


十九、所以 Repository 层应该尽量避免大量 Raw SQL

例如这种代码:

go 复制代码
db.Raw(`
	SELECT *
	FROM sys_user
	WHERE status = 1
	LIMIT 10
`).Scan(&users)

看起来没什么问题。

但是:

text 复制代码
LIMIT

并不是所有数据库都按照完全相同的方式处理。

如果改成:

go 复制代码
db.
	Where("status = ?", 1).
	Limit(10).
	Find(&users)

GORM 可以根据不同 Dialector:

text 复制代码
生成对应数据库的 SQL。

所以如果一个项目真的想支持多数据库:

优先级应该是:

text 复制代码
GORM API
  ↓
GORM Expression
  ↓
Dialect Adapter
  ↓
必要时 Raw SQL

而不是到处:

go 复制代码
db.Raw(...)

二十、分页也不要自己拼 SQL

比如很多人习惯:

go 复制代码
sql := fmt.Sprintf(
	"SELECT * FROM users LIMIT %d OFFSET %d",
	pageSize,
	offset,
)

更推荐:

go 复制代码
db.
	Offset(offset).
	Limit(pageSize).
	Find(&users)

甚至封装:

go 复制代码
func Paginate(
	page int,
	pageSize int,
) func(*gorm.DB) *gorm.DB {

	return func(db *gorm.DB) *gorm.DB {

		if page <= 0 {
			page = 1
		}

		if pageSize <= 0 {
			pageSize = 10
		}

		offset := (page - 1) * pageSize

		return db.
			Offset(offset).
			Limit(pageSize)
	}
}

调用:

go 复制代码
db.
	Scopes(Paginate(page, pageSize)).
	Find(&users)

这样以后数据库切换时风险会小很多。


二十一、模型设计也会影响数据库兼容性

假设模型这样写:

go 复制代码
type User struct {
	ID uint `gorm:"primaryKey"`
}

通常比较容易跨数据库。

但如果大量指定数据库特有类型:

go 复制代码
gorm:"type:jsonb"

这就明显偏向:

text 复制代码
PostgreSQL

如果写:

go 复制代码
gorm:"type:mediumtext"

又明显偏向:

text 复制代码
MySQL

所以一个真正强调跨数据库的项目,Model 层应该尽量使用通用类型。

比如:

go 复制代码
type User struct {
	ID uint64

	Username string

	Nickname string

	Status int

	CreatedAt time.Time

	UpdatedAt time.Time
}

而让:

text 复制代码
GORM Dialector

负责尽可能完成底层类型映射。


二十二、配置文件应该如何切换?

ShiyuAdmin 已经给不同环境准备了独立配置。

例如 MySQL:

yaml 复制代码
database:
  driver: mysql
  host: shiyu-mysql
  port: 3306
  username: shiyu
  password: shiyu123
  database: shiyu_admin_scaffold
  timezone: Asia/Shanghai

如果切换成 PostgreSQL,可以变成:

yaml 复制代码
database:
  driver: postgres
  host: localhost
  port: 5432
  username: postgres
  password: 123456
  database: shiyu_admin
  ssl_mode: disable
  timezone: Asia/Shanghai

SQLite:

yaml 复制代码
database:
  driver: sqlite
  database: ./data/shiyu-admin.db

SQL Server:

yaml 复制代码
database:
  driver: sqlserver
  host: localhost
  port: 1433
  username: sa
  password: YourStrongPassword
  database: shiyu_admin

应用启动代码完全不用变化。

还是:

go 复制代码
db, err := database.Connect(cfg)

二十三、这就是"配置驱动架构"

如果今天:

yaml 复制代码
driver: mysql

系统启动:

text 复制代码
MySQL

明天修改:

yaml 复制代码
driver: postgres

代码不动:

text 复制代码
PostgreSQL

再改:

yaml 复制代码
driver: sqlite

代码依然不动:

text 复制代码
SQLite

这其实就是非常典型的:

text 复制代码
Configuration Driven Architecture

配置驱动架构。

把:

text 复制代码
环境差异

从:

text 复制代码
代码

中抽离出来。


二十四、如果让我继续优化 ShiyuAdmin 的数据库层

当前这种设计已经比较适合中小型后台项目。

但如果准备继续扩大,我会进一步拆分:

text 复制代码
database/
├── database.go
├── mysql.go
├── postgres.go
├── sqlite.go
├── sqlserver.go
└── pool.go

比如:

go 复制代码
func mysqlDialector(
	cfg *config.Config,
) gorm.Dialector {

	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		cfg.Database.Username,
		cfg.Database.Password,
		cfg.Database.Host,
		cfg.Database.Port,
		cfg.Database.Database,
	)

	return mysql.Open(dsn)
}

PostgreSQL:

go 复制代码
func postgresDialector(
	cfg *config.Config,
) gorm.Dialector {

	dsn := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s",
		cfg.Database.Host,
		cfg.Database.Username,
		cfg.Database.Password,
		cfg.Database.Database,
		cfg.Database.Port,
		cfg.Database.SSLMode,
		cfg.Database.Timezone,
	)

	return postgres.Open(dsn)
}

主函数就会变得非常干净:

go 复制代码
func Connect(
	cfg *config.Config,
) (*gorm.DB, error) {

	dialector, err := newDialector(cfg)

	if err != nil {
		return nil, err
	}

	db, err := gorm.Open(
		dialector,
		newGormConfig(),
	)

	if err != nil {
		return nil, err
	}

	if err := configurePool(db); err != nil {
		return nil, err
	}

	return db, nil
}

二十五、甚至可以进一步使用 Factory 模式

例如:

go 复制代码
type DialectorFactory func(
	cfg *config.Config,
) gorm.Dialector

然后注册:

go 复制代码
var factories = map[string]DialectorFactory{
	"mysql":      mysqlDialector,
	"postgres":   postgresDialector,
	"postgresql": postgresDialector,
	"sqlite":     sqliteDialector,
	"sqlserver":  sqlserverDialector,
	"mssql":      sqlserverDialector,
}

创建:

go 复制代码
func newDialector(
	cfg *config.Config,
) (gorm.Dialector, error) {

	factory, ok := factories[
		cfg.Database.Driver
	]

	if !ok {
		return nil, fmt.Errorf(
			"unsupported database: %s",
			cfg.Database.Driver,
		)
	}

	return factory(cfg), nil
}

以后增加:

text 复制代码
Oracle
达梦
TiDB
CockroachDB

理论上都可以继续扩展。


二十六、多数据库项目一定要做测试矩阵

这个问题特别容易被忽略。

很多项目 README 写着:

text 复制代码
支持 MySQL
支持 PostgreSQL
支持 SQLite

实际情况却是:

text 复制代码
只在 MySQL 跑过。

真正要维护多数据库,我更建议 CI 做:

text 复制代码
MySQL
PostgreSQL
SQLite
SQL Server

测试矩阵。

GitHub Actions 可以设计成:

yaml 复制代码
strategy:
  matrix:
    database:
      - mysql
      - postgres
      - sqlite
      - sqlserver

然后分别执行:

bash 复制代码
go test ./...

核心测试至少覆盖:

text 复制代码
创建用户
查询用户
更新用户
删除用户

创建角色
角色绑定用户

创建菜单
菜单树查询

事务

分页

唯一索引

软删除

否则所谓的:

text 复制代码
支持多数据库

很容易慢慢变成:

text 复制代码
理论支持多数据库。

二十七、一个值得注意的设计:不要为了兼容牺牲全部能力

跨数据库也不是数据库支持得越多越好。

比如 PostgreSQL 有:

text 复制代码
JSONB
Array
GIN Index
Full Text Search
RETURNING

MySQL 有自己的:

text 复制代码
JSON
FULLTEXT
Generated Column

SQL Server 也有大量企业功能。

如果为了做到:

text 复制代码
所有数据库完全一样

最后可能只能使用数据库的:

text 复制代码
最小公共子集。

这未必合理。

更加实际的方案是:

text 复制代码
核心功能跨数据库兼容
+
高级功能允许数据库特化

例如:

go 复制代码
switch db.Dialector.Name() {

case "postgres":
	// PostgreSQL 优化

case "mysql":
	// MySQL 优化
}

但是这种判断应该:

text 复制代码
集中在基础设施层

而不是散落在业务层。


二十八、总结

ShiyuAdmin 这个多数据库设计,看起来核心代码并不复杂。

本质上就是:

go 复制代码
switch cfg.Database.Driver

然后选择:

go 复制代码
mysql.Open()

postgres.Open()

sqlite.Open()

sqlserver.Open()

最后统一:

go 复制代码
gorm.Open()

但真正值得学习的并不是这个 switch

而是它背后的架构思想:

text 复制代码
配置
  ↓
数据库适配器
  ↓
GORM
  ↓
Repository
  ↓
Service
  ↓
API

让上层业务彻底摆脱:

text 复制代码
我到底在使用什么数据库?

这个问题。

一个后台系统真正想做到可复用、可部署、可二次开发,就不能只考虑:

text 复制代码
代码在我的电脑上能不能跑。

还应该考虑:

text 复制代码
别人拿到以后怎么跑?
企业环境怎么部署?
换数据库改多少代码?
开发环境能不能快速启动?
数据库差异有没有污染业务层?

这也是为什么我觉得:

数据库适配层看起来只是几十行代码,却是一个通用后台系统非常重要的基础设施设计。

ShiyuAdmin 目前使用 Go + Gin + GORM 构建后端,在数据库连接层把 PostgreSQL、MySQL、SQLite 和 SQL Server 统一抽象到了 *gorm.DB 之上。

如果你正在做自己的后台管理系统,这套思路同样值得参考:

text 复制代码
不要让业务代码依赖某一种数据库。

让数据库变成基础设施,而不是业务的一部分。

项目:

text 复制代码
ShiyuAdmin
https://github.com/Rodert/ShiyuAdmin
相关推荐
小翰生信13 分钟前
转录组下游分析全流程:DESeq2 差异分析、GO/KEGG 富集与 GSEA 实战
数据库·矩阵·golang
2603_9664372618 分钟前
拷贝中断文件丢失,电脑资料抢救实操方案
数据库·电脑
harmony&26 分钟前
MySQL 数据库从入门到实战:原理、安装与 SQL 详解
数据库·sql·mysql
艺杯羹35 分钟前
全栈信创落地实录:基于银河麒麟V10与达梦数据库DM8的SpringBoot工业级适配指南
java·数据库·spring boot·后端·spring
坚定信念,勇往无前6 小时前
mongodb的日志分割
数据库·mongodb
oradh9 小时前
Oracle TX 锁 Mode 4(Share)问题排查总结
数据库·oracle·tx 锁 mode 4·oracle tx 锁
2601_9620652510 小时前
[MySQL] SQL优化之性能分析
java·sql·mysql
yi.Ist10 小时前
数据定义语言-DDL操作
数据库·学习·mysql·oracle·大海豚
阿kun要赚马内10 小时前
MySQL 索引基础
后端·mysql