疯狂Java讲义(十三)----第五部分

 1.  Javar 的 RowSet

        RowSet接口继承了ResultSet接口,RowSet接口下包含JdbcRowSet.CachedRowSet、FilteredRowSet、JoinRowSet和 WebRowSet 常用子接口。除JdbcRowSet需要保持与数据库的连接之外,其余4个子接口都是离线的RowSet,无须保持与数据库的连接。
        与ResultSet相比,RowSet默认是可滚动、可更新、可序列化的结果集,而且作为JavaBean使用,因此能方便地在网络上传输,用于同步两端的数据。对于离线RowSet而言,程序在创建RowSet时已把数据从底层数据库读取到了内存,因此可以充分利用计算机的内存,从而降低数据库服务器的负载,提高程序性能。

 图13.22显示了RowSet规范的接口类图。

        在图13.22所示的各种接口中,CachedRowSet 及其子接口都代表了离线RowSet,它们都不需要底层数据库连接。

  (1) Java 7新增的RowSetFactory 与 RowSet

        Java 7新增了RowSetProvider类和 RowSetFactory接口,其中 RowSetProvider 负责创建RowSetFactory,而 RowSetFactory则提供了如下方法来创建RowSet实例。

  • CachedRowSet createCachedRowSet():创建一个默认的CachedRowSet。
  • FilteredRowSet createFilteredRowSet():创建一个默认的FilteredRowSet。
  • JdbcRowSet createJdbcRowSet():创建一个默认的JdbcRowSet。
  • JoinRowSet createJoinRowSet():创建一个默认的JoinRowSet。
  • WebRowSet createWebRowSet():创建一个默认的WebRowSet。

        通过使用RowSetFactory,就可以把应用程序与RowSet 实现类分离开,避免直接使用JdbcRow Setlmpl等非公开的API,也更有利于后期的升级、扩展。
        通过RowSetFactory的几个工厂方法不难看出,使用RowSetFactory创建的RowSet其实并没有装填数据。
        为了让RowSet能抓取到数据库的数据,需要为RowSet设置数据库的URL、用户名、密码等连接信息。因此,RowSet 接口中定义了如下常用方法。

  • setUrl(String url):设置该RowSet 要访问的数据库的URL。
  • setUsername(String name):设置该RowSet要访问的数据库的用户名。
  • setPassword(String password):设置该RowSet要访问的数据库的密码。
  • setCommand(String sql):设置使用该sql语句的查询结果来装填该RowSet。
  • execute():执行查询。

下面程序通过RowSetFactory示范了使用JdbcRowSet的可滚动、可修改特性。

        上面程序中的粗体字代码使用了 RowSetFactory来创建JdbcRowSet对象,这就避免了与JdbcRowSetImpl实现类耦合。由于通过这种方式创建的JdbcRowSet还没有传入Connection参数,因此程序还需调用setUrl()、setUsername()、setPassword()等方法来设置数据库连接信息。
        编译、运行该程序,一切正常。JdbcRowSet是一个可滚动、可修改的结果集,因此底层数据表中相应的记录也被修改了。

  (2) 离线RowSet

        在使用ResultSet的时代,程序查询得到ResultSet之后必须立即读取或处理它对应的记录,否则一旦Connection关闭,再去通过ResultSet读取记录就会引发异常。在这种模式下,JDBC编程十分痛苦——假设应用程序架构被分为两层:数据访问层和视图显示层,当应用程序在数据访问层查询得到ResultSet之后,对 ResultSet的处理有如下两种常见方式。

  • 使用迭代访问ResultSet里的记录,并将这些记录转换成Java Bean,再将多个Java Bean封装成一个List集合,也就是完成"ResultSet→Java Bean 集合”的转换。转换完成后可以关闭Connection等资源,然后将Java Bean集合传到视图显示层,视图显示层可以显示查询得到的数据。
  • 直接将ResultSet传到视图显示层-—这要求当视图显示层显示数据时,底层Connection必须一直处于打开状态,否则ResultSet无法读取记录。

        第一种方式比较安全,但编程十分烦琐;第二种方式则需要Connection一直处于打开状态,这不仅不安全,而且对程序性能也有较大的影响。
        通过使用离线RowSet可以十分“优雅”地处理上面的问题,离线RowSet 会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象则完全可以当成Java Bean来使用。因此不仅安全,而且编程十分简单.CachedRowSet是所有离线RowSet的父接口,因此下面以CachedRowSet为例进行介绍。看下面程序。

        上面程序中的①号粗体字代码调用了RowSet 的 populate(ResultSet rs)方法来包装给定的ResultSet,接下来的粗体字代码关闭了ResultSet.Statement.Connection等数据库资源。如果程序直接返回ResultSet,那么这个ResultSet无法使用——因为底层的Connection已经关闭;但程序返回的是CachedRowSet,它是一个离线RowSet,因此程序依然可以读取、修改RowSet中的记录。
        运行该程序,可以看到在 Connection关闭的情况下,程序依然可以读取、修改RowSet里的记录。为了将程序对离线RowSet所做的修改同步到底层数据库,程序在调用RowSet的 acceptChanges()方法时必须传入Connection。

  (3) 离线RowSet的查询分页

        由于CachedRowSet 会将数据记录直接装载到内存中,因此如果 SQL查询返回的记录过大,CachedRowSet将会占用大量的内存,在某些极端的情况下,它甚至会直接导致内存溢出。
        为了解决该问题,CachedRowSet提供了分页功能。所谓分页功能就是一次只装载ResultSet里的某几条记录,这样就可以避免CachedRowSet占用内存过大的问题。
        CachedRowSet提供了如下方法来控制分页。

  • populate(ResultSet rs, int startRow):使用给定的ResultSet装填RowSet,从ResultSet的第 startRow条记录开始装填。
  • setPageSize(int pageSize):设置CachedRowSet每次返回多少条记录。
  • previousPage():在底层ResultSet可用的情况下,让CachedRowSet读取上一页记录。
  • nextPage():在底层ResultSet可用的情况下,让CachedRowSet读取下一页记录。

