Go aah 框架集成数据库 ORM
更新
- 20220219 - 增加 SQLBoiler 驱动、补充存储库及基本使用示例。
- 20220224 - 补充存储库小结及用法,详见4.1。也可参考《Go aah 框架的安全设计理解及应用实例》或 aah + SQLBoiler + MySQL 实例:https://github.com/vulcangz/aah-form-based-auth
前言
在上一篇 aah 框架介绍中 的 “IN-MEMORY” 存储方式,实现简单,响应快(内存操作当然快了),部署方便(可运行 aah build --single 生成一个单一的执行文件),比较适合于那些不需要对数据变更做持久化存储的场景,例如测试、或演示等。
但是在实际应用开发中,很少有不用到数据库的。那么问题来了,在 aah 框架的文档中,鲜见数据库相关内容介绍,官方甚至没有提供一个使用数据库的演示项目。那么 aah 框架该如何集成数据库呢?这里提供了在 aah 框架集成 ORM 的一种思路和基于 SQLBoiler 的具体实现。实现代码在之前 repo 的 database 分支中:https://github.com/vulcangz/aah-recycleview-backen...
集成思路及具体实现
1.数据库 ORM
建议选用成熟的 ORM 来实现。您当然可以选择基于 database/sql,但是用了 ORM 可以让自己活得轻松一点 :-)。这里选择 SQLBoiler(可选 GORM?这里有坑,具体见 4)。
2. 数据库配置
aah 配置的语法和结构是简单而灵活的。它与 HOCON 的语法非常相似。我们在应用程序配置文件中增加一个 `database {}` 配置块。示例:
database { driver = "mysql" host = "localhost" port = "3306" username = "root" password = "123456" name = "aah-recycleview-backend" max_idle_connections = 10 max_active_connections = 10 max_connection_lifetime = 2 }
3.数据库的加载
1)aah 服务器扩展点(aah Server Extension Point)
aah 服务器将应用程序和请求的生命周期阶段作为一个事件公开,它被称为服务器扩展点。aah 提供了应用程序、HTTP Engine 和 WebSocket 三大类扩展点。数据库的操作当然是放到 “应用程序扩展点"。
- OnInit:一旦 aah.App().Config() 被加载,就会发布事件 OnInit。在这个阶段,只有 config/aah.conf 和通过 arg --config 提供的外部配置文件被初始化。应用程序变量、路由、i18n、安全、视图引擎、日志等将在此事件后被初始化。
- OnStart:事件 OnStart 是在 aah 服务器启动前发布的。在这个阶段,应用程序被完全初始化。服务器还没有被启动。
- OnPreShutdown:从 v0.11.0 开始,当应用程序收到操作系统信号 SIGINT 或 SIGTERM 时,在触发优雅关机之前,OnPreShutdown 事件被发布。在此事件发生后,aah 会以 server.timeout.grace_shutdown 的配置值来触发优雅关机。
- OnPostShutdown:事件 OnPostShutdown 是在 aah 服务器成功优雅关闭后发布的,然后应用程序进行清洁退出。
从上述扩展点的生命周期来看,数据库配置应添加到 config/aah.conf,数据库连接应该放到 “OnStart” 阶段,而关闭连接自然要放到 OnPostShutdown 阶段......最终的加载方式及顺序如下所示:
// app/init.go func init() { // 默认情况下,一个给定的函数将作为添加序列被执行。 // 除非指定 "优先级"。 app := aah.App() app.OnStart(SubscribeHTTPEvents) // 开始添加自定义扩展点事件 app.OnStart(connectDatabase) // 数据存储库 repository 初始化 app.OnStart(InitRepository) app.OnStart(refreshCache, 3) //有优先权 app.OnStart(connectRedis, 2) //有优先权 // HTTP 中间件部分 // init() 的尾部添加自定义扩展点事件 app.OnPostShutdown(flushCache) app.OnPostShutdown(disconnectDatabase) app.OnPostShutdown(disconnectRedis) } func connectDatabase(e *aah.Event) { // 您的逻辑 } func connectRedis(e *aah.Event) { // 您的逻辑 } func refreshCache(e *aah.Event) { // 您的逻辑 } func disconnectDatabase(e *aah.Event) { // 您的逻辑 } func disconnectRedis(e *aah.Event) { // 您的逻辑 } func flushCache(e *aah.Event) { // 您的逻辑 }
当然,这只是伪代码演示。由于这是 app 代码的顶层目录,真把这几个事件回调函数写到 init.go 里,如何调用就成了问题了!由于 aah 的可扩展性比较强——这里还可扩展中间件、模板函数、自定义应用错误处理等,当应用较为复杂时 init.go 就显得有点乱。个人建议把数据库初始化相关代码统一放到 app\database(或 app\db?)目录下,模板函数主代码放到 app\util 目录下,以保持 init.go 文件始终简洁明了。
4.数据存储库的初始化及调用
按照常理,数据存储库(repository)的初始化应该放到 “OnStart” 阶段、数据库连接初始化的后面。但 GORM 的初始化放到这里时,在控制器(controllers)中调用包含预加载(Preload)调用的查询方法时就会出现空指针的错误:“runtime error: invalid memory address or nil pointer dereference”。如果把 GORM 存储库初始化放到控制器拦截器的 “Before” 中是可以的,但在关联(Association)查询时会出现空指针的错误。这在 Echo + GORM 的搭配中是没有出现过的。
SQLBoiler 放在 “OnStart” 阶段初始化是可以的。因 aah 框架的 context 是完全自定义的,故需要处理 context 不兼容的问题。虽然 SQLBoiler 支持 --no-context 模式。
4.1 小结(更新于20220224)
"SQLBoiler" 能够以 "--no-context" 模式结合 aah 框架使用。由于 SQLBoiler 是采取预生成 models 代码的方式,性能表现要好一些(主观感受)。开启调试日志后来看,在复杂点的 "TO MANY" 查询中生成的 SQL 代码是正常的,不会出现在 GORM 关联查询中出现多余了条件却无对应值的情况。
4.2 使用
数据存储库(repository)在 “OnStart” 阶段初始化,代码如下:
import "aah-recycleview-backend/app/repository" func init() { app := aah.App() // ... // 数据库连接 app.OnStart(database.SConnect) // 数据存储库初始化 app.OnStart(repository.InitRepo) // ... }
在控制器或安全接口中这样调用:
import ( repo "aah-recycleview-backend/app/repository" ) // 这样调用 user, err := repo.R().GetUserByEmail(authcToken.Identity) if err != nil { // No subject exists, return nil and error return nil, authc.ErrSubjectNotExists }
5.应用目录结构
集成数据库 ORM 后的应用目录结构大致如下,仅供参考:
aah-recycleview-backend +---app | +---controllers | | +---v1 | | \---web | +---database | +---generated | +---models | +---repository | \---util +---build | \---bin +---config | \---env \---docs \---postman
其中:
- 1)主体结构遵从 aah 标准布局。
- 2)models 目录下主要保存实体模型定义文件。
- 3)与用户交互处理及应用逻辑代码分别保存在 v1(REST API)或 web 目录下。
- 4)它们公用的底层数据库 CRUD 操作仓库保存在 repository 目录下。
6.测试
注意观察终端 console 日志输出。服务器启动正常后,在浏览器访问 http://localhost/v1/health,正常提示如下所示:
{"Status":"ok","QueryCount":2,"SlaveRunning":false}