Java核心

JDBC入门教程 – 终极指南

本教程主要介绍JDBC(Java数据库连接)—— 一个Oracle提供的API,它可以帮助程序员处理Java应用程序中不同数据库的访问问题:程序员可以通过它建立与数据库的连接;定义特定的客户端使之可以访问给定的数据库;提供一种能够读取、插入、更新和删除数据库中的数据项的机制;以及控制由不同SQL语句组成的事务。

在本文中我们会介绍主要的JDBC组件,包括声明(Statement)、结果集(Result Set)、存储过程(Stored Procedure)。

程序员们需要针对不同的数据库提供相应的驱动程序;在后面的章节我们会结合一些实例来详细解释。

JDBC是伴随Java的产生而产生的;它的第一个版本产生于1997年2月,也就是JDK1.1的发布日期;自那以后,JDBC已经成为Java的一个重要组成部分。JDBC相关的主要包有: http://docs.oracle.com/javase/8/docs/api/java/sql/package-summary.html http://docs.oracle.com/javase/8/docs/api/javax/sql/package-summary.html.

关于JDBC的最新版本和开发、维护信息可以访问JSR 221获得。

本文中的所有例子的实现环境是Java 8 update 0_25和Eclipse SDK  Luna 4.4. 你可以在本文的最后下载所有这些例子,而且还可以下载其他更多的例子!

1. 组件(Components)

JDBC API使得程序员和Java应用程序可以与数据库进行交互。它能够在各种数据源环境下:执行不同的SQL语句、处理返回的结果集。

在这一节中,我们会综述并列出一些最重要的JDBC组件,他们都是Java应用程序的一部分;我们会在接下来的章节详细介绍它们。

  • 首先,Java应用需要创建并建立与特定数据库的连接。这可以通过Driven Manager完成,比如,接口java.sql.DriveManager的实例;或者直接通过JDBC数据源进行连接。接口javax.sq.DataSource可以用来建立JDBC数据源连接。后面的章节会详细介绍这些组件。
  • 一旦连接到数据库,我们就可以使用java.sql.Connection来执行CRUD (创建-create, 读取-read, 更新-update, 删除delete) SQL 语句或操作。后面的章节会详细介绍这些语句。
  • 为了执行这些操作,程序员可以使用基于java.sql.Statement and java.sql.PreparedStatement 的类。需要多次执行相同的语句,后者会更高效一些,另外它也提供了一些我们将会在本教程后面章节提到的优点。接口JDBC连接提供了很多机制来创建statement实例:
 
PreparedStatement countriesStatement = connection.prepareStatement("UPDATE COUNTRIES SET NAME = ? WHERE ID = ?");
countriesStatement.setString(1, "Spain");
countriesStatement.setInt(2, 123456789);

 

  • 插入、更新和删除操作会返回所改变的记录行数:
 
// countriesStatement belongs to the class Statement, returning number of updated rows
int n = countriesStatement.executeUpdate();
  • 选择操作(查询)会通过java.sql.ResultSet返回很多数据行;遍历这些记录时可以通过列名或行号;查询结果的元数据也可以获取得到:
 
// countriesStatement belongs to the class Statement
ResultSet rs = countriesStatement.executeQuery("SELECT NAME, POPULATION FROM COUNTRIES");
//rs contains the results in rows plus some metadata
...
  • 通常情况下,JDBC使用连接池来管理数据库连接。连接池有很多不同的实现方式,比如C3P0或DBCP。数据库连接池是一组JDBC连接,当应用程序请求连接时,它会分配一个空闲连接;当应用中有任务终结时,它会释放相应的连接。有很多文档介绍了如果使用和配置JDBC连接池,这个有个不错的教程http://docs.oracle.com/cd/E13222_01/wls/docs81/ConsoleHelp/jdbc_connection_pools.html 
  •  JDBC还有一些其它功能:存储过程(Stored Procedure)、可调用声明(Callable Statements), 批处理(Batch Processing)… 本教程会一一讲解这些概念。

2. 数据库连接(Connections)

我们通过java.sql.Connection 对象来连接到数据库,具体方法是调用java.sql.DriverManager类的getConnection()方法。这个方法的参数包括数据库主机名和登录信息。

如下代码片段展示了如何创建本地MySQL数据库的连接。

//MySQL driver is loaded
Class.forName( "com.mysql.jdbc.Driver" );
//Connection object is created using the db host and credentials
Connection connect = DriverManager.getConnection("jdbc:mysql://localhost/countries?"
            + "user=root&password=root" );

