12.4 Java Database Connectivity(JDBC)
Java Database Connectivity(JDBC)是一种标准,让你能够在JVM应用程序中访问数据库管理系统(DBMS)服务器。流行的企业级DBMS服务器包括:
- Oracle Database;
- Oracle MySQL;
- MariaDB;
- Microsoft SQL Server;
- IBM DB2;
- PostgreSQL。
要在JVM应用程序中使用JDBC连接到DBMS服务器,需要为目标数据库系统定制的JDBC驱动程序。应用程序将加载JDBC驱动程序,并提供一个通常包含服务器的主机名和端口以及凭证的连接字符串。JDBC系统将确保合适的驱动程序得以妥善地初始化,而该驱动程序将连接到数据库并返回一个Connection对象,供应用程序用来与数据库通信。
JDBC类似于Microsoft开发环境中的ADO.NET和ODBC标准。
JDBC标准并不要求DBMS服务器本身是使用Java或JVM实现的,但JDBC驱动程序通常是使用Java(或其他JVM语言)编写的。JDBC标准允许JDBC驱动程序使用原生(特定于平台的)驱动程序或库。使用原生软件的JDBC驱动程序安装起来可能更复杂,可能并非与所有JVM兼容平台都兼容。
JDBC驱动器分4类。
| 类型 | 描述 |
|---|---|
| 第1类:JDBC-ODBC bridge | 在幕后使用基于ODBC的驱动程序与数据库服务器通信的JDBC驱动程序 |
| 第2类:原生API驱动程序 | 使用本地安装的原生驱动程序与数据库服务器通信的JDBC驱动程序 |
| 第3类:网络协议 | 连接到管理数据库连接的中间层(中间件)的JDBC驱动程序。中间件通常运行在独立的服务器上。与数据库通信的逻辑是在中间层实现的,因此客户端不需要针对特定数据库的驱动程序 |
| 第4类:数据库协议驱动程序 | 完全使用Java(或其他JVM语言)实现,因此独立于平台的JDBC驱动程序。这种驱动程序直接连接到数据库服务器 |
JDBC驱动程序通常可随JVM应用程序一起安装,为此只需将所需的文件放在JVM的应用程序类路径中即可。在有些情况下,驱动程序必须与JVM应用程序分开安装,这通常是因为需要随驱动程序安装平台特定的软件,或许可条款要求这样做。数据库厂商或开源小组负责为其产品开发和维护JDBC驱动程序。由于Java的市场影响力巨大,大多数流行的DBMS系统都有相应的JDBC驱动程序,Microsoft甚至免费提供用于其Microsoft SQL Server产品线的JDBC驱动程序。
创建、更新和删除记录(也被称为CRUD)的操作是使用流行的SQL查询语言来完成的。大多数数据库系统都支持通过SQL来新建数据库以及创建或更新表、索引和视图等。在很大程度上说,SQL查询语言已标准化,因此只要小心行事,编写的应用程序就能与各种不同的数据库服务器交互。由于每种数据库都有其独特的SQL语言扩展(包括定制的函数名、特殊的数据类型以及自定义查询语法),因此实际上前述目标很难实现。数据库系统可自由决定要支持哪些SQL语句和功能;JDBC标准对此并没有明确的规定。
12.4.1 H2数据库
本章将使用H2数据库。H2是一种独立的开源DBMS系统,它相对较小,完全是使用Java编写的。这个数据库系统将文件写入本地文件系统,甚至将整个数据库都加载到内存中。它不像MySQL和PostgreSQL那样需要安装,同时它创建的数据库也不需要过多的维护。你只需将一些JAR文件添加到ClassPath中,JVM应用程序就能访问H2数据库系统及其属于第4类的JDBC驱动程序。
从某种程度上说,H2类似于流行的无版权(public domain)SQLite数据库。虽然有针对SQLite的开源包装器和JDBC驱动程序,但在Groovy等JVM语言中,H2使用起来更方便,因为编写它时全面考虑到了JVM。。
诸如H2等独立的数据库系统非常适合用于单用户应用程序,但对于需要向数百名同时读写数据库的用户提供服务,因此需要存储数GB数据或需要其他企业级可靠性或功能的多线程应用程序来说,这可能不是最佳的选择。然而,千万不要低估了H2这样的数据库系统的威力。例如,H2支持运行数据库服务器,这在多个应用程序需要访问同一个数据库时很方便;它还提供了高级集群选项。另外,它还自带了一个命令行工具,可用于通过方便的操作系统命令行界面来查询、分析、备份和恢复数据库。
建议你在阅读本章时随时参考H2数据库项目的主页(http://www.h2database.com)。
12.4.2 创建内存数据库
在这个项目中,我们将创建一个内存数据库,它在应用程序终止时将被立即删除。在原型阶段,这样做很方便,因为修改数据库结构后,手动更新数据库中的数据不会遇到任何麻烦。另一个优点是无需跟踪数据库存储路径。
要使用JDBC连接到数据库,必须提供一个连接字符串,它告诉JDBC应使用哪个驱动程序、数据库的位置以及访问数据库所需的凭证,它还包含随DBMS而异的配置选项。
在较旧的JDBC版本中,必须使用代码手动注册JDBC驱动程序。当前,较新的JDBC驱动程序会自动注册,因此通常只需将驱动程序的JAR文件放到应用程序的类路径中,它们就可供应用程序使用。
在这个示例中,我们将使用H2的嵌入模式,这意味着将整个H2数据库系统嵌入到应用程序中,因此无需运行外部服务器应用程序。我们将创建一个应用程序终止后就将消失的内存数据库。要访问这种H2数据库,必须向JDBC提供的连接字符串类似于下面这样:
String connectionString = "jdbc:h2:mem:blogs;DB_CLOSE_DELAY=-1"
连接字符串是一个用冒号分隔的列表,下面来看看其中的各个部分。
- JDBC连接字符串的第一项都是
jdbc。 - 第二项是标识数据库系统的名称。JDBC驱动程序在加载期间注册其名称。由于Ivy已经将H2 JDBC驱动程序添加到项目的ClassPath中,因此JDBC系统能够识别名称
h2。 - 第三项是数据库的名称。H2并不要求内存数据库有名称,但正如你将在下一项中看到的,在这个示例中实际上必须有名称。
- 第四项让H2将数据库保留在内存中,即便没有活动的连接亦如此。通常,对于没有活动连接的内存数据库,H2会将其删除,但我们希望只要应用程序在运行,就能访问这个数据库。为此,我们指定了选项
DB_CLOSE_DELAY=-1。
在连接字符串中,只有前两项是标准的,其他项通常因JDBC驱动程序而异。
下面来编写一些代码。在Eclipse IDE中,打开文件Main.groovy。我们将创建一个打开新建数据库连接的方法,为此请在Main类中添加如下方法:
def createDatabaseConnection() {def connection = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")return connection}
请使用Eclipse IDE快捷键Ctrl +空格来补全类名
DriverManager,这样Eclipse IDE将自动替你编写导入java.sql.DriverManager的语句。
变量connection包含的对象指向创建的数据库连接,可用来向数据库系统发布命令。变量connection所属类型的全限定名为java.sql.Connection,这是一个Java接口,但由于我们编写的是Groovy代码,因此无需指定类型。DriverManager是JDBC系统提供的一个类,知道如何访问已注册的JDBC驱动程序。
在这个示例中,我们使用的是嵌入的数据库系统,当H2的JDBC驱动程序将连接字符串传递给嵌入的H2数据库引擎时,H2将新建一个临时的内存数据库。应用程序终止时,这个数据库将被自动删除,因为我们在连接字符串中指定了选项DB_CLOSE_DELAY=-1。
我们在本章开头创建的是基于控制台的应用程序,但后面将修改其实现,使其变成Web服务。因此,下面来重写方法main(),使其调用刚创建的方法createDatabaseConnection():
static void main(String[] args) {def app = new Main()def connection = app.createDatabaseConnection()connection.close()}
方法main()是静态的,不能直接访问Main类的实例变量和实例方法。有鉴于此,我们创建Main类的一个实例,并使用这个实例来调用Main类的实例方法。
这里必须指出一个常见问题。必须将可打开的JDBC对象关闭,以防泄露宝贵的系统资源。打开数据库连接后,即便引发了异常,连接依然将处于打开状态,直到你将其关闭。这可能导致无法预料的问题和程序崩溃,因为数据库资源是有限的。因此,推荐使用try…catch块来使用JDBC对象,这样可在finally块中将连接关闭,这在第4章介绍过。通过这样做,可确保连接始终得以妥善地关闭,即便引发了异常。
现在可以创建数据库了,但空的数据库不是很有趣。我们需要创建一些用于存储数据的表,这些数据在关系型数据库中被称为记录。我们先直接使用JVM的JDBC类,然后再换成Groovy内置的JDBC包装类,这旨在让你明白使用Groovy与关系型数据库系统通信更容易。
我们将创建一个微型博客应用程序。首先来创建一个用于存储应用程序用户的表,为此在Main类中添加如下方法:
def createDatabaseStructure(connection) {def statement = connection.createStatement()def sqlUsers = """CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL,name VARCHAR(255),PRIMARY KEY (id))"""statement.executeUpdate(sqlUsers)}
方法createStatement()返回一个实现了接口java.sql.Statement的对象。为执行SQL语句,使用了一个Statement对象。
SQL语句就是包含SQL代码的普通字符串。在这里,我们执行了SQL查询CREATE TABLE,它新建一个表。这个表包含两个字段:id和name,其中id是主键,其值必须是独一无二的,可用来快速识别记录。通过指定选项AUTO_INCREMENT,我们告诉H2数据库,它必须自己生成id值。每次创建记录时,都自动将id加1。字段name是一个简单的文本字段,最多可包含255个字符。
即便是直接使用JDBC类,使用Groovy来编写程序也可节省大量的时间。在Java中,JDBC类的方法通常可能引发
checked异常,因此必须在try…catch块中调用它们,或在调用它们的方法中添加throws语句。Groovy不关心方法是否会引发checked或unchecked异常。
接下来需要创建一个用于存储博文的表,为此在方法createDatabaseStructure()末尾添加如下代码:
def sqlBlog = """CREATE TABLE blog (id INT AUTO_INCREMENT NOT NULL,title VARCHAR(255) NOT NULL,user INT NOT NULL,post CLOB,PRIMARY KEY (id),FOREIGN KEY(user) REFERENCES user(id))"""statement.executeUpdate(sqlBlog)statement.close()
数据表blog包含字段user,它指向数据表user中的一条记录。user字段是一个外键。外键指向一条记录,这条记录通常位于数据库中的另一个表中。在这里,它使用用户的id字段来标识创建博文的用户。
别忘了在方法main()中调用方法createDatabaseStructure(),为此在调用createDatabaseConnection()的代码后面添加调用它的代码:
static void main(String[] args) {def app = new Main()def connection = app.createDatabaseConnection()app.createDatabaseStructure(connection)connection.close()}
现在可以运行这个应用程序了。如果一切正常,你将看不到任何输出,因为我们还没有在代码中添加print()语句。如果看到了栈跟踪,请复查代码和SQL语句。
下面来添加一些硬编码的记录。这次我们将使用Groovy类Sql的实例来与数据库通信。请在Main类中添加如下方法:
def addDemoRecords(connection) {def sql = new Sql(connection)def createdUsers = sql.executeInsert("INSERT INTO user (name)VALUES (?)", ["Admin"])def userId = createdUsers[0][0]sql.execute("""INSERT INTO blog (title, user, post)VALUES (?, ?, ?)""",["Test post", userId, "This is a test post"])sql.close()}
别忘了使用组合键Ctrl + 空格(在macOS中为cmd + 空格)来补全类名
Sql,并选择groovy.sql包中相应的类。
Groovy类groovy.sql.Sql的方法executeInsert返回为主键字段生成的值。由于INSERT查询可能创建多条记录,而每条记录都可能有多个主键字段,因此这个方法返回嵌套列表,其中每个列表都包含一条记录的主键字段。在这里,我们仅为值添加了一条记录。由于数据表blog只有一个主键字段——id,因此可通过读取createdUsers[0][0]来获取生成的用户id。其中第一个索引指定了行(记录),而第二个索引指定了要读取这条记录的哪列(字段)。
与数据库连接对象一样,Sql对象也将占用宝贵而有限的数据库资源。在不再需要这种对象时,如果没有将其关闭,可能出现奇怪的问题。因此,在真实的应用程序中,总是使用
try…catch块来确保即便引发了异常,这种对象也将得以妥善地关闭。
在方法main()中,在调用createDatabaseStructure()的代码后面添加调用addDemoRecords()的方法:
....def app = new Main()def connection = app.createDatabaseConnection()app.createDatabaseStructure(connection)app.addDemoRecords(connection)connection.close()...
运行这个应用程序,它也将在不打印任何消息的情况下退出。下面来改变这种现状:打印博文的XML表示。
JDBC类似于Microsoft开发环境中的ADO.NET和ODBC标准。