跳到主要内容

12-2 RDBMS 与 SQL(上)

来一起玩耍

前面我们已经提到过数据库的基本概念了,现在来正式地介绍一下其中的一个主流领域,即关系型数据库,正式名称为关系数据系统(Relational Database Management System、RDBMS)。顾名思义,被称作关系型数据库,是因为它存储着各种数据值之间的关系,不过什么是关系呢?这需要我们用另一种方法来思考数据的组成方式。

在 Kotlin 中,我们已经习惯了面向对象的数据组织方式:一个数据就是一个对象,而对象中包含着一系列其它数据(同样也是对象)。属性一词的中文很好地描述了这一点 —— 这些数据是属于某个对象的。

在 RDBMS 中,我们必须暂时抛开面向对象的这种思维模式,而改用另一种更简单的结构来描述我们想要的东西:

  • 数据是由一系列基本的数据项(Item) 构成的,例如玩家名、钻石数等。这些数据项不可以拆分或者组合(请暂时把玩家对象这个概念从脑子里移出去)。
  • 数据项之间可以存在某种关系(Relation),比如玩家的钻石数是和玩家名挂钩的。如果系统中有更多的数据,那么就可能会有多种不同的关系。

想象一下你在各种服务器上见到的新手套装,它们通常是用潜影盒打包起来的,里面包含着各类物资。如果把它们比作是一个个的对象,那么 RDBMS 就好像把各种物品分门别类存储的仓库,一个箱子中只存放着一种数据,而它们之间通过漏斗之类的装置连接起来。

举一个实际的例子,想象你在开发一个登录插件,当玩家从异地登录时,你希望玩家通过邮箱接收一个验证码。这个插件就涉及到以下的数据项:

  • 玩家的 UUID
  • 最后一次登录的 IP
  • 邮箱地址
  • 当前在使用的验证码
  • 验证码的有效期限

这些数据间存在哪些关系呢?要注意的是,同一个帐户在两处同时尝试登录的事情是可能发生的(也许在玩家登录的同时,有人在尝试盗取帐户),因此一个玩家可以拥有多个有效的验证码。

  • 最后一次登录 IP、邮箱地址都是唯一与玩家的 UUID 关联的,一个玩家也只能拥有一个邮箱地址和登录 IP,因此这三项数据可以组成一个关系。
  • 一个玩家可能同时拥有多个不同的验证码,每个验证码都有各自的有效期限,而且不同玩家的验证码是可能相同的,所以仅凭验证码是不能断定它的有效期限的,还必须加上它所属的玩家才行,因此这三项数据合并在一起组成一个关系。

是不是头都要绕晕了?没关系,RDBMS 的概念理解起来确实有些麻烦,因为它和我们通常的思维模式有些差距,不过在实际问题中(至少就插件开发而言),通常不会有太复杂的数据关联,绝大多数时候都只是简单到用户名与密码这样的关系,所以大家即使刚才没有一下子就想到正确的关系,也不用太过担心。

备注

为什么 RDBMS 要这么麻烦地把完整的世界拆得支离破碎呢(这是笔者在初学 RDBMS 时最大的疑问)?这其实是为了高效地实现数据查询与修改。如果数据只是一行行简单的数字或者文本(接下来将提到这样的结构),那么诸如找出所有第二列大于 100 的行就有很多高效的方式来实现,这要比地毯式搜索快得多。

表,更多的表

RDBMS 的核心是一系列表(Table),就像你在 Excel 中见到的工作表一样:

玩家名钻石数金粒数
Aluka64100
Nyaci1280
Ted-655360.3

RDBMS 的行与行之间、列与列之间都是没有先后顺序之分的。你可以在查询数据时要求 RDBMS 对数据进行排序,但数据本身是无序存放在表中的。

每个表其实就是刚才提到的关系的一种表达方式,其中数据项作为列(Column),而每一条记录作为行(Row)。在上面的例子中,每一行的三个数据,彼此之间就被关联起来。例如,要找到 Aluka 所拥有的钻石数,只需要告诉 RDBMS 以下两个步骤:

  1. 找到玩家名为 Aluka 的行。
  2. 读取这一行中的钻石数。

每一行之间原本互相无关的数据,因为它们处在同一行中,而能互相传导 —— 只要能设法定位到某一行,就能立刻获知这一行中的全部信息,这就是关系的体现!RDBMS 的这种结构,使得我们可以把一些复杂的查询转换为一系列基本操作,然后交给 RDBMS 高效地执行。

Sequel

那么,要怎么告诉 RDBMS 我们想做的操作呢?让计算机做点什么的方法是?—— 编写代码嘛!不过,由于市面上的 RDBMS 产品众多,如果每一种都使用着自己的方言,那要在不同的数据库之间切换就会非常麻烦。设计 RDBMS 的人发明了一种名为结构化查询语言(Structured Query Language,SQL) 的新东西,看上去就像这样:

SELECT diamonds FROM deposit WHERE playerName = 'Aluka';

