
最近我一直在维护自己的开源后台项目 ShiyuAdmin。
项目地址:
text
https://github.com/Rodert/ShiyuAdmin
ShiyuAdmin 是一个基于:
- Go
- Gin
- GORM
- React
- Ant Design Pro
- Redis
- RBAC
实现的前后端分离后台管理系统。
做后台脚手架的时候,有一个问题迟早会遇到:
数据库到底要支持几个?
如果只是自己做一个项目,那么 MySQL 基本就够了。
但是如果你做的是一个准备长期维护的后台脚手架,情况就完全不同了。
有人习惯 MySQL,有人公司的基础设施全部是 PostgreSQL,有人只想本地跑个 SQLite,还有很多传统企业项目依然大量使用 SQL Server。
所以我最近在 ShiyuAdmin 里做了一件事:
让同一套 Go 后台尽可能兼容 PostgreSQL、MySQL、SQLite 和 SQL Server。
这篇文章就聊聊这个过程中比较核心的一个技术点:
Go + GORM 应该怎么设计多数据库适配。
一、为什么不能把数据库写死?
很多 Go 项目最开始连接数据库可能是这样:
go
dsn := "root:123456@tcp(127.0.0.1:3306)/demo"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
这当然没什么问题。
但是它实际上已经把整个项目和 MySQL 绑定了。
以后如果客户突然告诉你:
我们公司不用 MySQL,只能使用 SQL Server。
你就会发现问题来了。
首先连接方式不同。
其次驱动不同。
再往后还有:
- SQL 方言不同
- 分页实现差异
- JSON 类型差异
- 自增主键差异
- 时间函数差异
- 字段类型差异
- 索引实现差异
- 系统表查询方式不同
所以真正的"多数据库支持",绝对不是简单换一个 DSN 就结束了。
二、第一步:把数据库类型放进配置文件
我的思路是:
业务代码不要关心当前使用的是什么数据库。
数据库类型应该交给配置层决定。
例如可以设计成:
yaml
database:
driver: mysql
host: localhost
port: 3306
username: root
password: 123456
database: shiyu_admin
如果改成 PostgreSQL:
yaml
database:
driver: postgres
host: localhost
port: 5432
username: postgres
password: 123456
database: shiyu_admin
SQLite 更简单:
yaml
database:
driver: sqlite
database: shiyu_admin.db
SQL Server:
yaml
database:
driver: sqlserver
host: localhost
port: 1433
username: sa
password: YourPassword
database: shiyu_admin
这样做以后,上层程序只需要读取:
go
cfg.Database.Driver
就能决定加载哪个数据库驱动。
三、用 GORM 屏蔽大部分数据库差异
这也是为什么 ShiyuAdmin 后端选择 GORM。
GORM 本身提供了多个数据库 Dialector。
例如:
go
gorm.io/driver/mysql
gorm.io/driver/postgres
gorm.io/driver/sqlite
gorm.io/driver/sqlserver
于是数据库初始化代码可以统一封装。
思路类似:
go
func InitDatabase(cfg DatabaseConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "mysql":
dsn := buildMySQLDSN(cfg)
dialector = mysql.Open(dsn)
case "postgres":
dsn := buildPostgresDSN(cfg)
dialector = postgres.Open(dsn)
case "sqlite":
dialector = sqlite.Open(cfg.Database)
case "sqlserver":
dsn := buildSQLServerDSN(cfg)
dialector = sqlserver.Open(dsn)
default:
return nil, fmt.Errorf(
"unsupported database driver: %s",
cfg.Driver,
)
}
return gorm.Open(dialector, &gorm.Config{})
}
这么做之后,业务层拿到的永远是:
go
*gorm.DB
而不是:
text
MySQLDB
PostgresDB
SQLServerDB
SQLiteDB
这非常重要。
四、业务层不要写数据库专属 SQL
例如查询用户:
go
var users []User
db.Where("status = ?", 1).
Order("id desc").
Find(&users)
这段代码对于 MySQL、PostgreSQL、SQLite、SQL Server 来说,都可以由 GORM 去处理。
创建数据也是一样:
go
db.Create(&User{
Username: "shiyu",
Nickname: "王仕宇",
})
更新:
go
db.Model(&User{}).
Where("id = ?", id).
Update("nickname", "JavaPub")
删除:
go
db.Delete(&User{}, id)
这些就是 ORM 最大的价值之一:
把大部分 SQL 方言差异挡在业务代码之外。
这样 Repository 和 Service 层基本不需要知道下面到底跑的是哪个数据库。
五、为什么我又加入了 SQL Server?
这其实是一个比较现实的问题。
现在很多互联网开发者默认想到的数据库通常是:
text
MySQL
PostgreSQL
但是如果做过政企、制造业、ERP、金融或者一些传统企业项目,就会发现:
SQL Server 依然有大量存量用户。
尤其很多公司的老系统可能已经运行了十几年。
数据库就是:
text
Microsoft SQL Server
你不可能为了接入一个新的 Go 管理后台,就要求别人:
先把整个数据库迁移成 MySQL。
这显然不现实。
所以一个通用后台脚手架如果能够支持 SQL Server,实际使用范围会大很多。
GORM 官方生态本身就有 SQL Server Driver,因此在 ORM 这一层,接入并没有想象中那么复杂。
基本形式就是:
go
import (
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
dsn := "sqlserver://sa:password@localhost:1433?database=shiyu_admin"
db, err := gorm.Open(
sqlserver.Open(dsn),
&gorm.Config{},
)
真正麻烦的部分,反而在后面。
六、多数据库真正麻烦的是 SQL 方言
很多项目号称:
支持 MySQL、PostgreSQL、SQL Server。
实际上只测试了连接成功。
这还远远不够。
举一个非常简单的例子。
MySQL 获取当前时间经常写:
sql
SELECT NOW();
SQL Server 常见写法是:
sql
SELECT GETDATE();
再比如字符串拼接。
不同数据库的语法也可能不同。
还有获取表结构。
MySQL 可以查:
sql
information_schema.tables
PostgreSQL、SQL Server 虽然也提供元数据查询能力,但是字段定义、Schema 和返回结果并不完全一致。
一旦系统中出现类似:
go
db.Raw(`
SELECT ...
FROM information_schema.xxx
`)
这个时候就要警惕了。
因为:
你已经绕过 ORM,开始直接依赖数据库方言了。
七、我的处理方式:80% 走 ORM,20% 做 Adapter
我比较推荐这样的结构:
text
业务层
↓
Service
↓
Repository
↓
GORM
↓
Database Driver
↓
MySQL / PostgreSQL / SQLite / SQL Server
绝大多数增删改查都交给 GORM。
只有必须使用原生 SQL 的地方才单独适配。
例如:
go
type DatabaseInspector interface {
GetTables() ([]TableInfo, error)
GetColumns(table string) ([]ColumnInfo, error)
GetDatabaseSize() (int64, error)
}
然后分别实现:
text
MySQLInspector
PostgresInspector
SQLiteInspector
SQLServerInspector
业务层调用:
go
inspector.GetTables()
而不需要关心:
text
information_schema
sys.tables
sqlite_master
pg_catalog
到底使用哪一个。
这其实就是很典型的 Adapter Pattern ------ 适配器模式。
八、Docker Compose 也应该跟着数据库拆分
程序代码支持多数据库还不够。
开发者真正关心的问题是:
我怎么跑起来?
所以 ShiyuAdmin 现在也把不同数据库的启动方式拆开。
例如:
text
docker-compose.yml
docker-compose.mysql.yml
docker-compose.sqlite.yml
docker-compose.sqlserver.yml
这种设计我觉得对于开源项目非常有必要。
如果想测试 SQL Server:
bash
docker compose -f docker-compose.sqlserver.yml up -d
Compose 可以负责启动:
text
SQL Server
↓
初始化数据库
↓
Redis
↓
ShiyuAdmin
而不是要求使用者:
- 自己安装 SQL Server;
- 自己创建数据库;
- 自己启动 Redis;
- 自己修改配置;
- 再启动 Go 服务。
对于一个开源项目来说:
降低启动成本,本身就是产品体验的一部分。
九、为什么要加 Health Check?
Docker 多容器部署还有一个非常容易踩坑的地方。
很多人会写:
yaml
depends_on:
- sqlserver
然后认为:
text
SQL Server 已经启动了
→ Go 程序就可以连接了
实际上并不是。
容器进程启动和数据库真正可用之间,可能还有几十秒时间。
于是就会出现经典问题:
text
backend started
↓
database not ready
↓
connection failed
↓
backend exits
所以更好的方式是增加:
yaml
healthcheck:
例如检测 SQL Server 的 1433 端口是否真正可用。
数据库 Ready 之后,再执行初始化任务:
text
SQL Server Ready
↓
创建 shiyu_admin 数据库
↓
数据库初始化完成
↓
启动 ShiyuAdmin
这种启动链条才比较稳定。
十、SQLite 为什么也值得支持?
很多人可能觉得:
后台管理系统为什么要支持 SQLite?
其实我觉得 SQLite 非常适合下面几个场景。
1. 快速体验
用户:
bash
git clone
然后直接启动。
不需要先准备:
text
MySQL
Redis
数据库账号
数据库密码
对于开源项目体验非常友好。
2. 单机工具
如果以后把后台系统改造成:
text
AI 工具
内部管理工具
桌面应用
单机服务
SQLite 非常合适。
3. 自动化测试
测试环境中临时创建 SQLite 数据库也非常方便。
所以我的观点一直是:
SQLite 不一定适合你的生产系统,但非常适合你的开发工具链。
十一、多数据库支持最容易踩的几个坑
做完这一套以后,我觉得有几个地方尤其值得注意。
1. 不要大量写 Raw SQL
比如:
go
db.Raw(...)
写得越多,多数据库适配成本越高。
优先使用:
go
Where
Find
Create
Updates
Delete
Scopes
让 ORM 帮你生成 SQL。
2. 不要过度依赖数据库特殊字段类型
例如:
text
JSONB
ENUM
ARRAY
GEOMETRY
这些数据库特色能力很好用。
但是使用得越多,迁移其他数据库就越困难。
如果项目明确只运行 PostgreSQL,那完全没问题。
但如果目标是:
通用后台脚手架
就应该尽量使用通用字段类型。
3. 注意主键和自增机制
不同数据库对于:
text
AUTO_INCREMENT
SERIAL
IDENTITY
处理方式并不一样。
最好交给 GORM Schema 和 Migrator 去处理,而不是手写建表语句。
4. 注意分页
业务代码最好写:
go
db.Offset(offset).Limit(pageSize)
不要自己拼:
sql
LIMIT
TOP
OFFSET FETCH
5. 每一种数据库都要真正跑测试
最危险的一句话是:
理论上应该支持。
数据库适配最怕"理论支持"。
真正靠谱的做法应该是:
text
MySQL 跑一次
PostgreSQL 跑一次
SQLite 跑一次
SQL Server 跑一次
至少保证核心的:
text
登录
用户管理
角色管理
菜单管理
权限
增删改查
分页
迁移
全部正常。
十二、多数据库支持,本质上是在降低项目耦合
很多人看到这里可能觉得:
不就是多加几个数据库驱动吗?
其实我觉得不是。
做这件事的过程中,你会被迫重新审视自己的代码。
你会开始思考:
text
这个 SQL 是不是写死了?
这个 Repository 是不是和 MySQL 耦合了?
这个字段是不是用了 PostgreSQL 特性?
这个表结构查询是不是应该抽象?
这个数据库初始化是不是应该独立?
最后你会发现:
多数据库适配本质上是在倒逼项目做架构解耦。
如果一个 Go 项目能够比较轻松地从 MySQL 切换到 PostgreSQL、SQLite,甚至 SQL Server,那么至少说明:
业务层和基础设施层之间的边界已经比较清晰。
这比"支持四种数据库"本身更加重要。
十三、ShiyuAdmin
上面这些实践目前都在我的开源项目 ShiyuAdmin 中持续完善。
ShiyuAdmin 是一个 Go + Gin + GORM + React + Ant Design Pro 实现的通用后台管理系统。
目前主要包括:
- 用户管理
- 角色管理
- 菜单管理
- 部门管理
- RBAC 权限控制
- JWT 登录认证
- 动态菜单
- 操作日志
- Redis 缓存管理
- 系统监控
- 数据管理
- ECharts 数据仪表盘
- Docker Compose 部署
如果你正在学习:
text
Go 后台开发
Gin
GORM
RBAC
React
Ant Design Pro
Docker
多数据库适配
也可以直接拉下来研究。
项目地址:
text
https://github.com/Rodert/ShiyuAdmin
开源项目就是这样。
不一定一开始就做到多么完善,但是可以一边解决真实问题,一边把这些能力沉淀下来。
如果这个项目对你有帮助,也欢迎 Star、Issue 或 PR。
写在最后
现在是最好的时代。中国有全世界最高性价比的制造业,有发达的网络和全球物流。
只要你愿意,你几乎可以买到这个世界上任何地方生产的、任何你想要的商品。你可以用很低的成本,撬动全球的资源为你服务。
不过,真正稀缺的,从来不是商品,而是注意力、判断力,以及把事情做成的能力。
所以,这是最好的时代,也是最坏的时代。
我是王仕宇,关注 AI、Web3 与开源,持续探索如何把技术做成产品,把产品转化为真实价值。