数据库连接对象使得程序员可以做如下操作:

  • 创建JDBC Statement:使用已建立的连接,可以创建Statement, PreparedStatement 或CallableStatement 实例,然后通过这些实例提供的各种方法来执行不同的SQL语句。举个例子说明如何创建PreparedStatement:
//the connection conn is used to create a prepared statement with the given sql operation
PreparedStatement updateStmt = conn.prepareStatement( sql );

这个Statement可以执行通过参数传入的更新SQL语句。

  • 可以提交或者回退指定的事务。JDBC连接支持两种工作方式:autocommit=true 和 autocommit=false. 第一种方式直接提交所有事务至数据库;第二种方式需要执行一条特殊命令来提交或者回退这些事务。我们会在后面的相关章节中更详细的讲解这个特性。下面以一小段代码为例来说明如何更改JDBC连接的自动提交模式:
//it changes the mode to auto commit=false
connect.setAutoCommit( false );
  • 提供了功能可以获得所使用的数据库的元信息。
  • 其它一些功能例如批处理(batch processing)、存储过程(stored procedure)等等。

我们后面会详细解释这些功能,这里我们先初步的了解什么是JDBC连接以及它可以用来完成什么功能。

3. 数据类型(Data types)

在使用Java程序中的数据之前,JDBC会将Java数据类型转换成相对应的JDBC数据类型。它们之间有一个默认的对应关系,能够保证在不同的数据库实现和驱动之间的一致性。

下表列出了它们的对应关系:

SQLJDBC/Javasettergetter
VARCHARjava.lang.StringsetStringgetString
CHARjava.lang.StringsetStringgetString
LONGVARCHARjava.lang.StringsetStringgetString
BITbooleansetBooleangetBoolean
NUMERICBigDecimalsetBigDecimalgetBigDecimal
TINYINTbytesetBytegetByte
SMALLINTshortsetShortgetShort
INTEGERintsetIntgetInt
BIGINTlongsetLonggetLong
REALfloatsetFloatgetFloat
FLOATfloatsetFloatgetFloat
DOUBLEdoublesetDoublegetDouble
VARBINARYbyte[ ]setBytesgetBytes
BINARYbyte[ ]setBytesgetBytes
DATEjava.sql.DatesetDategetDate
TIMEjava.sql.TimesetTimegetTime
TIMESTAMPjava.sql.TimestampsetTimestampgetTimestamp
CLOBjava.sql.ClobsetClobgetClob
BLOBjava.sql.BlobsetBlobgetBlob
ARRAYjava.sql.ArraysetARRAYgetARRAY
REFjava.sql.RefSetRefgetRef
STRUCTjava.sql.StructSetStructgetStruct

 
SQL和Java对于空值(null)有不同的处理方式。在使用SQL处理一些Java中的空值时,我们最好能够遵循一些最佳实践,比如避免使用原始类型(primitive type),因为它们默认值不能为空而是会依据类型设定默认值,比如int型默认值为0、布尔型(boolean)默认值为false,等等。

与此相反,我们建议使用这些原始类型的包装类。类似这样的情况,类ResultSet的wasNull()方法就会非常有用。

举例来看看它的用法:

Statement stmt = conn.createStatement( );
String sql = "SELECT NAME, POPULATION FROM COUNTRIES";
ResultSet rs = stmt.executeQuery(sql);

int id = rs.getInt(1);
if( rs.wasNull( ) ) {
   id = 0;
}

4. 驱动程序(Drivers)

JDBC驱动管理器(java.sql.DriverManager)是JDBC API中最重要的元素之一。 它是用来处理一系列JDBC驱动程序的基本服务。它含有很多机制和对象,能够将Java应用程序连接到所需的JDBC驱动程序。它负责管理这些不同类型的JDBC数据库驱动程序。总结一下驱动管理器的主要功能就是:获取当前可用的驱动列表;处理特定的的驱动程序和数据库之间的连接。

举例来说明它的用法:

// Create the connection with the default credentials
java.sql.Connection conn = DriverManager.getConnection("jdbc:hsqldb:mem:mydb", "SA", "" );

我们通过方法DriverManager.registerDriver()来注册驱动程序:

new org.hsqldb.jdbc.JDBCDriver();
DriverManager.registerDriver( new org.hsqldb.jdbc.JDBCDriver() );

