本文分析了在 Java 平台上可用的两个数据管理策略:Java 对象序列化和 Java 数据库连接(JDBC)。尽管本质上这两种数据管理策略并不存在孰优孰劣的问题,但在管理企业信息系统时,JDBC 轻而易举地得以胜出。在本文中,Java 开发人员 G.V.B. Subrahmanyam 和 Shankar Itchapurapu 对序列化和 JDBC都进行了介绍,并通过讨论和实例来向您展示了 JDBC 是您的最佳选择的原因。
当您正在建立企业信息系统时,需要确保以某种有效的方式存储、检索和显示企业数据。对于所有业务而言,数据都是独一无二的最大资产。所有软件系统都涉及数据,因此,数据的重要性是无论如何强调都不过分的。
应用程序的数据管理功能包括四个基本操作,通常也需要对企业数据执行这四个操作,它们是:建立、检索、更新 和 删除(即 CRUD)。管理在企业系统的数据涉及在很长一段时间范围之内,始终如一地、成功地执行 CRUD 操作,而不必频繁地更改实际执行这些操作的代码。换句话说,管理数据意味着开发稳健的、可扩展和可维护的软件系统,以确保成功地进行 CRUD 操作,在软件的生命期中能够以一致的方式执行操作。
本文讨论了 J2EE 中的两种可用数据管理策略:Java 对象序列化和 Java 数据库连接(JDBC)。我们将查看这两种方法的优缺点。这两种数据管理策略实质上不存在孰优孰劣。在特定实现中,策略的可用性取决于项目的范围(出现在系统环境中的活动的活动范围),系统的上下文(驱动系统/子系统运行时的值的集合),以及其他的外部因素。然而,Java 序列化并不适合于企业系统,其数据需要用一种定义良好的结构(如RDBMS)来组织。我们首先将快速浏览 Java 对象序列化,然后查看 JDBC 更重要的一些方面,从而了解后者是如何实现前者所缺乏的一些关键特性的。
本文并不打算对 Java 对象序列化或者 JDBC 进行全面介绍。有关这两项技术的更多信息,请回顾参考资料小节。
Java 对象序列化
对象序列化是最简单的 Java 持久性策略。对象序列化是一个将对象图平面化为一个字节的线性序列的过程。对象图是作为对象继承、关联和聚合的结果而实现的一些关系式。对象的非暂态实例属性以字节的形式被写入到持久存储中。实例属性的值就是执行时间序列化时内存中的值。如果一个 Java 对象是可序列化的,那么它至少必须实现 java.io.Serializable
接口,该接口具有如下所示的结构:
|
您可以看到,java.io.Serializable
接口并没有声明任何方法。它是一个记号或者标记接口。它告诉 Java 运行时环境,该实现类是可序列化的。列表 1 显示实现该接口的一个示例类。
|
无需自己实现 writeObject(...)
和 readObject(...)
方法来执行序列化;Java 运行时环境具有使这些方法可用的默认实现。然而,您可以重写这些方法,提供如何存储对象状态的您自己的实现。
关于序列化,您需要记住一些要点。首先,在序列化期间,整个对象图(即,所有父类和被引用类)都将被序列化。其次, Serializable
类的所有实例变量自身都应该是可序列化的,除非已经特别声明它们为暂态,或者已经重写 writeObject(...)
和 readObject(...)
来只序列化那些可序列化的实例变量。如果违反了后一规则,在运行时将出现一个异常。
每个后续 J2SE 版本都对对象序列化系统进行少量的增加。J2SE 1.4 也相应地向 ObjectOutputStream
and ObjectInputStream
增加 writeUnshared()
and readUnshared()
方法。通常,一个序列化的流只包含任何给定对象的一个序列化实例,并且共享对该对象引用的其他对象可以对它进行后向引用。通常期望序列化一个对象独立于其他对象可能维护的任何引用。非共享的读写方法允许对象作为新的、独一无二的对象被序列化,从而获得一个类似于对象克隆但开销更少的效果。
Java 对象序列化存在的问题
序列化涉及到将对象图从内存具体化到持久存储(例如硬盘)中。这涉及到大量 I/O 开销。通常,对应用程序而言,序列化并不是最佳选择:
对存储企业数据而言,序列化是一个错误选择,因为:
Java 数据库连接(JDBC)
Java 数据库连接(JDBC)是一个标准的 API,它使用 Java 编程语言与数据库进行交互。诸如 JDBC 的调用级接口是编程接口,它们允许从外部访问 SQL 命令来处理和更新数据库中的数据。通过提供与数据库连接的库例程,它们允许将 SQL 调用集成到通用的编程环境中。特别是,JDBC 有一个使接口变得极其简单和直观的例程的丰富收集。
在下面几个小节中,我们将查看通过 JDBC 与数据库连接所涉及的一些步骤。我们将特别关注与 Java 对象序列化相比,JDBC 是如何成为一个企业数据管理策略的。
建立一个数据库连接
在利用 JDBC 做任何其他事情之前,需要从驱动程序供应商那里获取数据库驱动程序,并且将该库添加到类路径中。一旦完这项工作,就可以在 Java 程序中使用类似于下面所示的代码来实现实际的连接。
|
Java 对象序列化并不需要这个该步骤,因为使用序列化来执行持久性操作并不需要 DBMS。 序列化是一个基于文件的机制;因此,在序列化一个对象之前,需要在目标文件系统中打开一个 I/O 流。
创建 JDBC Statement 和 PreparedStatement
可以用 JDBC Statement
对象将 SQL 语句发送到数据库管理系统(DBMS),并且不应该将该对象与 SQL 语句混淆。 JDBC Statement
对象是与打开连接有关联,而不是与任何单独的 SQL 语句有关联。可以将 JDBC Statement
对象看作是位于连接上的一个通道,将一个或多个(您请求执行的)SQL 语句传送给 DBMS。
为了创建 Statement
对象,您需要一个活动的连接。通过使用我们前面所创建的 Connection
对象 con
——下面的代码来完成这项工作。
|
到目前为止,我们已经有了一个 Statement
对象,但是还没有将对象传递到 DBMS 的 SQL 语句。
当数据库接收到语句时,数据库引擎首先会分析该语句并查找句法错误。一旦完成对语句的分析,数据库就必须计算出执行它的最有效方法。在计算上,这可能非常昂贵。数据库会检查哪些索引可以提供帮助,如果存在这样的索引的话,或者检查是否应该完全读取表中的所有行。数据库针对数据进行统计,找出最佳的执行方式。一旦创建好查询计划,数据库引擎就可以执行它。
生成这样一个计划会占用 CPU 资源。理想情况是,如果我们两次发送相同的语句到数据库,那么我们希望数据库重用第一个语句的访问计划,我们可以使用 PreparedStatement
对象来获得这种效果。
这里有一个主要的特性是,将 PreparedStatement
与其超类 Statement
区别开来:与 Statement
不同,在创建 PreparedStatement
时,会提供一个 SQL 语句。然后了立即将它发送给 DBMS,在那里编译该语句。因而, PreparedStatement
实际上是作为一 个通道与连接和被编译的 SQL 语句相关联的。
那么,它的优势是什么呢?如果需要多次使用相同的查询或者不同参数的类似查询,那么利用 PreparedStatement
,语句,只需被 DBMS 编译和优化一次即可。与使用正常的 Statement
相比,每次使用相同的 SQL 语句都需要重新编译一次。
还可以通过 Connection
方法创建PreparedStatement
。下面代码显示了如何创建一个带有三个输入参数的参数化了的 SQL 语句。
|
注意,Java 序列化不支持类似于 SQL 的查询语言。使用 Java 序列化访问对象属性的惟一途径就是反序列化该对象,并调用该对象上的 getter/accessor 方法。反序列化一个完整的对象在计算上可能很昂贵,尤其是在程序的生命期中,应用程序需要重复执行它。
在执行 PreparedStatement
之前,需要向参数提供值。通过调用 PreparedStatement
中定义的 setXXX()
方法可以实现它。最常使用的方法是 setInt()
,setFloat()
,setDouble()
,以及 setString()
。每次执行已准备的声明之前,都需要设置这些值。
执行语句和查询
执行 JDBC 中的 SQL 语句的方式是根据 SQL 语句的目的而变化的。DDL(数据定义语言)语句(例如表建立和表更改语句)和更新表内容的语句都是通过使用 executeUpdate()
执行的。列表 2 中包含 executeUpdate()
语句的实例。
|
我们将通过先前插入的参数值(如上所示)执行 PreparedStatement
,然后在这之上调用 executeUpdate()
,如下所示:
|
相比之下,查询期望返回一个行作为它的结果,并且并不改变数据库的状态。这里有一个称为 executeQuery()
的相对应的方法,它的返回值是 ResultSet
对象,如列表 3 所示。
|
由于查询而产生的行集包含在变量 rs
中,该变量是 ResultSet
的一个实例。集合对于我们来说并没有太大用处,除非我们可以访问每一个行以及每一个行中的属性。ResultSet
提供了一个光标,可以用它依次访问每一个行。光标最初被设置在正好位于第一行之前的位置。每个方法调用都会导致光标向下一行移动,如果该行存在,则返回 true
,或者如果没有剩余的行,则返回 false
。
我们可以使用适当类型的 getXXX()
来检索某一个行的属性。在前面的实例中,我们使用 getString()
和 getFloat()
方法来访问列值。注意,我们提供了其值被期望用作方法的参数的列的名称;我们可以指定用列号来代替列名。检索到的第一列的列号为 1,第二列为 2,依次类推。
在使用 PreparedStatement
时,可以通过先前插入的参数值来执行查询,然后对它调用 executeQuery()
,如下所示:
|
关于访问 ResultSet 的注释
JDBC 还提供一系列发现您在结果集中的位置的方法:getRow()
,isFirst()
,isBeforeFirst()
,isLast()
,以及 isAfterLast()
。
这里还有一些使可滚动光标能够自由访问结果集中的任意行的方法。在默认情况下,光标只向前滚动,并且是只读的。在为 Connection
创建 Statement
时,可以将 ResultSet
的类型更改为更为灵活的可滚动或可更新模型,如下所示:
|
不同的类型选项: TYPE_FORWARD_ONLY
、 TYPE_SCROLL_INSENSITIVE
和 TYPE_SCROLL_SENSITIVE
。可以通过使用 CONCUR_READ_ONLY
和 CONCUR_UPDATABLE
选项来选择光标是只读的还是可更新的。对于默认光标,可以使用 rs.next()
向前滚动它。对于可滚动的光标,您有更多的选项,如下所示:
|
对于可滚动光标的工作方式,这里有更多的详细描述。尽管可滚动光标对于特定应用程序是有用的,但是它导致极大的性能损失,所以应该限制和谨慎使用。可以在 参考资料小节中找到关于可滚动 ResultSet
的更多信息。
在序列化中不存在与 JDBC 的 ResultSet
相对应的机制。序列化和 JDBC 观察底层的数据的角度不同。JDBC (通常)假定底层数据是关系型结构的;而序列化假定底层数据是一个对象图。两种技术的底层数据结构存在显著差异。JDBC 的 Set
结构并不能自然地映射到序列化的对象图结构,反之亦然。当通过使用序列化语义将一个 Java 对象持久化时,数据的底层结构变成了一个字节流,该字节流展示了已经序列化了的核心对象的各种内部对象之间的关联。
JDBC 中的 ResultSet
导航是从一个 Set
元素移动到其他元素的过程,而在对象序列化中,这是不可能的,因为序列化涉及到对象关联,而不是将一组行封装到一个实体集合中。因此,Java 对象序列化无法向您提供用这种方式访问数据单独某个部分的能力。
事务
JDBC 允许将 SQL 语句组合到单独一个事务中。因此,我们可以通过使用 JDBC 事务特性来确保 ACID 属性。
Connection
对象执行事务控制。当建立连接时,在默认情况下,连接是自动提交模式下。这意味着每个 SQL 语句自身都被看作是一个事务,并且一完成执行就会被提交。
可以用以下方法开启或关闭自动提交模式:
|
一旦关闭了自动提交,除非通过调用 commit()
显式地告诉它提交语句,否则无法提交 SQL 语句(即,数据库将不会被持久地更新)。在提交之前的任何时间,我们都可以调用 rollback()
回滚事务,并恢复最近的提交值(在尝试更新之前)。
我们还可以设置期望的事务隔离等级。例如,我们可以将设置事务隔离等级为 TRANSACTION_READ_COMMITTED
,这使得在提交值之前,不允许对它进行访问。并且禁止脏读。在 Connection
接口中为隔离等级提供了五个这样的值。默认情况下,隔离等级是可序列化的。JDBC 允许我们发现数据库所设置的是什么事务隔离等级(使用 Connection
的 getTransactionIsolation()
方法)以及设置适当的等级(使用 Connection
的 setTransactionIsolation()
方法)。
回滚通常与 Java 语言的异常处理能力结合在一起使用。这种结合为处理数据完整性提供一个简单高效的机制。在下一节中,我们将研究如何使用 JDBC 进行错误处理。
注意,Java 对象序列化并不直接支持事务管理。如果您正在使用序列化,则将需要借助其他的 API,例如 JTA,来获得这个效果。然而,为了获得事务隔离的效果,可以选择在执行一个更新操作时同步该序列化对象,如下所示:
|
利用异常处理错误
软件程序中总是出现一些错误。通常,数据库程序是关键性应用程序,而且适当地捕获和处理错误是有必要的。程序应该恢复并且让数据库处于某种一致的状态下。将回滚与 Java 异常处理程序结合使用是达到这种要求的一种简便方法。
访问服务器(数据库)的客户(程序)需要能够识别从服务器返回的所有错误。JDBC 通过提供两种等级的错误条件来访问这种信息:SQLException
和 SQLWarning
。SQLException
是 Java 异常,它(如果未被处理)将会终止该应用程序。SQLWarning
是 SQLException
的子类,但是它们代表的是非致命错误或意想不到的条件,因此,可以忽略它们。
在 Java 代码中,希望抛出异常或者警告的语句包含于 try
块中。如果在 try
块中的语句抛出异常或者警告,那么可以在对应的某个 catch
语句中捕获它。每个捕获语句都指出了它准备捕获的异常。
换句话说,如果数据类型是正确的,但是数据库大小超出其空间限制并且不能建立一个新表,则可能会抛出一个异常。 可以从 Connection
,Statement
,以及 ResultSet
对象中获取 SQLWarning
。每个对象都只是存储最近 SQLWarning
。因此,如果通过 Statement
对象执行其他语句,则将放弃所有早期的警告。列表 4 举例说明了 SQLWarning
的使用。
|
实际上,SQLWarning
在某种程度上比 SQLException
更为罕见。最常见的是 DataTruncation
警告,它表示在从数据库读或写数据时存在问题。
Java 并没有提供序列化所使用的特定的异常类。使用序列化时发生的大多数异常都与执行的 I/O 操作有关,因此,在这些情况中 I/O 异常类将满足要求。
批处理
JDBC 2.0 提供一个用于批处理的强大API。批处理允许积累一组 SQL 语句,并且将它们一起发送并处理。一个典型的批处理就是银行应用程序,该应用程序每隔一刻钟就要更新许多账号。在减少从 Java 代码到数据库的往返次数方面,批处理是一个强大功能。
Statement
接口提供 addBatch(String)
方法,将 SQL 语句添加到一个批处理中。一旦已经将所有的 SQL 语句都增加到该批处理中,就可以使用 executeBatch()
方法一起执行它们。
然后,用executeBatch()
方法执行 SQL 语句,并返回 int
值的一个数组。该数组包含受每条语句影响的行数。将 SELECT
语句或者其他返回 ResultSet
的 SQL 语句放在一个批处理中会导致 SQLException
。
列表 5 中列出了利用 java.sql.Statement
进行批处理的一个简单实例。
|
在您不知道特定语句将运行的次数时,批处理是一个处理 SQL 代码的好方法。例如,如果在不使用批处理的情况下试图插入 100 条记录,那么性能可能会受到影响。如果编写一个脚本,增加 10000 条记录,那么情况会变得更糟。添加批处理可以帮助提高性能,后者甚至能够提高代码的可读性。
Java 对象序列化并不支持批处理。通常,会在某个对象的范围(联系图)上运用序列化,在这种情况下,批处理没有意义。因此,批处理在数据更新的定时和分组方面为您提供一定的灵活性,而这些对于序列化来说不一定是可用的。
从 Java 代码调用存储过程
存储过程是一组 SQL 语句,它们建立了一个逻辑单元,并执行特定任务。可以用存储过程来封装一个操作或者查询的集合,这些操作或查询都将在一个数据库服务器上执行。存储过程是在数据库服务器中被编译和存储的。因此,每次调用存储过程时,DBMS 都将重用已编译的二进制代码,因此执行速度会更快。
JDBC 允许您从 Java 应用程序中调用数据库存储过程。第一步是创建 CallableStatement
对象。与 Statement
和 PreparedStatement
对象一样,这项操作是用一个打开的 Connection
对象完成的。CallableStatement
对象包含对存储过程的调用;但它并不包含存储过程自身。列表 6 中的第一行代码使用 con
连接建立了对存储过程 SHOW_ACCOUNT
的调用。波形括号中括住的部分是存储过程的转义语法。当驱动程序遇到 {call SHOW_ACCOUNT}
时,它将该转义语法翻译成数据库所使用的本地 SQL,从而调用名为 SHOW_ACCOUNT
的存储过程。
|
假设 Sybase 中的存储过程 SHOW_ACCOUNT
包含列表 7 中所示的代码。
|
ResultSet rs
看起来类似于:
|
注意,用来执行 cs
的方法是 executeQuery()
,由于 cs
调用的存储过程只包含一个查询,所以只产生一个结果集。如果该过程只包含一个更新或者一个 DDL 语句,则将使用 executeUpdate()
方法。然而,有时候存在存储过程包含多个 SQL 语句的情况,在这种情况下,它将产生多个结果集、多个更新计数,或者结果集和更新计数的某种结合。因此,应该使用 execute()
方法执行 CallableStatement
。
CallableStatement
类是 PreparedStatement
的子类,因此 CallableStatement
对象可以接受与 PreparedStatement
对象相同的参数。而且,CallableStatement
对象可以接受输出参数,并将该参数用于输入和输出。INOUT
参数和 execute()
方法通常很少使用。要想处理 OUT
参数,需要通过使用 registerOutParameter(int, int)
方法将 OUT
参数注册到存储过程。
举例说明,我们假设 GET_ACCOUNT
过程包含列表 8 中的代码。
|
在这个实例中,参数 balance
被声明是一个 OUT
参数。现在,调用该过程的 JDBC 代码如列表 9 所示。
|
正使用 Java 序列化时,并不需要访问任何外部的系统,如 DBMS。换句话说,序列化是一个纯 Java 语言现象,它不涉及执行一个外部环境中的已编译代码。因此,在序列化中不存在与 CallableStatement
对象相对应的机制。这意味着您不能将数据处理转移到外部系统或者组件中,尽管这些系统或者组件可能更适合它。
包装
在读完本文之后,我们希望您赞同:对于数据管理和持久化而言, JDBC 是比 Java 对象序列化要好得多的方法。
JDBC 是一个用来访问数据存储的极好的 API。 JDBC 最好的东西是它提供单一的 API 集合来访问多种数据源。用户只需要学习一个 API 集合,就可以访问任何数据源,这些数据源可以是关系型的、层次型的或者任何其他格式。您需要的只是一个 JDBC 驱动程序,用它连接到目标数据源。JDBC 做了大量工作,将所有技术细节都封装到了一个实现软件包中,从而将程序员从供应商特定的桎梏中解放出来。
表 1 对比了 JDBC 和 Java 对象序列化的各种特性。
表 1. JDBC 对 Java 序列化
对象序列化 | JDBC | |
数据管理 | 使用文件系统存储序列化对象格式。这些系统中不包括特定的数据管理系统。序列化对象(存储在普通文件中的)通常是以自己的特殊方式通过底层 OS 来管理的。 | 使用一个 EAI/数据库来存储数据。EAI 或者数据库具有一个用来管理数据源中的数据指定的数据库管理系统(DBMS)。JDBC 是将请求发送到 DBMS 的 JVM 和 DBMS 之间的接口。JDBC 自身并不具有任何数据管理功能。 |
数据结构 | 底层的数据结构是一个对象图。序列化将 Java 对象的状态写入到文件系统。 | 底层的数据结构可以是关系型的、层次型的,或者是网络形状。但是数据的逻辑视图通常是一个表。 |
数据定义 | 数据定义涉及到使用序列化语义建立一个可序列化对象并持久化该对象。 | 数据定义涉及到在目标数据存储中建立必要的表,并且提供实体集之间的域级关系的明确定义。这一般是通过使用目标 DBMS 所提供的软件来完成的。 |
数据检索 | 数据检索涉及反序列化对象,并使用访问者方法读取对象的属性。 | DBMS 提供一个特殊的数据子语言来检索数据。通过 JDBC API 可以将以这种数据子语言编写的语句传递给目标数据源。DBMS 负责验证、执行和返回该语句的结果。 |
安全 | 没有可以使用的定义良好的安全机制。然而,底层的 OS 可以提供已序列化文件的安全。 | DBMS 提供一个广泛的安全特性集合。它可以完成认证和授权的工作。在可以访问或者操作 DBMS 上的数据之前,JDBC API 需要给目标 DBMS发送证书。 |
事务 | 没有可以使用的特定的事务控制机制。通过使用其他的 J2EE API,例如 JTA 或者 JTS,可以在程序上维护事务。 | DBMS 提供复杂的事务管理。JDBC API 提供有用的方法来提交和回滚事务。 |
并发控制 | 没有可以使用的特定的并发控制机制。不过,通过使用 Java 语言中的同步技术可以获得并发控制的效果。 | DBMS 提供多种等级的事务隔离。可以使用 JDBC API 方法来选择一个特定等级的隔离。 |
Java 对象序列化和 JDBC 是 Java 技术领域中许多数据持久化机制中的两种。在需要在多个 JVM 之间以 Java 语言特定格式共享数据(例如用 RMI 的按值传递机制共享数据)时,序列化最适合不过。然而,Java 序列化并不适用于企业数据,需要以一种定义良好的结构对这些数据进行组织。在这样的企业系统中,需要在多个系统和子系统之间共享数据,而这些系统并不一定都与 Java 语言兼容。在这种情况中,对象序列化根本不能工作。
JDBC 提供一个公用 API来访问异构的数据存储。它是 JVM 和目标 DBMS 之间的粘合剂。它提供了一个使用 Java 平台访问数据存储和维护企业数据的纲领性方法。然而,执行 CRUD 操作所需的所有代码都是由开发人员编写。
为了在企业环境中最有效地使用 JDBC,架构设计人员需要分析其企业中的数据,并开发一个用于数据持久性的框架。由于使用 JDBC 持久化数据的机制与系统想要解决的商业问题无关,因此强烈建议将数据持久性层与应用程序的商业逻辑相分离。设计模式对设计这种框架非常有帮助。