使用数据库

TIP

本节所介绍的内容需要你安装一个数据库支持。如果你暂时不打算使用数据库,可以略过本节。

对于几乎所有大型机器人项目,数据库的使用都是不可或缺的。但如果每个插件都使用了自己的数据库,这将导致插件之间的兼容性非常差——用户要么选择同时安装多个数据库,要么只能放弃一些功能或者重复造轮子。为此,Koishi 设计了一整套对象关系映射(ORM)接口,它易于扩展并广泛地运用于各种插件中。同时,我们也提供了一些常用数据库的官方插件,足以应对绝大部分使用场景。

安装数据库

为了保证纯粹性,Koishi 的核心库 koishi-core 并不希望写入对某个具体的数据库的支持。因此,Koishi 的数据库采取了注入的方法。如果你是插件开发者,你可能不需要关心具体的数据库实现。但是如果你是 Koishi 的使用者,只有在安装了数据库之后,你才能正常使用所有的特性,包括下一节中的用户管理系统。首先你需要安装数据库依赖:

命令行npmyarn

然后与你添加插件同样的方法配置你的数据库:

koishi.config.js
module.exports = {
  plugins: {
    mysql: {
      host: '[host]',
      port: 3306,
      user: 'root',
      password: '[password]',
      database: '[database]',
    },
  },
}

运行程序后,你就可以通过访问 ctx.database 来调用数据库接口了:

// 获取用户数据
const user = await ctx.database.getUser(type, id)

// 修改群数据
await ctx.database.setChannel(type, id, { assignee: 123456789 })

你可以在后面的 API 文档中看到全部内置的 数据库方法

使用会话 API

对于 Koishi 内部的两个抽象表 User 和 Channel,我们在 会话对象 中封装了几个高级方法:

// 中间增加了一个第二参数,表示默认情况下的权限等级
// 如果找到该用户,则返回该用户本身
// 否则创建一个新的用户数据,权限为 authority
// 如果 authority 大于 0,则将新的用户数据添加到表中
session.getUser(id, authority, fields)

// 在当前会话上绑定一个可观测用户实例
// 也就是所谓的 session.user
session.observeUser(fields)

// 中间增加了一个第二参数,表示默认情况下的 assignee
// 如果找到该频道,则不修改任何数据,返回该频道本身
// 如果未找到该频道,则创建一个新的频道,代理者为 selfId
// 如果 selfId 大于 0,则将新的频道数据添加到表中
session.getChannel(id, selfId, fields)

// 在当前会话上绑定一个可观测频道实例
// 也就是所谓的 session.channel
session.observeChannel(fields)

使用 ORM API

Koishi 设计了一套 对象关系映射(ORM) 接口,它易于扩展并广泛地运用于各种插件中。

获取和删除数据

使用 database.get() 方法以获取特定表中的数据。下面是一个最基本的形式:

// 获取 schedule 表中 id 为 1234 或 5678 的数据行,返回一个数组
const rows = await ctx.database.get('schedule', [1234, 5678])

对于复杂的数据表,如果你只需要获取少数字段,你可以通过第三个参数手动指定要获取的字段:

// 返回的数组中每个元素只会包含 command, lastCall 属性
const rows = await ctx.database.get('schedule', [1234], ['command', 'lastCall'])

你还可以向第二个参数传入一个对象,用来查询非主键上的数据或者同时指定多列的值:

// 获取名为 schedule 的表中 assignee 为 onebot:123456 的数据行
const rows = await ctx.database.get('schedule', { assignee: ['onebot:123456'] })

对于需要进行复杂的数据库搜索的,ORM 也提供了相对应的方法:

// 获取名为 schedule 的表中 id 大于 2 但是小于等于 5 的数据行
const rows = await ctx.database.get('schedule', { id: { $gt: 2, $lte: 5 } })

你可以在 这里 看到更多相关的 API。

删除数据的语法与获取数据类似:

// 从 schedule 表中删除特定 id 的数据行
// 第二个参数也可以使用上面介绍的对象语法
await ctx.database.remove('schedule', [id])

添加和修改数据

除了获取和删除数据,常用的需求还有添加和修改数据。

// 向 schedule 表中添加一行数据,row 是要添加的数据行
// 返回值是添加的行的完整数据(包括自动生成的 id 和默认属性等)
await ctx.database.create('schedule', row)

修改数据的逻辑稍微有些不同,需要你传入一个数组:

// 用 rows 来对数据进行更新,你需要确保每一个元素都拥有 id 属性
// 修改时只会用 rows 中出现的键进行覆盖,不会影响未记录在 data 中的字段
await ctx.database.update('schedule', rows)