我们也可以调用方法Class.forName() 加载驱动程序:

// Loading the HSQLDB JDBC driver
Class.forName( "org.hsqldb.jdbc.JDBCDriver" );

...

// connection to JDBC using mysql driver
Class.forName( "com.mysql.jdbc.Driver" );

这二者的主要区别是:前者方法registerDerver()需要保证驱动程序在编译时就是可用的;后者加载驱动程序类文件的方式,不需要驱动程序在编译时是可用的。JDBC 4版本后,实际上没有必要调用这些方法,应用程序不需要单独注册这些驱动,也不需要加载驱动类。我们也不推荐使用方法registerDriver()来手动加载驱动程序。

其它很有趣的方法有DriverManager 类的getDriver(String url)和getDrivers() 方法:前者可以通过给定的字符串去定位相应的驱动程序;后者可以返回一个包含了已在驱动管理器中注册的所有驱动程序的集合:

Enumeration drivers = DriverManager.getDrivers();
while( drivers.hasMoreElements() )
{
	Driver driver = drivers.nextElement();
	System.out.println( driver.getClass() );
}

5. 数据库(Databases)

JDBC能够支持相当多的数据库。它高度抽象出数据库的共性和工作方式,使得可以通过不同的驱动程序来支持它们。类DriverManager负责加载合适的数据库驱动,加载完毕后,那些用来读取和修改数据的代码基本是跨数据库通用的(或许会有一点点差异)。

这里有个链接可以查看JDBC支持的数据库列表(已在Oracle官方注册的): http://www.oracle.com/technetwork/java/index-136695.html

本章中,我们会展示如何使用不同的数据库:MySQL和HSQLDB。第一个MySQL广为程序员熟知,而且广泛使用;第二个HSQLDB,在测试时非常有用,因为它具有内存数据库的属性。我们会展示如何使用它们,而且我们将会发现,在这个应用程序中,除了需要加载不同的JDBC驱动,其它部分是完全相同的:

MySQL 示例:

public static void main( String[] args ) throws ClassNotFoundException, SQLException
    {

        // connection to JDBC using mysql driver
        Class.forName( "com.mysql.jdbc.Driver" );
        Connection connect = DriverManager.getConnection("jdbc:mysql://localhost/countries?"
            + "user=root&password=root" );

       
        selectAll( connect );

        // close resources, in case of exception resources are not properly cleared
...

    }
    
    /**
     * select statement and print out results in a JDBC result set
     * 
     * @param conn
     * @throws SQLException
     */
    private static void selectAll( java.sql.Connection conn ) throws SQLException
    {
        Statement statement = conn.createStatement();

        ResultSet resultSet = statement.executeQuery( "select * from COUNTRIES" );

        while( resultSet.next() )
        {
            String name = resultSet.getString( "NAME" );
            String population = resultSet.getString( "POPULATION" );

            System.out.println( "NAME: " + name );
            System.out.println( "POPULATION: " + population );
        }

    }

内存数据库 (HSQLDB) 示例:

public static void main( String[] args ) throws ClassNotFoundException, SQLException
    {

        // Loading the HSQLDB JDBC driver
        Class.forName( "org.hsqldb.jdbc.JDBCDriver" );

        // Create the connection with the default credentials
        java.sql.Connection conn = DriverManager.getConnection( "jdbc:hsqldb:mem:mydb", "SA", "" );

        // Create a table in memory
        String countriesTableSQL = "create memory table COUNTRIES (NAME varchar(256) not null primary key, POPULATION varchar(256) not null);";

        // execute the statement using JDBC normal Statements
        Statement st = conn.createStatement();
        st.execute( countriesTableSQL );

        // nothing is in the database because it is just in memory, non persistent
        selectAll( conn );

        // after some insertions, the select shows something different, in the next execution these
        // entries will not be there
        insertRows( conn );
        selectAll( conn );

    }

...

    /**
     * select statement and print out results in a JDBC result set
     * 
     * @param conn
     * @throws SQLException
     */
    private static void selectAll( java.sql.Connection conn ) throws SQLException
    {
        Statement statement = conn.createStatement();

        ResultSet resultSet = statement.executeQuery( "select * from COUNTRIES" );

        while( resultSet.next() )
        {
            String name = resultSet.getString( "NAME" );
            String population = resultSet.getString( "POPULATION" );

            System.out.println( "NAME: " + name );
            System.out.println( "POPULATION: " + population );
        }

    }

