Go aah 框架集成数据库 ORM

更新

前言

在上一篇 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 三大类扩展点。数据库的操作当然是放到 “应用程序扩展点"。

2)应用程序扩展点
  • 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}

喜歡:
0
去到頂部