如果想以非主键来索引要修改的数据,可以使用第三个参数:

// 用 rows 来对数据进行更新,你需要确保每一个元素都拥有 onebot 属性
await ctx.database.update('user', rows, 'onebot')

使用 ORM API 扩展数据库

你也可以使用 ORM API 扩展数据库中的已有表,或者新建数据表。

扩展表

向内置的 User 表中注入字段的方式如下:

jsts

向 Channel 注入字段同理。

WARNING

实际上,在 Koishi v3 中还有以下的扩展方法:

// 向用户表中注入字符串字段 foo,简写形式
foo: 'string'

但是这个方法和下面的方法都会导致新增的字段不可为空(Not Nullable):

// 拓展定义的完整形式,配置了初始值 (initial),但是没有指定可空 (nullable)
foo: { type: 'string', initial: 'bar' }

这样会导致一个问题:在使用 koishi-plugin-mysql 时,如果你安装了用这样的方法扩展了数据库的插件,之后又因为一些原因卸载了它,那么数据库会因为新增行时 Koishi 没有对不可为空的字段赋值而报错。(#414open in new window)

如果你使用的是 mysql 数据库,或者你想让更多的人使用你的插件,那么,为了避免这个问题,在对已有数据库进行扩展的时候,不应该使用上面的两种写法,以确保新增的字段均可空。

如果你已经遇到了这个问题,则需要手动将这些字段修改为可空字段。关于 mysql 的基础操作在这里就不多做赘述了。

在这一节的稍后,我们会介绍可以使用这些扩展方法的场景。

WARNING

在安装了使用 ORM 对数据库扩展 JSON 类型的字段的插件后,可能会出现这个错误:

SyntaxError: Unexpected end of JSON input

在大部分情况下,这是因为 Koishi v3 的 ORM JSON 解析虽然能正常解析 NULL{} (或其他正常的 JSON 字符串),但不能正常解析空字符串。(#382open in new window)

这一问题的诱因之一就是上文提到的“不可为空”(Not Nullable)问题。如果:

  • 插件在扩展数据库时使用了 JSON 类型,但没有设置可空;
  • 插件是在机器人开发中途被安装,在此之前,数据库中已经产生了数据行,

那么,在安装插件后,在解析既有数据行的 JSON 字段时就会出现这个错误。(#415open in new window)

如果你已经遇到了这个问题,除了将相关字段手动设置为可空,还需要将已有的数据手动初始化为 NULL

定义新表

和扩展已有的表类似,定义新表也需要用到 Tables 接口。不过除了 fields 属性,我们还需要指定一些其他的属性。下面的例子取自 koishi-plugin-schedule 中对新表的定义:

jsts

你也可以对每个字段使用上文中描述过的方法进行更加详细的定义。关于定义新表时的各个配置项请参考 API 文档

TIP

在定义新表时,就不需要关注前文中提到的“不可为空”(Not Nullable)问题了。因为,不论是安装插件还是卸载插件,新表均独立运作,不会给其他表带来不可空的字段,从而引发上述问题。

WARNING

另一方面,如果你的插件的功能是对其他用户开发的插件进行增强,其中你需要扩展其他用户定义的数据库,那么你仍然会受到上文中提到的“不可为空”制约。

对单一数据库的支持

WARNING

下列方法都会导致你的代码只能用于单一数据库。使用下列方法代表你完全理解这一前提所带来的后果。

在一些特殊的情况下,ORM 可能无法满足你的使用需求。以下的方法能过作为面对这种情形的最终手段。

扩展数据库方法

要为已有的数据库添加新的自定义方法,需要调用 Database.extend()

jsts

这么做并不需要引入 koishi-plugin-mysql 作为插件的依赖。

实际上,编写数据库支持时也需要进行类似的方法定义,区别在于数据库支持中需要定义各个标准方法,而此处定义的则是用户的自定义方法。关于编写数据库支持的更多详情请参考 编写数据库支持

使用原始方案 不推荐

如果你需要调用原始的数据库接口而不是使用 ORM,你可以利用 Database 对象的特殊属性,它只在你使用了特定的数据库插件的时候有效:

// TypeScript 用户需要手动引入模块,否则将产生类型错误
import {} from '@koishijs/plugin-database-mysql'

// 直接发送 SQL 语句
ctx.database.mysql.query('select * from user')

其他数据库也有类似接口,例如 MongoDB 的原始接口可以通过 ctx.database.mongo 访问到。