我们可以看到,在上面的两个程序中,selectAll 方法的代码是完全相同的,只有JDBC驱动程序加载和创建连接的部分不一样;你可以想象,当我们在不同的工作环境中,这有多么强大。HSQLDB版本的代码,另外也包含了一段代码用来创建内存数据库、插入一些数据行,但这只是为了展示和澄清的目的,而且也可以通过其它方式实现。

6. 结果集(Result Sets)

类java.sql.resultset代表数据库中表数据的结果集。通常它是通过执行SQL查询(使用Statement或者PreparedStatement的查询语句)来生成。它包含了存储数据的那些数据行。这些数据可以通过索引(从1开始)或者属性名来访问:

        // creating the result set
        ResultSet resultSet = statement.executeQuery( "select * from COUNTRIES" );

        // iterating through the results rows

        while( resultSet.next() )
        {
            // accessing column values by index or name
            String name = resultSet.getString( "NAME" );
            int population = resultSet.getInt( "POPULATION" );

            System.out.println( "NAME: " + name );
            System.out.println( "POPULATION: " + population );


            // accessing column values by index or name
            String name = resultSet.getString( 1 );
            int population = resultSet.getInt( 2 );

            System.out.println( "NAME: " + name );
            System.out.println( "POPULATION: " + population );


        }

正如代码中展示的,ResultSet包含不同的getter方法来检索不同Java类型的列值。它还包含一个指向当前行数据的游标。初始时,游标指向第一行,next()方法会移动游标至下一行:java.sql.ResultSet.next().

我们也可以创建某些具有默认值的ResultSet,比如建立一个只能前移、不可做更新操作的游标。如果程序员需要设定其它类型的属性,可以在创建Statement时指定,然后在Statement创建ResultSet时它就会传入这些属性值:

/**
* indicating result sets properties that will be created from this statement: type,
* concunrrency and holdability
*/
Statement statement = conn.createStatement( ResultSet. TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE, ResultSet.CLOSE_CURSORS_AT_COMMIT );

经过如上代码设置后获得的ResultSet结果集,可以双向移动游标,也可以更新或插入新的数据到数据库中。

7. 存储过程(Stored procedures)

在这一章我们将解释什么是存储过程以及如何在JDBC中使用它们。在后面的示例中我们会使用基于MySql的存储过程。

存储过程是一系列SQL语句的集合,它们共同组成一个逻辑单元,用来执行一些特定的任务。在我们需要封装一组操作来执行某些数据库操作时,它会非常有用。

首先我们在MySQL数据库中创建一个存储过程,下面的脚本将帮助我们完成本例中的任务:

delimiter //
 
CREATE PROCEDURE spanish (OUT population_out INT)
 BEGIN
 SELECT COUNT(*) INTO population_out FROM countries;
 END//
 
 
 delimiter ;
 
 CALL simpleproc(@a);
 

简单来说,上面的脚本创建了一个名为Spanish的存储过程,带有一个类型为int的输出参数,没有输入参数。这个存储过程会返回数据库中所有国家的总数。

创建完毕这个存储过程,我们可以开始在Java应用中使用它了。调用存储过程时,我们需要使用一个特殊的基于java.sql.CallableStatement接口的声明(Statement),这些声明可以显式的指明输出属性和输入参数然后执行存储过程。在如上我们这个简单的示例中,只配置了输出属性。来看代码示例:

        CallableStatement callableStatement = null;

        // the procedure should be created in the database
        String spanishProcedure = "{call spanish(?)}";

        // callable statement is used
        callableStatement = connect.prepareCall( spanishProcedure );

        // out parameters, also in parameters are possible, not in this case
        callableStatement.registerOutParameter( 1, java.sql.Types.VARCHAR );

        // execute using the callable statement method executeUpdate
        callableStatement.executeUpdate();

        // attributes are retrieved by index
        String total = callableStatement.getString( 1 );

        System.out.println( "amount of spanish countries " + total );

通过方法java.sql.PreparedStatement.executeUpdate(),我们可以体会到在应该如何保存存储过程的输出以及如何执行它。绝大部分的数据库都能支持存储过程,不过它们的语法和行为可能会有差异,这也就是为什么在Java应用程序中处理存储过程的代码会有不同,这些不同主要用来根据不同的数据库做出调整。

8. 声明(Statements)

