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对象,供应用程序用来与数据库通信。

12.4 Java Database Connectivity(JDBC) - 图1 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驱动程序。

12.4 Java Database Connectivity(JDBC) - 图2 从某种程度上说,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而异的配置选项。

12.4 Java Database Connectivity(JDBC) - 图3 在较旧的JDBC版本中,必须使用代码手动注册JDBC驱动程序。当前,较新的JDBC驱动程序会自动注册,因此通常只需将驱动程序的JAR文件放到应用程序的类路径中,它们就可供应用程序使用。

在这个示例中,我们将使用H2的嵌入模式,这意味着将整个H2数据库系统嵌入到应用程序中,因此无需运行外部服务器应用程序。我们将创建一个应用程序终止后就将消失的内存数据库。要访问这种H2数据库,必须向JDBC提供的连接字符串类似于下面这样:

  1. 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类中添加如下方法:

  1. def createDatabaseConnection() {
  2. def connection = DriverManager.getConnection("jdbc:h2:mem:test;
  3. DB_CLOSE_DELAY=-1")
  4. return connection
  5. }

12.4 Java Database Connectivity(JDBC) - 图4 请使用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()

  1. static void main(String[] args) {
  2. def app = new Main()
  3. def connection = app.createDatabaseConnection()
  4. connection.close()
  5. }

方法main()是静态的,不能直接访问Main类的实例变量和实例方法。有鉴于此,我们创建Main类的一个实例,并使用这个实例来调用Main类的实例方法。

这里必须指出一个常见问题。必须将可打开的JDBC对象关闭,以防泄露宝贵的系统资源。打开数据库连接后,即便引发了异常,连接依然将处于打开状态,直到你将其关闭。这可能导致无法预料的问题和程序崩溃,因为数据库资源是有限的。因此,推荐使用try…catch块来使用JDBC对象,这样可在finally块中将连接关闭,这在第4章介绍过。通过这样做,可确保连接始终得以妥善地关闭,即便引发了异常。

现在可以创建数据库了,但空的数据库不是很有趣。我们需要创建一些用于存储数据的表,这些数据在关系型数据库中被称为记录。我们先直接使用JVM的JDBC类,然后再换成Groovy内置的JDBC包装类,这旨在让你明白使用Groovy与关系型数据库系统通信更容易。

我们将创建一个微型博客应用程序。首先来创建一个用于存储应用程序用户的表,为此在Main类中添加如下方法:

  1. def createDatabaseStructure(connection) {
  2. def statement = connection.createStatement()
  3. def sqlUsers = """
  4. CREATE TABLE user (
  5. id INT AUTO_INCREMENT NOT NULL,
  6. name VARCHAR(255),
  7. PRIMARY KEY (id)
  8. )
  9. """
  10. statement.executeUpdate(sqlUsers)
  11. }

方法createStatement()返回一个实现了接口java.sql.Statement的对象。为执行SQL语句,使用了一个Statement对象。

SQL语句就是包含SQL代码的普通字符串。在这里,我们执行了SQL查询CREATE TABLE,它新建一个表。这个表包含两个字段:idname,其中id是主键,其值必须是独一无二的,可用来快速识别记录。通过指定选项AUTO_INCREMENT,我们告诉H2数据库,它必须自己生成id值。每次创建记录时,都自动将id加1。字段name是一个简单的文本字段,最多可包含255个字符。

12.4 Java Database Connectivity(JDBC) - 图5 即便是直接使用JDBC类,使用Groovy来编写程序也可节省大量的时间。在Java中,JDBC类的方法通常可能引发checked异常,因此必须在try…catch块中调用它们,或在调用它们的方法中添加throws语句。Groovy不关心方法是否会引发checkedunchecked异常。

接下来需要创建一个用于存储博文的表,为此在方法createDatabaseStructure()末尾添加如下代码:

  1. def sqlBlog = """
  2. CREATE TABLE blog (
  3. id INT AUTO_INCREMENT NOT NULL,
  4. title VARCHAR(255) NOT NULL,
  5. user INT NOT NULL,
  6. post CLOB,
  7. PRIMARY KEY (id),
  8. FOREIGN KEY(user) REFERENCES user(id))
  9. """
  10. statement.executeUpdate(sqlBlog)
  11. statement.close()

数据表blog包含字段user,它指向数据表user中的一条记录。user字段是一个外键。外键指向一条记录,这条记录通常位于数据库中的另一个表中。在这里,它使用用户的id字段来标识创建博文的用户。

别忘了在方法main()中调用方法createDatabaseStructure(),为此在调用createDatabaseConnection()的代码后面添加调用它的代码:

  1. static void main(String[] args) {
  2. def app = new Main()
  3. def connection = app.createDatabaseConnection()
  4. app.createDatabaseStructure(connection)
  5. connection.close()
  6. }

现在可以运行这个应用程序了。如果一切正常,你将看不到任何输出,因为我们还没有在代码中添加print()语句。如果看到了栈跟踪,请复查代码和SQL语句。

下面来添加一些硬编码的记录。这次我们将使用Groovy类Sql的实例来与数据库通信。请在Main类中添加如下方法:

  1. def addDemoRecords(connection) {
  2. def sql = new Sql(connection)
  3. def createdUsers = sql.executeInsert("INSERT INTO user (name)
  4. VALUES (?)", ["Admin"])
  5. def userId = createdUsers[0][0]
  6. sql.execute("""
  7. INSERT INTO blog (title, user, post)
  8. VALUES (?, ?, ?)""",
  9. ["Test post", userId, "This is a test post"])
  10. sql.close()
  11. }

12.4 Java Database Connectivity(JDBC) - 图6 别忘了使用组合键Ctrl + 空格(在macOS中为cmd + 空格)来补全类名Sql,并选择groovy.sql包中相应的类。

Groovy类groovy.sql.Sql的方法executeInsert返回为主键字段生成的值。由于INSERT查询可能创建多条记录,而每条记录都可能有多个主键字段,因此这个方法返回嵌套列表,其中每个列表都包含一条记录的主键字段。在这里,我们仅为值添加了一条记录。由于数据表blog只有一个主键字段——id,因此可通过读取createdUsers[0][0]来获取生成的用户id。其中第一个索引指定了行(记录),而第二个索引指定了要读取这条记录的哪列(字段)。

12.4 Java Database Connectivity(JDBC) - 图7 与数据库连接对象一样,Sql对象也将占用宝贵而有限的数据库资源。在不再需要这种对象时,如果没有将其关闭,可能出现奇怪的问题。因此,在真实的应用程序中,总是使用try…catch块来确保即便引发了异常,这种对象也将得以妥善地关闭。

在方法main()中,在调用createDatabaseStructure()的代码后面添加调用addDemoRecords()的方法:

  1. ....
  2. def app = new Main()
  3. def connection = app.createDatabaseConnection()
  4. app.createDatabaseStructure(connection)
  5. app.addDemoRecords(connection)
  6. connection.close()
  7. ...

运行这个应用程序,它也将在不打印任何消息的情况下退出。下面来改变这种现状:打印博文的XML表示。