下面程序示范了CachedRowSet的分页支持。

        上面两行粗体字代码就是使用CachedRowSet实现分页的关键代码。程序中①号代码显示要查询第2页的记录,每页显示3条记录。运行上面程序,可以看到程序只会显示从第4行到第6行的记录,这就实现了分页。

2.事务处理

        对于任何数据库应用而言,事务都是非常重要的,事务是保证底层数据完整的重要手段,没有事务支持的数据库应用,那将非常脆弱。
 

  (1) 事务的概念和 MySQL事务支持

        事务是由一步或几步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。程序和事务是两个不同的概念。一般而言,一段程序中可能包含多个事务。
        事务具备4个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性( Durability)。这4个特性也简称为ACID性。

  • 原子性(Atomicity):事务是应用中最小的执行单位,就如原子是自然界的最小颗粒,具有不可再分的特征一样,事务是应用中不可再分的最小逻辑执行体。
  • 一致性(Consistency):事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库,此时,数据库就处于一种不正确的状态。比如银行在两个账户之间转账:从A账户向B账户转入1000元,系统先减少A账户的1000元,然后再为B账户增加1000元。如果全部执行成功,数据库处于一致性状态;如果仅执行完A账户金额的修改,而没有增加B账户的金额,则数据库就处于不一致性状态;因此,一致性是通过原子性来保证的。
  • 隔离性(Isolation):各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务都是隔离的。也就是说,并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能互相影响。
  • 持续性(Durability):持续性也称为持久性(Persistence),指事务一旦提交,对数据所做的任何改变都要记录到永久存储器中,通常就是保存进物理数据库。