前面章节已经提到过,JDBC使用接口java.sql.Statement执行不同的SQL查询和插入、更新或删除操作。它是一个最基本的接口,提供了一些执行SQL语句的基本方法,如java.sql.Statement.executeQuery(String)或 java.sql.Statement.executeUpdate(String).

这个接口的实现类的建议使用场景是:程序员不需要多次执行相同的查询操作;或者查询和声明不需要参数化。一般来说,这个接口适用于执行DDL语句(创建(create),更改(alter),删除(drop))。通常这些语句不需要多次执行,也不需要支持不同的参数。

当程序员需要更好的效率去执行重复性的SQL查询或者需要参数化时,他们应该使用java.sql.PreparedStatement。这个接口继承自基本接口-Statement,能够提供参数化功能。也正由于这个特性,这个接口在面对SQL注入攻击时更加安全。下面来看一段代码演示它的用法:

System.out.println( "Updating rows for " + name + "..." );

String sql = "UPDATE COUNTRIES SET POPULATION=? WHERE NAME=?";

PreparedStatement updateStmt = conn.prepareStatement( sql );

// Bind values into the parameters.
updateStmt.setInt( 1, 10000000 ); // population
updateStmt.setString( 2, name ); // name

// update prepared statement using executeUpdate
int numberRows = updateStmt.executeUpdate();

System.out.println( numberRows + " rows updated..." );

使用Prepared Statements的另外一个好处就是可以使用方法setObject()处理一些非标准对象。示例如下:

        PreparedStatement updateStmt2 = conn.prepareStatement( sql );

        // Bind values into the parameters using setObject, can be used for any kind and type of
        // parameter.
        updateStmt2.setObject( 1, 10000000 ); // population
        updateStmt2.setObject( 2, name ); // name

        // update prepared statement using executeUpdate
        numberRows = updateStmt2.executeUpdate();

        System.out.println( numberRows + " rows updated..." );
        updateStmt2.close();

我们在前面介绍存储过程的章节曾经提到过,还有一个可以可用于相同目的的接口-继承自PrerparedStatement的java.sql.CallableStatement。

9. 批处理命令(Batch commands)

JDBC提供了以批处理方式执行很多SQL语句的功能。根据程序员使用的statement的类型,代码可能会有些微不同,不过总体概念是一样的。我们在下一个代码片段中展示如何使用java.sql.Statement完成批处理:

Statement statement = null;

statement = connect.createStatement();

// adding batchs to the statement
statement.addBatch( "update COUNTRIES set POPULATION=9000000 where NAME='USA'" );
statement.addBatch( "update COUNTRIES set POPULATION=9000000 where NAME='GERMANY'" );
statement.addBatch( "update COUNTRIES set POPULATION=9000000 where NAME='ARGENTINA'" );

// usage of the executeBatch method
int[] recordsUpdated = statement.executeBatch();

int total = 0;
for( int recordUpdated : recordsUpdated )
{
	total += recordUpdated;
}

System.out.println( "total records updated by batch " + total );

使用java.sql.PreparedStatement的示例如下:

       String sql = "update COUNTRIES set POPULATION=? where NAME=?";

        PreparedStatement preparedStatement = null;

        preparedStatement = connect.prepareStatement( sql );

        preparedStatement.setObject( 1, 1000000 );
        preparedStatement.setObject( 2, "SPAIN" );

        // adding batches
        preparedStatement.addBatch();

        preparedStatement.setObject( 1, 1000000 );
        preparedStatement.setObject( 2, "USA" );

        // adding batches
        preparedStatement.addBatch();

        // executing all batchs
        int[] updatedRecords = preparedStatement.executeBatch();
        int total = 0;
        for( int recordUpdated : updatedRecords )
        {
            total += recordUpdated;
        }

        System.out.println( "total records updated by batch " + total );

我们可以看到,不同之处基本上是SQL查询参数的使用方式和这些查询的创建方式,但是在一个批次中执行多个statement语句的理念是完全一样的。第一段代码示例调用了方法java.sql.Statement.executeBatch(),第二段代码示例调用了方法java.sql.PreparedStatement.addBatch()。

10. 事务(Transactions)