SQL 是一种命令式的语言,也就是说,它就像 Minecraft 中的命令一样,一条一条执行,每一条代表一个操作。尽管部分 RDBMS 支持过程化 SQL,但通常来说认为对客户端程序而言,SQL 是非过程的 —— 不能在里面编写函数,或者使用 while 循环之类的流程控制功能。不过,即使不使用这些功能,SQL 本身的设计也可以表述很复杂的表格操作,这一点我们很快就会见到。

RDBMS 通常在 Bukkit 服务器之外,作为独立的程序运行。打算使用 SQL 的程序(即客户端)可以将 SQL 代码(术语称作语句(Sentence))以字符串的形式发送给 RDBMS,RDBMS 解析并执行之后,将结果发回给我们的程序,这样客户端就不需要知道怎么将 SQL 语句翻译成具体的数据库操作,而只需要知道如何构造它们就行了。

转义相对论

插件运行在服务器上,在这里却作为客户端来使用 RDBMS 提供的功能,看上去有些奇怪是不是?其实这是很正常的现象,因为服务器和客户端的角色是针对一个会话(Session) 而言的。对于游戏会话(处理游戏逻辑)而言,Bukkit 是服务端,玩家的 Minecraft 是客户端;而对于插件与 RDBMS 之间交换数据的会话而言,插件就变成了客户端,RDBMS 要处理插件的请求。

我的 SQL

为了使用和测试 SQL 语句,我们需要一个 RDBMS 服务端。遗憾的是,由于 RDBMS 本身是一个相当复杂的系统,不论是 Mojang 还是 Bukkit 都没有在服务端中内置 RDBMS,如果想要使用,我们就需要另行安装。市面上提供 SQL 支持的 RDBMS 非常多,但在 Minecraft 服务器领域,最常用的还是 MySQL,或许是因为它性能优秀,也或许仅仅是因为它不需要收费(笑)。

值得一提的是,MySQL 是由 Oracle 开发的,后者现在也是 Java 的发行商以及同名数据库 Oracle 的开发公司。

你可以从 MySQL 的官方网站 下载 MySQL 的安装程序,如果你在下载过程中看到了如下图所示的界面,请点按 No thanks, just start my download. 链接,因为我们不打算使用 Oracle 提供的在线功能。

Skip Accounts

下载完成后简单安装即可,如果安装程序询问安装方式,可以选择 Typical(标准)。当安装程序完成后,MySQL 会紧接着运行初始化程序,我们需要进行一些配置,才能使用 MySQL。

在初始化程序中,先一路点按 Next,直到看到如下的界面:

Password

MySQL 正要求你设置一个密码,作为测试用的服务端,我们不需要使用太过于复杂的密码,所以你可以随意选择你喜欢的。不过要记住,如果是在生产服务器中,应当选择更强一些的密码。

接下来的步骤就可以继续使用 Next 按钮,一路选择默认设置。到达最后一步之后,点按 Execute 按钮,让 MySQL 执行初始化操作。随后点按 Next 并完成初始化程序即可。

试用 SQL

安装完成后,在开始菜单中找到 MySQL 9.4 Command Line Client - Unicode(版本号可能不同)并将其打开,MySQL 会要求你键入密码:

Enter password:

输入先前设定的密码,并按 Enter,你将看到如下内容:

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 9.4.0 MySQL Community Server - GPL

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

这代表 MySQL 已经准备就绪,正在等待你键入指令。

那么我们就来试试吧!在 MySQL 中连续键入以下五条语句。每键入一条语句,都要按 Enter 来将其提交给 MySQL。像是 CREATE TABLE 这样的语句可能跨越多行,你可以直接在 MySQL 终端中按 Enter,MySQL 终端很聪明,它知道你的意思是新建一行而不是提交

CREATE DATABASE hello;

USE hello;

CREATE TABLE deposit (
    name    VARCHAR(255),
    diamond INTEGER,
    gold    INTEGER
);

INSERT INTO deposit VALUES ('Aluka', 64, 100);

INSERT INTO deposit VALUES ('Nyaci', 128, 0);

上面的这些语句创建了一张表,并插入了一些初始数据,现在键入如下的语句,查询这些数据:

SELECT * FROM deposit;

如果一切正常,你将看到以下内容:

+-------+---------+------+
| name | diamond | gold |
+-------+---------+------+
| Aluka | 64 | 100 |
| Nyaci | 128 | 0 |
+-------+---------+------+

MySQL 终端把查询到的数据以表格的形式可视化地显示出来方便我们查看。看到这样的数据,就说明 MySQL 在正常工作。如果哪里看上去不对,可以在终端中键入 DROP DATABASE hello;,然后从头再试一次。

你的 MySQL 如你所愿地运行起来了吗?希望如此。虽说我们确实在这里弄出来了一张表,还能查看其中的数据,但想必大家对于那些 SQL 语句的功能还是毫无头绪。那么,我们就在下一节中介绍一些常用的 SQL 操作。