数据库的事务由下列语句组成。

  • 一组DML语句,经过这组DML语句修改后的数据将保持较好的一致性。
  • 一条DDL语句。
  • 一条DCL语句。

        DDL和 DCL语句最多只能有一条,因为DDL和DCL语句都会导致事务立即提交。
        当事务所包含的全部数据库操作都成功执行后,应该提交(commit)事务,使这些修改永久生效。事务提交有两种方式:显式提交和自动提交。

  • 显式提交:使用commit。
  • 自动提交:执行DDL或DCL语句,或者程序正常退出。

        当事务所包含的任意一个数据库操作执行失败后,应该回滚( rollback)事务,使该事务中所做的修改全部失效。事务回滚有两种方式:显式回滚和自动回滚。

  • 显式回滚:使用rollback。
  • 自动回滚:系统错误或者强行退出。

        MySQL默认关闭事务(即打开自动提交),在默认情况下,用户在MySQL控制台输入一条DML语句,这条DML语句将会立即保存到数据库里。为了开启MySQL 的事务支持,可以显式调用如下命令:

         一旦在 MySQL的命令行窗口中输入 set autocommit=0开启了事务,该命令行窗口里的所有DML语句都不会立即生效,上一个事务结束后第一条DML语句将开始一个新的事务,而后续执行的所有SQL语句都处于该事务中,除非显式使用commit 来提交事务,或者正常退出,或者运行DDL、DCL 语句导致事务隐式提交。当然,也可以使用rollback回滚来结束事务,使用rollback 结束事务将导致本次事务中 DML语句所做的修改全部失效。

        除此之外,如果不想关闭整个命令行窗口的自动提交,而只是想临时性地开始事务,则可以使用MySQL 提供的 start transaction或 begin两个命令,它们都表示临时性地开始一次事务,处于starttransaction或 begin后的 DML 语句不会立即生效,除非使用commit显式提交事务,或者执行 DDL、DCL语者来隐式提交事务。如下SOL代码将不会对数据库有任何影响。

        执行上面SQL语句中的第①条查询语句将会看到刚刚插入的3条记录,如果打开MySQL 的其他命令行窗口将看不到这3条记录——这正体现了事务的隔离性。接着程序rollback了事务中的全部修改,执行第②条查询语句时将看到数据库又恢复到事务开始前的状态。
        提交,不管是显式提交还是隐式提交,都会结束当前事务;回滚,不管是显式回滚还是隐式回滚,都会结束当前事务。
        除此之外,MySQL还提供了savepoint 来设置事务的中间点,通过使用savepoint 设置事务的中间点可以让事务回滚到指定中间点,而不是回滚全部事务。如下 SQL语句设置了一个中间点:

  (2)  JDBC的事务支持

        JDBC 连接也提供了事务支持,JDBC连接的事务支持由Connection提供,Connection默认打开自动提交,即关闭事务,在这种情况下,每条SQL语句一旦执行,便会立即提交到数据库,永久生效,无法对其进行回滚操作。
        可以调用Connection的 setAutoCommit()方法来关闭自动提交,开启事务,如下代码所示:

        程序中还可调用Connection提供的getAutoCommit()方法来返回该连接的自动提交模式。
        一旦事务开始之后,程序可以像平常一样创建Statement对象,创建了Statement对象之后,可以执行任意多条DML语句,如下代码所示:

        上面这些SOL语句虽然被执行了,但这些SQL语句所做的修改不会生效,因为事务还没有结束。如果所有的SQL语句都执行成功,程序可以调用Connection的commit()方法来提交事务,如下代码所示:

        如果任意一条SQL语句执行失败,则应该用Connection的 rollback()方法来回滚事务,如下代码所示:

        下面程序示范了当程序出现未处理的SQLException异常时,系统将自动回滚事务。

        上面程序中的粗体字代码只是开启事务、提交事务的代码,并没有回滚事务的代码。但当程序执行到第4条SQL语句(①处代码)时,这条语句将会引起外键约束异常,该异常没有得到处理,引起程序非正常结束,所以事务自动回滚。
        Connection也提供了设置中间点的方法: setSavepoint(),Connection提供了两个方法来设置中间点。

  • Savepoint setSavepoint():在当前事务中创建一个未命名的中间点,并返回代表该中间点的Savepoint对象。
  • Savepoint setSavepoint(String name):在当前事务中创建一个具有指定名称的中间点,并返回代表该中间点的Savepoint对象。

        通常来说,设置中间点时没有太大的必要指定名称,因为 Connection回滚到指定中间点时,并不是根据名字回滚的,而是根据中间点对象回滚的,Connection提供了rollback(Savepoint savepoint)方法回滚到指定中间点。

  (3) Java8增强的批量更新

        JDBC 还提供了一个批量更新的功能,使用批量更新时,多条SQL语句将被作为一批操作被同时收集,并同时提交。

        使用批量更新也需要先创建一个Statement对象,然后利用该对象的addBatch()方法将多条SQL语句同时收集起来,最后调用Java 8为Statement对象新增的executeLargeBatch()(或原有的executeBatch())方法同时执行这些SQL语句。只要批量操作中任何一条SQL语句影响的记录条数可能超过Integer.MAX_VALUE,就应该使用executeLargeBatch()方法,而不是executeBatch()方法。
        如下代码片段示范了如何执行批量更新。

        执行executeLargeBatch()方法将返回一个long[]数组,因为使用Statement执行DDL、DML 语句都将返回一个long值,而执行多条DDL、DML语句将会返回多个long值,多个long值就组成了这个long[]数组。如果在批量更新的addBatch()方法中添加了select查询语句,程序将直接出现错误。
        为了让批量操作可以正确地处理错误,必须把批量执行的操作视为单个事务,如果批量更新在执行过程中失败,则让事务回滚到批量操作开始之前的状态。为了达到这种效果,程序应该在开始批量操作之前先关闭自动提交,然后开始收集更新语句,当批量操作执行结束后,提交事务,并恢复之前的自动提交模式。如下代码示范了如何使用JDBC的批量更新。