JDBC支持事务处理,它包含了很多方法和功能来实现基于事务的应用程序。在本章我们会列举其中最重要的几个部分。

  • java.sql.Connection.setAutoCommit(boolean): 这个方法接受一个boolean型的变量参数。如果其值为true(默认值就是true),则对于数据库的所有SQL操作都会立即自动提交进行持久化;如果其值为false,所有更改不会自动提交并持久化,而是通过调用方法java.sql.Connection.commmit()。
  • java.sql.Connection.commit(). 这个方法只在自动提交被设置为false或禁用时才有用;也就是说,它只能够工作在‘非自动提交’模式下。执行这个方法后,所有自上一次提交/回滚以来的更改将会提交并持久化至数据库。
  • java.sql.Connection.rollback(). 这个方法只在自动提交禁用时可用。它可以撤消或回滚所有当前事务中的更改。

下面来看一个例子,展示如何使用方法setAutoCommit(false)禁用自动提交模式。调用commit()方法时所有更改会提交,而调用rollback()方法则所有当前事务内的更改会回滚:

Class.forName( "com.mysql.jdbc.Driver" );
Connection connect = null;
try
{
    // connection to JDBC using mysql driver
    connect = DriverManager.getConnection( "jdbc:mysql://localhost/countries?"
                + "user=root&password=root" );
    connect.setAutoCommit( false );

    System.out.println( "Inserting row for Japan..." );
    String sql = "INSERT INTO COUNTRIES (NAME,POPULATION) VALUES ('JAPAN', '45000000')";

    PreparedStatement insertStmt = connect.prepareStatement( sql );

    // insert statement using executeUpdate
    insertStmt.executeUpdate( sql );
    connect.rollback();

    System.out.println( "Updating row for Japan..." );
    // update statement using executeUpdate -> will cause an error, update will not be
    // executed becaues the row does not exist
    sql = "UPDATE COUNTRIES SET POPULATION='1000000' WHERE NAME='JAPAN'";
    PreparedStatement updateStmt = connect.prepareStatement( sql );

    updateStmt.executeUpdate( sql );
    connect.commit();

}
catch( SQLException ex )
{
    ex.printStackTrace();
    //undoes all changes in current transaction
    connect.rollback();
}
finally
{
    connect.close();
}

11. CRUD 命令

CRUD代表创建(Create)、读取(Read)、更新(Update)和删除(Delete)。JDBC支持所有这些操作和命令,在这一章中,我们将通过不同的Java代码片段来展示如何执行相关的操作:

创建(Create)语句。我们可以通过JDBC创建数据库,来看一个创建内存数据库的例子:

// Create a table in memory
String countriesTableSQL = "create memory table COUNTRIES (NAME varchar(256) not null primary key, POPULATION varchar(256) not null);";

// execute the statement using JDBC normal Statements
Statement st = conn.createStatement();
st.execute( countriesTableSQL );

插入(Insert)语句。JDBC支持insert语句,在程序中可以使用通用的SQL语句、并传递这些语句到不同的statement类,如Statement、PreparedStatement或CallableStatement。来看几个例子:

Statement insertStmt = conn.createStatement();

String sql = "INSERT INTO COUNTRIES (NAME,POPULATION) VALUES ('SPAIN', '45Mill')";
insertStmt.executeUpdate( sql );

sql = "INSERT INTO COUNTRIES (NAME,POPULATION) VALUES ('USA', '200Mill')";
insertStmt.executeUpdate( sql );

sql = "INSERT INTO COUNTRIES (NAME,POPULATION) VALUES ('GERMANY', '90Mill')";
insertStmt.executeUpdate( sql );

如上的statement语句会返回插入的数据行数。同样的情况也适用于update语句。下面来看一个在数据库中更新一组数据的例子:

System.out.println( "Updating rows for " + name + "..." );

 Statement updateStmt = conn.createStatement();

 // update statement using executeUpdate
 String sql = "UPDATE COUNTRIES SET POPULATION='10000000' WHERE NAME='" + name + "'";
 int numberRows = updateStmt.executeUpdate( sql );

 System.out.println( numberRows + " rows updated..." );

上面这段代码的输出如下:

Updating rows for SPAIN...
4 rows updated...

查询(select)语句。JDBC声明可以执行(几乎)任何类型的SQL。来看一个简单示例,查询给定表的所有数据并在标准控制台输出:

Statement statement = conn.createStatement();

ResultSet resultSet = statement.executeQuery( "select * from COUNTRIES" );

while( resultSet.next() )
{
	String name = resultSet.getString( "NAME" );
      	String population = resultSet.getString( "POPULATION" );
	System.out.println( "NAME: " + name );
      	System.out.println( "POPULATION: " + population );
}

程序的输入如下(取决于数据库的当前状态):

