跳到主要内容

7-3 创建、使用和关闭数据库

创建数据库

使用文件

要在我们的插件中使用数据库,我们需要先创建数据库对象(还记得吗,在 Kotlin 中一切都是对象),这需要在 onEnable 中完成:

if (config.getBoolean("enabled")) {     // 仅当启用插件时才创建数据库
    val db = DBMaker.fileDB(File(dataFolder, "backpacks.db")).make()
}

我们首先构造一个 File 对象,File 是 Java 标准库的一部分,用来表示一个文件,它的构造函数用法如下:

/**
 * 构造一个文件对象,表示 `parent` 目录中名为 `name` 的文件。
 */
class File(parent: File, name: String)

dataFolderJavaPlugin 的一个属性,类型为 File,表示插件数据所在的目录,插件的配置和其它数据都在这里存放。先前我们在这里存储过 config.yml,不过是通过 saveDefaultConfig 做了这件事,而现在轮到我们来亲自访问这个目录了。

我们使用 backpacks.db 作为文件名,数据库的名称可以任意(笔者在这里本来是想使用 data.ciallo 的,不过为了显得专业让文件的含义更清晰,还是使用了更为正式的名称)。

无旗之下

文件的扩展名仅是一种形式上的约定,与文件的实际类型并无必然的联系 —— 它们只是提示该文件可能是什么类型。一个后缀为 .exe 的文件也完全可以包含 Kotlin 源代码,MP3 音频或者一张图片。

构造数据库对象

dataFolder 和数据库名通过 File 结合起来,就得到数据库文件对象,然后我们就可以使用 MapDB 的 fileDB 方法创建数据库对象:

/**
 * 使用指定的文件存储数据库内容。
 * 返回的对象是一个构造器,稍后可以通过其 `make` 方法构造所需的数据库。
 */
fun fileDB(f: File): DBMaker.Maker

fileDB 返回一个使用指定文件的数据库构造器,构造器的功能和构造函数差不多,它们能用来构造另一个类的对象。我们随后调用它的 make 方法创建数据库对象

顺便一提,你可以在 MapDB 的官方网站 上找到它的文档。MapDB 是用 Kotlin 编写的,所以文档使用的也是 Kotlin 的语法。

关闭数据库

数据库创建好了固然令人兴奋,但这并不是使用数据库的全部。我们需要在服务器关闭时保存数据,MapDB 将数据的保存合并在关闭数据库的操作中了,也就是说,我们需要在服务器关闭时关闭数据库。如果不这么做,数据库中的内容就会丢失。

onEnable 一样,Bukkit 为 JavaPlugin 提供了 onDisable 方法,允许插件在停用时执行一些操作,我们打算在这里保存数据,所以在 Main 中添加如下代码:

override fun onDisable() {
    // 要做之事
}

为了关闭数据库,我们需要使用先前创建的 db 对象,但是它只在 onEnable 中可用。为了能在 onDisable 中使用它,我们需要将 db 变量提升为属性

class Main : JavaPlugin() {
    private var db: DB? = null  // 此时无法决定 db 的内容,因为可能不会创建数据库

    override fun onEnable() {
        saveDefaultConfig()
        if (config.getBoolean("enabled")) {
            // 如果创建了数据库,就将 db 更改为数据库对象
            db = DBMaker.fileDB(File(dataFolder, "backpacks.db")).make()
        }
    }

    override fun onDisable() {
        // 如果 db 不是 null,就调用 close 方法
        db?.close()
    }
}

由于我们有可能不会创建数据库,所以不必过早地初始化 db,我们可以让其初始为 null。当数据库被创建后,我们再将 db 的值修改为数据库对象,从而能在 onDisable 中再次找到并关闭它。为了能修改这个属性,需要使用 var 而不是 val

db?.close() 通过 ?. 运算符,在 db 不是 null 的时候调用 close 方法,将数据库内容写入文件

注意

插件运行中任何没有保存的数据都会在服务器重启后丢失,包括所有的变量值、已创建的对象等。

存储在 PDC 中的数据,以及对世界的修改,都由 Bukkit 负责保存,而像集成式数据库这种由插件自行维护的系统,必须手动保存。

存取数据

创建数据表

创建数据库后,我们就可以在数据库中创建一张映射表

val bpMap = db!!.hashMap("backpacks", Serializer.UUID, Serializer.BYTE_ARRAY).createOrOpen()

hashMap 负责映射表的创建,它接受三个参数,分别是表名、键类型和值类型。

createOrOpen 告诉 MapDB 读取这张表的内容,如果不存在就创建一个。这样,如果先前我们在这张表中存储了数据,就可以继续使用它们。

我们计划用 UUID 存储玩家,而将物品栏信息序列化ByteArray,即字节数组。之所以选用 ByteArray 这个类型,是因为 Bukkit 刚好提供了将物品栏转换为 ByteArray 的方法,我们稍后会介绍。

备注

为什么要进行序列化而不是直接使用 Inventory 呢?因为数据库在组织管理数据时,对于数据类型有一定的要求。我们需要将物品信息转换为数据库可以处理的格式ByteArrayUUID 都是 MapDB 支持的格式)。

事实上,MapDB 可以处理任意类型的数据,但由于它不知道物品信息的具体格式,把物品信息硬塞进 MapDB 里就会很慢,而且还不可靠。Bukkit 更了解物品信息的结构,所以由它将数据转换ByteArray,再由 MapDB 去存储,才是最佳组合。

术语库

序列化(Serialization) 就是将对象转换成字节序列的过程,通常用来存储数据,这些数据稍后会被反序列化(Deserialization) 以获取原始的对象。

最简单的序列化方法就是把对象对应的内存复制下来,但这么做不怎么靠谱。Bukkit 提供了单独的方法对物品栏数据进行序列化。

读写键值

用这种方法创建的 bpMap 实现了 MutableMap 接口,键的类型为 UUID、值的类型为 ByteArray。既然如此,我们可以像之前一样使用 [] 来读写数据,只需要提供键和值:

bpMap[player.uniqueId] = ...          // 设置值
println(bpMap[player.uniqueId])       // 读取值
bpMap.containsKey(player.uniqueId)    // 查询键是否存在

看吧,我们完全不需要使用复杂的函数,也不需要学习一门新语言,只需要使用熟悉的方法,而且 MapDB 还会把我们的修改自动存入数据库,这真是太棒了!