3.分析数据库信息

        大部分时候,只需要对指定数据表进行插入(C)、查询(R)、修改(U)、删除(D)等CRUD操作;但在某些时候,程序需要动态地获取数据库的相关信息,例如数据库里的数据表信息、列信息。除此之外,如果希望在程序中动态地利用底层数据库所提供的特殊功能,则都需要动态分析数据库相关信息。

  (1) 使用DatabaseMetaData分析数据库信息

        JDBC提供了DatabaseMetaData来封装数据库连接对应数据库的信息,通过 Connection提供的getMetaData()方法就可以获取数据库对应的DatabaseMetaData对象。
        DatabaseMetaData接口通常由驱动程序供应商提供实现,其目的是让用户了解底层数据库的相关信息。使用该接口的目的是发现如何处理底层数据库,尤其是对于试图与多个数据库一起使用的应用程序—因为应用程序需要在多个数据库之间切换,所以必须利用该接口来找出底层数据库的功能,例如,调用supportsCorrelatedSubqueries()方法查看是否可以使用关联子查询,或者调用supportsBatchUpdates()方法查看是否可以使用批量更新。
        许多DatabaseMetaData方法以 ResultSet对象的形式返回查询信息,然后使用ResultSet 的常规方法(如 getString()和 getInt())即可从这些ResultSet对象中获取数据。如果查询的信息不可用,则将返回一个空ResultSet对象。
        DatabaseMetaData的很多方法都需要传入一个xxxPattern 模式字符串,这里的xxxPattern不是正则表达式,而是SQL里的模式字符串,即用百分号(%)代表任意多个字符,使用下画线(_)代表一个字符。在通常情况下,如果把该模式字符串的参数值设置为null,即表明该参数不作为过滤条件。
        下面程序通过DatabaseMetaData分析了当前Connection 连接对应数据库的一些基本信息,包括当前数据库包含多少数据表,存储过程,student_table表的数据列、主键、外键等信息。

        上面程序中的粗体字代码就是使用DatabaseMetaData分析数据库信息的示例代码。运行上面程序,将可以看到通过DatabaseMetaData分析数据库信息的结果。

  (2) 使用系统表分析数据库信息

        除可以使用DatabaseMetaData来分析底层数据库信息之外,如果已经确定应用程序所使用的数据库系统,则可以通过数据库的系统表来分析数据库信息。前面已经提到,系统表又称为数据字典,数据字典的数据通常由数据库系统负责维护,用户通常只能查询数据字典,而不能修改数据字典的内容。

        MySQL 数据库使用information_schema数据库来保存系统表,在该数据库里包含了大量系统表,常用系统表的简单介绍如下。

  • tables:存放数据库里所有数据表的信息。
  • schemata:存放数据库里所有数据库(与MySQL 的Schema对应)的信息。
  • views:存放数据库里所有视图的信息。
  • columns:存放数据库里所有列的信息。
  • triggers:存放数据库里所有触发器的信息。
  • routines:存放数据库里所有存储过程和函数的信息。
  • key_column_usage:存放数据库里所有具有约束的键信息。
  • table_constraints:存放数据库里全部约束的表信息。
  • statistics:存放数据库里全部索引的信息。

        从这些系统表中取得的数据库信息会更加准确,例如,若要查询当前MySQL数据库中包含多少数据库及其详细信息,则可以查询 schemata 系统表;如果需要查询指定数据库中的全部数据表,则可以查询tables系统表;如果需要查询指定数据表的全部数据列,就可以查询columns系统表。图13.23显示了通过系统表查询所有的数据库、select_test 数据库的全部数据表、student_table表的所有数据列的sQL语句及执行效果。

  (3) 选择合适的分析方式

        本章后面的练习需要完成一个仿SQLyog应用程序,这个应用程序需要根据数据库、表、列等信息创建一棵树,这就需要利用DatabaseMetaData来分析数据库信息,或者利用MySQL系统表来分析数据库信息。
        通常而言,如果使用DatabaseMetaData来分析数据库信息,则具有更好的跨数据库特性,应用程序可以做到数据库无关;但可能无法准确获得数据库的更多细节。
        使用数据库系统表来分析数据库系统信息会更加准确,但使用系统表也有坏处——这种方式与底层数据库耦合严重,采用这种方式将会导致程序只能运行于特定的数据库之上。
        通常来说,如果需要获得数据库信息,包括该数据库驱动提供了哪些功能,则应该利用 DatabaseMetaData来了解该数据库支持哪些功能。完全可能出现这样一种情况:对于底层数据库支持的功能,但数据库驱动没有提供该功能,程序还是不能使用该功能。使用DatabaseMetaData则不会出现这种问题。
        如果需要纯粹地分析数据库的静态对象,例如分析数据库系统里包含多少数据库、数据表、视图、索引等信息,则利用系统表会更加合适。