NAME: GERMANY
POPULATION: 90Mill
NAME: SPAIN
POPULATION: 45Mill
NAME: USA
POPULATION: 200Mill

删除(delete)语句。最后,JDBC支持删除表数据、删除数据表和其它SQL元素。下面的代码片段展示了删除一个特定条件的所有数据行(本例中的条件是名字等于“JAPAN”):

System.out.println( "Deleting rows for JAPAN..." );
String sql = "DELETE FROM COUNTRIES WHERE NAME='JAPAN'";
PreparedStatement deleteStmt = connect.prepareStatement( sql );

// delete statement using executeUpdate
int numberRows = deleteStmt.executeUpdate( sql );

System.out.println( numberRows + " rows deleted..." );

删除语句返回受影响的行数,本例中输出如下(取决于数据库的状态):

Deleting rows for JAPAN...
0 rows deleted...

本章中这些例子都很简单,这里展示它们只是出于学习目的,不过你应该可以想象的到你可以通过更改传入方法executeQuery()和executeUpdate()的参数来执行更复杂的SQL查询操作。

12. Java 8

Java 8中没有任何和JDBC或JDBC框架相关的重大变化。不过当我们使用JDBC时,可以利用Java 8的一些新特性获得更好的效果。我们在这里展示一些例子。例如,相较于我们往常习惯使用的查询方式,我们改用一种非常不同的方式。下面我们来看看当不使用Java 8特性时我们会怎么做,它和我们在本文所有例子中使用的代码差不多:

// we always need to write this code
System.out.println( "using Java 7" );
// connection to JDBC using mysql driver
Class.forName( "com.mysql.jdbc.Driver" );
Connection connect = DriverManager.getConnection( "jdbc:mysql://localhost/countries?"
            + "user=root&password=root" );

// select query
PreparedStatement statement = connect.prepareStatement( "select * from COUNTRIES" );
ResultSet resultSet = statement.executeQuery();

// iterating results
while( resultSet.next() )
{
	// access via name
       Object name = resultSet.getObject( 1 );
       Object population = resultSet.getObject( 2 );

       System.out.println( "Name: " + name );
       System.out.println( "Population: " + population );
}

// close resources, in case of exception resources are not properly cleared
resultSet.close();
statement.close();
connect.close();

下面是完成相同功能但是使用了Lambda表达式的版本:

// select method is called and lambda expression is provided, this expression will be used
// in the handle method of the functional interface
select( connect, "select * from COUNTRIES", ( resultSet ) -> {
	System.out.println( resultSet.getObject( 1 ) );
       System.out.println( resultSet.getObject( 2 ) );
} );

上面展示的代码段包括一个查询语句的调用,其第一个参数是Connection对象,第二个参数是一个SQL查询语句,第三个参数是一个Lambda表达式。这个Lambda表达式接收一个参数(ResultSet的实例),输出它的前两个属性,但是在这个Lamdba函数体内可以进行任何针对结果集的操作。下面是select()方法的实现代码:

public static void select( Connection connect, String sql, ResultSetHandler handler ) throws SQLException
    {
        PreparedStatement statement = connect.prepareStatement( sql );

        try (ResultSet rs = statement.executeQuery())
        {
            while( rs.next() )
            {
                handler.handle( rs );
            }

        }
    }

还有一个函数式接口ResultSetHandler:

@FunctionalInterface
public interface ResultSetHandler
{

    /**
     * This method will be executed by the lambda expression
     * 
     * @param resultSet
     * @throws SQLException
     */
    public void handle( ResultSet resultSet ) throws SQLException;

}

我们可以看到,通过使用Java 8的一些新特性,代码变得更加清晰,而且代码量大大减少。

13. 基于JDBC的SQL类库

