Go 后台如何同时兼容 MySQL、PostgreSQL、SQLite 和 SQL Server?从 ShiyuAdmin 看 GORM 多数据库适配

最近我一直在维护自己的开源后台项目 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

而不是要求使用者:

  1. 自己安装 SQL Server;
  2. 自己创建数据库;
  3. 自己启动 Redis;
  4. 自己修改配置;
  5. 再启动 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 与开源,持续探索如何把技术做成产品,把产品转化为真实价值。

相关推荐
瀚高PG实验室1 小时前
SQL优化案例:存储过程、复杂SQL拆成简单SQL提升整体性能
数据库·sql·postgresql·瀚高数据库
winfredzhang2 小时前
从零构建家庭照片管理系统:Django 模块化单体实战,以及我踩到的 4 个真实坑
python·postgresql·django·sqlite·bootsrap
Wang's Blog2 小时前
PostgreSQL笔记36:执行计划基础解读与优化器成本模型
数据库·笔记·postgresql
Wang's Blog2 小时前
PostgreSQL笔记35:索引常见问题诊断与解决方案全景解析
数据库·笔记·postgresql
LayZhangStrive2 小时前
后端通识 - 后端开发职位接触的开发流程
数据库·prd·技术方案·库表设计·后端开发流程
爱吃火鸡面呀3 小时前
MySQL 查询与函数详解:从基础条件筛选到高级字符处理
数据库·mysql
奥莱维3 小时前
KNX酒店方案_KNX专用线与高端酒店技术逻辑
java·服务器·前端·数据库
牛大兵3 小时前
机顶盒获取局域网已经使用IP,开放的端口号,扫描摄像头,NAS,共享主机等开机自启局域网全量扫描工具
数据库·网络协议·tcp/ip
知行产研3 小时前
中国矿山无人驾驶出海的三重门
数据库·mysql·php