4.使用连接池管理连接

        数据库连接的建立及关闭是极耗费系统资源的操作,在多层结构的应用环境中,这种资源的耗费对系统性能影响尤为明显。通过前面介绍的方式(通过DriverManager获取连接)获得的数据库连接,一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完后立即关闭连接。频繁地打开、关闭连接将造成系统性能低下。
        数据库连接池的解决方案是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池。每次应用程序请求数据库连接时,无须重新打开连接,而是从连接池中取出已有的连接使用,使用完后不再关闭数据库连接,而是直接将连接归还给连接池。通过使用连接池,将大大提高程序的运行效率。

        对于共享资源的情况,有一个通用的设计模式:资源池(Resource Pool),用于解决资源的频繁请求、释放所造成的性能下降。为了解决数据库连接的频繁请求、释放,JDBC 2.0规范引入了数据库连接池技术。数据库连接池是Connection对象的工厂。数据库连接池的常用参数如下。

  • 数据库的初始连接数。
  • 连接池的最大连接数。
  • 连接池的最小连接数。
  • 连接池每次增加的容量。

        JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由商用服务器(如WebLogic、WebSphere)等提供实现,也有一些开源组织提供实现(如DBCP和C3PO等)。

        本节不打算介绍任何商用服务器的数据源实现,主要介绍 DBCP和 C3PO两种开源的数据源实现。

  (1) DBCP数据源

        DBCP是 Apache 软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统:common-pool。如果需要使用该连接池实现,则应在系统中增加如下两个jar文件。

  • commons-dbcp.jar:连接池的实现。
  • commons-pool.jar:连接池实现的依赖库。

        登录http:llcommons.apache.org/站点即可下载commons-pool.zip和commons-dbcp.zip两个压缩文件,解压缩这两个文件即可得到上面提到的两个JAR文件。为了在程序中使用这两个JAR文件,应该把它们添加到系统的类加载路径中(比如添加到CLASSPATH环境变量中)。
        Tomcat的连接池正是采用该连接池实现的。数据库连接池既可以与应用服务器整合使用,也可以由应用程序独立使用。下面的代码片段示范了使用DBCP来获得数据库连接的方式。

        数据源和数据库连接不同,数据源无须创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。也就是说,对于一个应用,上面代码只要执行一次即可。建议把上面程序中的 ds设置成static成员变量,并且在应用开始时立即初始化数据源对象,程序中所有需要获取数据库连接的地方直接访问该ds对象,并获取数据库连接即可。通过 DataSource获取数据库连接的代码示例如下:

        但上面代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给连接池,让其他客户端可以使用该连接。

  (2) C3PO数据源

        相比之下,C3P0 数据源性能更胜一筹,Hibernate 就推荐使用该连接池。C3PO连接池不仅可以自动清理不再使用的Connection,还可以自动清理Statement和 ResultSet。C3PO连接池需要版本为1.3以上的JRE,推荐使用1.4以上的JRE。如果需要使用C3PO连接池,则应在系统中增加如下JAR文件。

  • c3p0-0.9.1.2.jar:C3PO连接池的实现。

        登录http:lsourceforge.net/projects/c3pO/站点即可下载C3PO 数据源的最新版本,下载后得到一个c3p0-0.9.1.2.bin.zip文件(版本号可能有区别),解压缩该文件,即可得到上面提到的JAR文件。
        下面代码通过C3PO连接池获得数据库连接。

        在程序中创建C3PO连接池的方法与前面介绍的创建DBCP连接池的方法基本类似,此处不再解释。一旦获取了C3PO连接池之后,程序同样可以通过如下代码来获取数据库连接。

5.本章小结

        本章从标准的SQL语句讲起,简单介绍了关系数据库的基本理论及标准的SQL 语句的相关语法,包括DDL、DML、简单查询语句、多表连接查询和子查询语句。本章重点讲解了JDBC 数据库访问的详细步骤,包括加载数据库驱动,获取数据库连接,执行SQL语句,处理执行结果等。
        本章在介绍JDBC数据库访问时详细讲解了Statement、PreparedStatement、CallableStatement的区别和联系,并介绍了如何处理数据表的Blob列。本章还介绍了事务相关知识,包括如何在标准的SQL语句中进行事务控制和在JDBC编程中进行事务控制。本章最后介绍了如何利用DatabaseMetaData、系统表来分析数据库信息,并讲解了数据源的原理和作用,示范了两个开源数据源实现的用法。

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121275213