有很多非常有名的Java类库使用了JDBC来构建它们的API。在本节内容我们会介绍其中一部分类库:

  • HSQLDB (Hyper SQL Database) 是一个关系型数据库管理系统,提供了内存和持久化存储两种模式。它有一个JDBC驱动(如其它示例所示)。在测试时这个数据库非常有用,因为它拥有非持久化特性,而且支持几乎所有的SQL核心功能。更多信息请访问 http://hsqldb.org/
  • DBUnit是JUnit的一个扩展程序。它非常适用于涉及到数据库操作的单元测试。它可以将测试对象数据库置于一个测试轮回之间的状态。如需下载更多资源和文档请访问http://www.dbunit.org
  • DBUtils 是Apache Commons的一个库,它的目标是使得JDBC更易于使用。这个类库含有的特性包括:资源的清理释放,减少代码量,简化和自动化结果集的生成。这个库非常小巧、透明和快速,如果程序员需要直接使用Java 1.6或更高版本中的JDBC,应该使用这个类库。更多文档信息请访问http://commons.apache.org/proper/commons-dbutils/
  • Spring Data 也包含一个与JDBC相关的模块,名为Spring Data JDBC扩展包。它能够支持JDBC最常用的一些特性,同时它提供了专供Oracle数据库的一些特性。如果你想了解更多信息请访问http://projects.spring.io/spring-data-jdbc-ext/
  •  JOOQ 是一个来自Data Geekery公司的很有意思的JDBC框架;它能够根据SQL数据库生成Java代码,也提供了一些简单易用的API用于获取JDBC连接、查询数据和处理结果集。更多信息请访问它的Github账户:https://github.com/jOOQ/jOOL 

14. 单元测试

当涉及到单元测试和数据库时,总是会碰到一些老生常谈的问题:

  • 我们使用的测试环境是什么?
  • 我们是使用真实的数据吗?
  • 还是使用一些合成数据?
  • 如果我们没有合适的权限的话,怎么测试数据库?

有一些类库可以在我们处理以上这些任务时提供帮助。在本章中,我们会列举其中一部分类库,同时提供一些有用的链接来帮助你们找到更多信息:

  • DBUnit: 如前所述,DBUnit是一个用来与JUnit协同工作的测试框架。更多信息访问 http://dbunit.org
  • TestNG:这个测试框架涵盖了大量的测试场景,如单元测试、功能测试、集成测试,等等。它是基于注解(Annotation)实现的。关于这个框架的更多信息,请访问他们的网站:http://testng.org/doc/index.html
  • JOOQ:这个框架提供了JDBC的模拟(mock)和测试功能。它提供了非常详细的文档,易于使用。更多信息请访问http://jooq.org

15. 总结

JDBC(Java数据库连接)作为数据库连接的标准API,可以用于连接Java和数量庞大的数据库或数据源(从基于SQL的数据库到Excel电子表格)。在本教程中,我们试图解释JDBC的体系结构,以及如何使用它;列举了JDBC使用到的主要组件,也列举了一些不同的广泛使用的数据库(如MySql)对应的驱动程序。

需要牢记的最重要的几点如下:

  • 驱动程序是用来使得Java应用程序和数据库可以良好工作的组件。JDBC需要为每个特定的数据库提供对应的驱动程序。JDBC的可用驱动程序列表在这里 http://www.oracle.com/technetwork/java/index-136695.html
  • SQL声明(Statements)每次执行时都是直接发送到数据库服务器端的。JDBC通过PreparedStatement提供了一种预处理机制,可以更加高效的执行语句、更好的利用资源。
  • 结果集(Result Sets)代表查询时获取到的所有数据行。
  • 存储过程(Stored Procedures)是整合在一起的一组SQL语句,这些语句可以通过存储过程的名字一起被调用执行,而不用分别调用它们。
  • 事务(Transactions)是一组SQL语句。一个事务的结束以调用commit()或rollback()为标志。这种分组模式使得不同的工作可以并行进行。
  • CRUD 命令是指创建(create),读取(read), 更新(update) 和 删除(delete) 命令。JDBC提供了很多机制来执行这些命令。

本教程包含了一些伴随Java 8而出现的一些新特性,比如与JDBC相关的JOOQ;我们也提到了一些使用JDBC实现的比较重要的类库,比如Spring-Data或Apache DBUtils。

16. 源码下载

你可以通过如下链接下载本教程的所有源代码:jdbc_ultimate_tutorial.

17. 相关链接

除了在本文中涉及到的所有链接和资源之外,如果你有兴趣去学习更多关于JDBC API及其特性和机制的话,你可以在Oracle的官网上找到最新最好的信息:

Translated by: Vincent Jia
This post is a translation of 69 Spring Interview Questions and Answers – The ULTIMATE List from Dani Buiza

Dani Buiza

Daniel Gutierrez Diez holds a Master in Computer Science Engineering from the University of Oviedo (Spain) and a Post Grade as Specialist in Foreign Trade from the UNED (Spain). Daniel has been working for different clients and companies in several Java projects as programmer, designer, trainer, consultant and technical lead.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button