JDBC

2017/04/05 Java

连接步骤

  1. 注册驱动(Driver)
  2. 建立连接(Connection)
  3. 创建执行声明(Statement)
  4. 执行语句
  5. 处理结果(ResultSet)
  6. 释放资源

注册驱动

// 注册时加载一次,new类时静态类加载一次
DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
// DriverManager初始化时会读取参数进行加载
System.setProperty("jdbc.driver", "oracle.jdbc.OracleDriver");
// Driver静态代码块加载 推荐使用
Class.forName("oracle.jdbc.OracleDriver");
// JDBC 4.0之后,每个驱动的jar包中,在META-INF/services目录下提供了一个名为java.sql.Driver的文件
// 文件的内容就是该接口的实现类的名称,DriverManager会自动扫描这个文件并加载驱动

创建连接

数据库连接是稀缺资源,用完后必须马上释放,Connection的使用原则是尽量晚创建,尽量早释放。

String url = "jdbc:oracle:thin:@127.0.0.1:1521:dbname";
String username = "username";
String password = "password";
Connection connection = DriverManager.getConnection(url, username, password);

连接字符串

  • oracle
    • driverClass:oracle.jdbc.driver.OracleDriver
    • url:jdbc:oracle:thin:@127.0.0.1:1521:dbname
  • mysql
    • driverClass:com.mysql.jdbc.Driver
      • 有的时候,mysql的驱动类也也会看到使用org.gjt.mm.mysql.Driver的情况,org.gjt.mm.mysql.Driver是早期的驱动名称,后来就改名为com.mysql.jdbc.Driver,现在一般都推荐使用 com.mysql.jdbc.Driver。在最新版本的mysql jdbc驱动中,为了保持对老版本的兼容,仍然保留了org.gjt.mm.mysql.Driver,但是实际上 org.gjt.mm.mysql.Driver中调用了com.mysql.jdbc.Driver,因此现在这两个驱动没有什么区别。
    • url:jdbc:mysql://localhost:3306/mydb
  • DB2
    • driverClass:com.ibm.db2.jcc.DB2Driver
    • url:jdbc:db2://127.0.0.1:50000/dbname
  • sybase
    • driverClass:com.sybase.jdbc.SybDriver
    • url:jdbc:sybase:Tds:localhost:5007/dbname
  • PostgreSQL
    • driverClass:org.postgresql.Driver
    • url:jdbc:postgresql://localhost/dbname
  • Sql Server 2000
    • driverClass:com.microsoft.jdbc.sqlserver.SQLServerDriver
    • url:jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=dbname
  • Sql Server 2005
    • driverClass:com.microsoft.sqlserver.jdbc.SQLServerDriver
      • SQL Server2000和2005的驱动是有区别的,使用错误的时候,会出现下面这个错误。java.sql.SQLException: [Microsoft][SQLServer JDBC Driver][SQLServer]传入的表格格式数据流(TDS)远程过程调用(RPC)协议流不正确。参数 1 (“”): 数据类型 0x38 未知。
    • url:jdbc:sqlserver://localhost:1433; DatabaseName=dbname
Mysql连接常见参数
参数名 作用 缺省值 版本
user 数据库用户名(用于连接数据库)   所有版本
password 用户密码(用于连接数据库)   所有版本
useUnicode 是否使用Unicode字符集,如果参数characterEncoding设置为gb2312或gbk,本参数值必须设置为true false 1.1g
characterEncoding 当useUnicode设置为true时,指定字符编码。比如可设置为gb2312或gbk false 1.1g
autoReconnectForPools 是否使用针对数据库连接池的重连策略 false 3.1.3
connectTimeout 和数据库服务器建立socket连接时的超时,单位:毫秒。 0表示永不超时,适用于JDK 1.4及更高版本 0 3.0.1
socketTimeout socket操作(读写)超时,单位:毫秒。 0表示永不超时 0 3.0.1
useServerPrepStmts 是否使用服务器端预处理语句 true 3.1.0
cachePrepStmts 当使用不同的PreparedStatement对象来执行相同的SQL语句时,还是会出现编译两次的现象,这是因为驱动没有缓存编译后的函数key,导致二次编译。如果希望缓存编译后函数的key,那么就要设置cachePrepStmts参数为true false 3.0.10
rewriteBatchedStatements 是否批量执行SQL false  

创建执行声明

Statement

//String sql = "insert into users (name,birthday) values ('xpress','1991-1-1')";
String sql = "select name,birthday from users where username =" + username;

Statement statement = connection.createStatement();

PreparedStatement

//String sql = "insert into users (name,birthday) values ('xpress','1991-1-1')";
String sql = "select name,birthday from users where username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,"xpress");
PreparedStatement优点
  1. 没有sql注入问题
  2. Statement会使数据库频繁编译sql,可能造成数据库缓冲区溢出
  3. 数据库和驱动可以对PreparedStatement进行优化(只有相关联的数据库链接没有关闭的情况下有效)

预处理(预编译)原理:

  1. 前提:数据库支持预处理(几乎都支持)
  2. 每个PreparedStatement都与一个sql模板连接在一起,先把sql给数据库,数据库先进行校验,再将sql编译成可执行函数
  3. 执行时传递参数
  4. 第二次执行时不需要语法校验和编译,直接执行

执行语句

Statement

int count = statement.executeUpdate(sql);
ResultSet resultSet = statement.executeQuery(sql);

PreparedStatement

// 注意executeQuery()没有sql参数,否则调用Statement方法执行
int count = preparedStatement.executeQuery();
ResultSet resultSet = preparedStatement.executeQuery();

处理结果

while (resultSet.next()) {
    System.out.println(resultSet.getString("name"));
    System.out.println(resultSet.getDate("birthday"));
}

释放资源

resultSet.close();
statement.close();
connection.close();

数据类型处理

Date

java.sql与数据库类型对应关系

  • java.sql.Date–>DATE
  • java.sql.Time–>TIME
  • java.sql.Timestamp–>TIMESTAMP
// 使用getTime()转换
preparedStatement.setDate(2,new java.sql.Date(new java.util.Date().getTime()));

// 子类直接赋值给父类完成转换,但是toString()方法包含了格式化处理yyyy-MM-dd
java.util.Date = resultSet.getDate("birthday");
// 使用getTime()转换
java.util.Date = new java.util.Date(resultSet.getDate("birthday").getTime());

大文本

Reader reader = new BufferedReader(new FileReader(file));
preparedStatement.setCharacterStream(3,reader,(int)file.length());
// 将reader结果字符串传递给preparedStatement.setString()也是支持的
preparedStatement.setString(3,clobString);

Clob clob = resultSet.getClob("content");
Reader reader = clob.getCharacterStream();
// 直接通过列序号或者列label获取
Reader reader = rresultSet.getCharacterStream("content");
// 使用getString()也可以获取
String clobString = resultSet.getString("content");

二进制

// 直接设置stream
InputStream inputStream = new FileInputStream(file);
preparedStatement.setBinaryStream(4,inputStream,(int)file.length());
// 通过blob设置 使用commons-io
byte[] bytes = IOUtils.toByteArray(new FileInputStream(file));
Blob blob = new SerialBlob(bytes);
preparedStatement.setBlob(4,blob);

Blob blob = resultSet.getBlob("image");
inputStream = blob.getBinaryStream();
// 直接通过列序号或者列label获取
inputStream = resultSet.getBinaryStream(4);

异常处理

使用RuntimeException上层可以根据需要是否处理,DAOException可以继承自Exception或者RuntimeException上层是否处理

try {
    Class.forName("oracle.jdbc.OracleDriver");
} catch (ClassNotFoundException e) {
    throw new ExceptionInInitializerError(e);//抛出一个error
}
String url = "jdbc:oracle:thin//127.0.0.1:1521/orcl";
String username = "username";
String password = "password";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
    connection = DriverManager.getConnection(url, username, password);
    String sql = "select username from user where userId = ?";
    preparedStatement = connection.prepareStatement(sql);
    preparedStatement.setString(1,"001");
    resultSet = preparedStatement.executeQuery(sql);
    while (resultSet.next()) {
        System.out.println(resultSet.getObject("username"));
    }
} catch (SQLException e) {
    throw new DAOException(e);
    // throw new RuntimeException(e);
} finally {
	//使用finally确保报异常后继续释放后面的资源
	try {
	    resultSet.close();
	} catch (SQLException e) {
	    throw new RuntimeException(e);
	} finally {
	    try {
	        preparedStatement.close();
	    } catch (SQLException e) {
	        throw new RuntimeException(e);
	    } finally {
	        try {
	            connection.close();
	        } catch (SQLException e) {
	            throw new RuntimeException(e);
	        }
	    }
	}
}

事务

特性

ACID

  • 原子性(Atomicity):组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分
  • 一致性(Consistency):在事务处理执行前后,数据库是一致的(数据库完整性约束)
  • 隔离性(Isolcation):一个事务对另一个事务的影响
  • 持续性:(Durability):事务处理的效果能被永久的保存下来

事务处理

try {
    Class.forName("oracle.jdbc.OracleDriver");
} catch (ClassNotFoundException e) {
    throw new ExceptionInInitializerError(e);//抛出一个error
}
String url = "jdbc:mysql://localhost:3306/mydb";
String username = "username";
String password = "password";
Connection connection = null;
ResultSet resultSet = null;
PreparedStatement preparedStatement = null;
try {
    connection = DriverManager.getConnection(url, username, password);
    connection.setAutoCommit(false);

    String sql = "UPDATE USERS SET MONEY = MONEY+10 WHERE USERNAME = ?";
    preparedStatement = connection.prepareStatement(sql);
    preparedStatement.setString(1, "xpress");
    preparedStatement.executeUpdate();

    String sqlSelect = "SELECT MONEY FROM USERS WHERE USERNAME = ?";
    preparedStatement = connection.prepareStatement(sqlSelect);
    preparedStatement.setString(1, "xpress");
    resultSet = preparedStatement.executeQuery();

    while (resultSet.next()) {
        float money = resultSet.getFloat("MONEY");
        System.out.println(money);
        if (money > 30f) {
            throw new RuntimeException("超过最大金额");
        }
    }
    connection.commit();
} catch (Exception e) {
    if (connection != null) {
        connection.rollback();
    }
    e.printStackTrace();
    throw e;
} finally {
    try {
        if (resultSet != null) {
            resultSet.close();
        }
    } finally {
        try {
            if (preparedStatement != null) {
                preparedStatement.close();
            }
        } finally {
            if (connection != null) {
                connection.close();
            }

        }
    }
}

保存点

try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
    throw new ExceptionInInitializerError(e);//抛出一个error
}
String url = "jdbc:mysql://192.168.94.129:3306/mydb";
String username = "username";
String password = "password";
Connection connection = null;
ResultSet resultSet = null;
PreparedStatement preparedStatement = null;
Savepoint savepoint = null;
try {
    connection = DriverManager.getConnection(url, username, password);
    connection.setAutoCommit(false);

    String sql = "UPDATE USERS SET money = money+10 WHERE name = ?";
    preparedStatement = connection.prepareStatement(sql);
    preparedStatement.setString(1, "xpress");
    preparedStatement.executeUpdate();
    savepoint = connection.setSavepoint();
    String sqlSelect = "SELECT money FROM users WHERE name = ?";
    preparedStatement = connection.prepareStatement(sqlSelect);
    preparedStatement.setString(1, "john");
    resultSet = preparedStatement.executeQuery();

    while (resultSet.next()) {
        float money = resultSet.getFloat("money");
        System.out.println(money);
        if (money > 10f) {
            throw new RuntimeException("超过最大金额");
        }
    }
    connection.commit();
} catch (RuntimeException e) {
    if (connection != null && savepoint != null) {
        connection.rollback(savepoint);// 回滚到还原点
        connection.commit();//提交事务
    }
    e.printStackTrace();
    throw e;
} catch (Exception e) {
    if (connection != null) {
        connection.rollback();
    }
    e.printStackTrace();
    throw e;
} finally {
    try {
        if (resultSet != null) {
            resultSet.close();
        }
    } finally {
        try {
            if (preparedStatement != null) {
                preparedStatement.close();
            }
        } finally {
            if (connection != null) {
                connection.close();
            }

        }
    }
}

隔离级别

  • 脏读:事务1更新了记录,但没有提交,事务2读取了更新后的行,然后事务T1回滚,现在T2读取无效
  • 不可重复读:事务1读取记录时,事务2更新了记录并提交,事务1再次读取时可以看到事务2修改后的记录
  • 幻读:事务1读取记录时事务2增加了记录并提交,事务1再次读取时可以看到事务2新增的记录
隔离级别 脏读 不可重复读 幻读
读未提交 (Read uncommitted)
读已提交 (Read committed) ×
可重复读 (Repeatable read) × ×
可穿行化 (Serializable) × × ×
connection.setTransactionIsolation(Connection.TRANSACTION_NONE);// JDBC驱动不支持事务
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读未提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);// 读已提交
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);// 可重复读
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);// 可穿行化

数据库默认隔离级别

数据库 隔离级别 查询语句
mysql Repeatable read SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
oracle Read committed  

存储过程

Connection conn = null;
CallableStatement cs = null;
ResultSet rs = null;
try {
	conn = JdbcUtils.getConnection();

	String sql = "{ call addUser(?,?,?,?) } ";
	cs = conn.prepareCall(sql);
	cs.registerOutParameter(4, Types.INTEGER);// 注册返回参数
	cs.setString(1, "ps name");
	cs.setDate(2, new java.sql.Date(System.currentTimeMillis()));
	cs.setFloat(3, 100f);

	cs.executeUpdate();// 执行

	int id = cs.getInt(4);// 获取返回参数

	System.out.println("id=" + id);
} finally {
	JdbcUtils.free(rs, cs, conn);// 释放资源
}

其他API

获取返回值

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
	conn = JdbcUtils.getConnection();
	String sql = "insert into user(name,birthday, money) values (?,?,?) ";
	ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);// 设置获取返回值
	ps.setString(1, user.getName());
	ps.setDate(2, new java.sql.Date(user.getBirthday().getTime()));
	ps.setFloat(3, user.getMoney());
	ps.executeUpdate();

	rs = ps.getGeneratedKeys();// 获得ResultSet
	if (rs.next())
		user.setId(rs.getInt(1));// 获取id
} catch (SQLException e) {
	throw new DaoException(e.getMessage(), e);
} finally {
	JdbcUtils.free(rs, ps, conn);
}

可滚动结果集

createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)参数:

resultSetType

  • ResultSet.TYPE_FORWARD_ONLY 不滚动
  • ResultSet.TYPE_SCROLL_INSENSITIVE 滚动不敏感,不感应数据库变化
  • ResultSet.TYPE_SCROLL_SENSITIVE 滚动敏感,感应数据库变化(驱动一般不支持)

resultSetConcurrency

  • ResultSet.CONCUR_READ_ONLY 只读,不影响数据库
  • ResultSet.CONCUR_UPDATABLE 可更新,影响数据库

resultSetHoldability

  • ResultSet.HOLD_CURSORS_OVER_COMMIT 修改提交时ResultSet不关闭
  • ResultSet.CLOSE_CURSORS_AT_COMMIT 修改提交时ResultSet关闭
statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);// 这是可滚动和resultSet可读取
resultSet = statement
        .executeQuery("select id, name, money, birthday  from users");
while (resultSet.next()) {
    System.out.println(resultSet.getObject("id") + "\t"
            + resultSet.getObject("name") + "\t"
            + resultSet.getObject("birthday") + "\t"
            + resultSet.getObject("money"));
}

System.out.println("------------");
resultSet.absolute(3);// 定位resultSet 从1开始计算
resultSet.relative(-1);// 相对位移,正数向下负数向上
resultSet.getRow();// 获取当前行数
int i = 0;
while (resultSet.next() && i < 10) {
    i++;
    System.out.println(resultSet.getObject("id") + "\t"
            + resultSet.getObject("name") + "\t"
            + resultSet.getObject("birthday") + "\t"
            + resultSet.getObject("money"));
}
// 向前滚动
if (resultSet.previous()) {
    System.out.println(resultSet.getObject("id") + "\t"
            + resultSet.getObject("name") + "\t"
            + resultSet.getObject("birthday") + "\t"
            + resultSet.getObject("money"));
}

批处理

Statement和PreparedStatement都支持批处理

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
	conn = JdbcUtils.getConnection();
	String sql = "insert into users(name,birthday, money) values (?, ?, ?) ";
	ps = conn.prepareStatement(sql);
	for (int i = 0; i < 100; i++) {
		ps.setString(1, "batch name" + i);
		ps.setDate(2, new Date(System.currentTimeMillis()));
		ps.setFloat(3, 100f + i);

		ps.addBatch();
	}
	int[] is = ps.executeBatch();
} finally {
	JdbcUtils.free(rs, ps, conn);
}

元数据

  • DatabaseMetaData可以获得数据库相关的信息如:数据库版本、数据库名、数据库厂商信息、是否支持事务、是否支持某种事务隔离级别,是否支持滚动结果集等
  • ParameterMetaData可以获得参数信息
  • ResultSetMetaData可以获得结果有几列、各列名、各列别名、各列类型等
// DatabaseMetaData
DatabaseMetaData databaseMetaData = connection.getMetaData();
System.out.println("db name: " + databaseMetaData.getDatabaseProductName());
System.out.println("tx: " + databaseMetaData.supportsTransactions());

// ParameterMetaData
ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
int count = parameterMetaData.getParameterCount();

for (int i = 1; i <= count; i++) {
    System.out.print(parameterMetaData.getParameterClassName(i) + "\t");// java类型
    System.out.print(parameterMetaData.getParameterType(i) + "\t");// 数据类型
    System.out.println(parameterMetaData.getParameterTypeName(i));
}

//ResultSetMetaData
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int count = resultSetMetaData.getColumnCount();
String[] colNames = new String[count];
for (int i = 1; i <= count; i++) {
    // System.out.print(resultSetMetaData.getColumnClassName(i) + "\t");// java类型
    // System.out.print(resultSetMetaData.getColumnName(i) + "\t");// 列名
    // System.out.println(resultSetMetaData.getColumnLabel(i));// 别名
    colNames[i - 1] = resultSetMetaData.getColumnLabel(i);
}
List<Map<String, Object>> datas = new ArrayList<Map<String, Object>>();

while (resultSet.next()) {
    Map<String, Object> data = new HashMap<String, Object>();
    for (int i = 0; i < colNames.length; i++) {
        data.put(colNames[i], resultSet.getObject(colNames[i]));
    }
    datas.add(data);
}

数据源和连接池

DataSource用来取代DriverManager来获取Connection,通过DataSource获得Connection速度很快;通过DataSource获得的Connection都是已经被包裹过的(不是驱动原来的连接),他的close方法已经被修改。一般DataSource内部会用一个连接池来缓存Connection,这样可以大幅度提高数据库的访问速度;连接池可以理解成一个能够存放Connection的Collection;

DBCP

# 连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=root
# 初始化连接
dataSource.initialSize=10
# 最大空闲连接
dataSource.maxIdle=20
# 最小空闲连接
dataSource.minIdle=5
# 最大连接数量
dataSource.maxActive=50
# 是否在自动回收超时连接的时候打印连接的超时错误
dataSource.logAbandoned=true
# 是否自动回收超时连接
dataSource.removeAbandoned=true
# 超时时间(以秒数为单位)
# 设置超时时间有一个要注意的地方,超时时间=现在的时间-程序中创建Connection的时间,如果maxActive比较大,比如超过100,那么removeAbandonedTimeout可以设置长一点比如180,也就是三分钟无响应的连接进行回收,当然应用的不同设置长度也不同。
dataSource.removeAbandonedTimeout=180
# 超时等待时间以毫秒为单位
# maxWait代表当Connection用尽了,多久之后进行回收丢失连接
dataSource.maxWait=1000
Properties properties = new Properties();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("dbcp.properties");
properties.load(inputStream);// load配置文件
DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);//创建数据源
Connection connection = dataSource.getConnection();// 从连接池获取连接

C3P0

仅从测试结果来看,dbcp会更快,效率更高。但从实际应用来看,c3p0更稳定。

ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 使用配置文件可以加载不同数据源配置
// ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("oracle-config");
// 使用配置文件可以省略下面设置,同时使用使用java代码设置覆盖配置文件配置
// 连接设置
comboPooledDataSource.setUser("username");
comboPooledDataSource.setPassword("password");
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
comboPooledDataSource.setDriverClass("com.mysql.jdbc.Driver");
// 池设置
comboPooledDataSource.setAcquireIncrement(5);
comboPooledDataSource.setInitialPoolSize(10);
comboPooledDataSource.setMaxPoolSize(50);
comboPooledDataSource.setMinPoolSize(5);

Connection connection = comboPooledDataSource.getConnection();

配置文件

  • 文件名称:必须叫c3p0-config.xml
  • 文件位置:classpath下
<c3p0-config>
    <!--默认配置-->
    <default-config>
        <!--连接设置-->
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb</property>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="user">username</property>
        <property name="password">password</property>
        <!--池设置-->
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">25</property>
        <property name="minPoolSize">5</property>
    </default-config>
    <!--命名配置-->
    <named-config name="oracle-config">
        <property name="jdbcUrl">jdbc:oracle:thin:@127.0.0.1:1521:dbname</property>
        <property name="driverClass">oracle.jdbc.driver.OracleDriver</property>
        <property name="user">username</property>
        <property name="password">password</property>
    </named-config>
</c3p0-config>

JDBC工具

Spring

设计

服务对象本身并不会处理数据访问,而是将数据访问委托给Repository。Repository接口确保其与服务对象的松耦合

design

好处:

  • 第一,它使得服务对象易于测试,因为它们不再与特定的数据访问实现绑定在一起。实际上,你可以为这些数据访问接口创建mock实现,这样无需连接数据库就能测试服务对象,而且会显著提升单元测试的效率并排除因数据不一致所造成的测试失败。
  • 此外,数据访问层是以持久化技术无关的方式来进行访问的。持久化方式的选择独立于Repository,同时只有数据访问相关的方法才通过接口进行暴露。这可以实现灵活的设计,并且切换持久化框架对应用程序其他部分所带来的影响最小。如果将数据访问层的实现细节渗透到应用程序的其他部分中,那么整个应用程序将与数据访问层耦合在一起,从而导致僵化的设计。

异常体系

为了将数据访问层与应用程序的其他部分隔离开来,Spring采用的方式之一就是提供统一的异常体系,这个异常体系用在了它支持的所有持久化方案中。

可能导致抛出SQLException的常见问题包括:

  • 应用程序无法连接数据库
  • 要执行的查询存在语法错误
  • 查询中所使用的表和/或列不存在
  • 试图插入或更新的数据违反了数据库约束

Spring所提供的平台无关的持久化异常

Spring JDBC提供的数据访问异常体系解决了两个问题:

  • 一方面,JDBC的异常体系过于简单了——实际上,它算不上一个体系
  • 另一方面,Hibernate的异常体系是其本身所独有的。我们需要的数据访问异常要具有描述性而且又与特定的持久化框架无关
JDBC的异常 Spring的数据访问异常
BatchUpdateException BadSqlGrammarException
DataTruncation CannotAcquireLockException
SQLException CannotSerializeTransactionException
SQLWarning CannotGetJdbcConnectionException
  CleanupFailureDataAccessException
  ConcurrencyFailureException
  DataAccessException
  DataAccessResourceFailureException
  DataIntegrityViolationException
  DataRetrievalFailureException
  DataSourceLookupApiUsageException
  DeadlockLoserDataAccessException
  DuplicateKeyException
  EmptyResultDataAccessException
  IncorrectResultSizeDataAccessException
  IncorrectUpdateSemanticsDataAccessException
  InvalidDataAccessApiUsageException
  InvalidDataAccessResourceUsageException
  InvalidResultSetAccessException
  JdbcUpdateAffectedIncorrectNumberOfRowsException
  LbRetrievalFailureException
   
BatchUpdateException NonTransientDataAccessResourceException
DataTruncation OptimisticLockingFailureException
SQLException PermissionDeniedDataAccessException
SQLWarning PessimisticLockingFailureException
  QueryTimeoutException
  RecoverableDataAccessException
  SQLWarningException
  SqlXmlFeatureNotImplementedException
  TransientDataAccessException
  TransientDataAccessResourceException
  TypeMismatchDataAccessException
  UncategorizedDataAccessException
  UncategorizedSQLException

尽管Spring的异常体系比JDBC简单的SQLException丰富得多,但它并没有与特定的持久化方式相关联。这意味着我们可以使用Spring抛出一致的异常,而不用关心所选择的持久化方案。这有助于我们将所选择持久化机制与数据访问层隔离开来。

这些异常都继承自DataAccessException。DataAccessException的特殊之处在于它是一个非检查型异常。

为了利用Spring的数据访问异常,我们必须使用Spring所支持的数据访问模板。

数据访问模板化

Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板管理过程中固定的部分,而回调处理自定义的数据访问代码。

template-callback

Spring提供的数据访问模板,分别适用于不同的持久化机制:

模板类(org.springframework.*) 用途
jca.cci.core.CciTemplate JCA CCI连接
jdbc.core.JdbcTemplate JDBC连接
jdbc.core.namedparam.NamedParameterJdbcTemplate 支持命名参数的JDBC连接
jdbc.core.simple.SimpleJdbcTemplate 通过Java 5简化后的JDBC连接(Spring 3.1中已经废弃)
orm.hibernate3.HibernateTemplate Hibernate 3.x以上的Session
orm.ibatis.SqlMapClientTemplate iBATIS SqlMap客户端
orm.jdo.JdoTemplate Java数据对象(Java Data Object)实现
orm.jpa.JpaTemplate Java持久化API的实体管理器

配置数据源

Spring提供了在Spring上下文中配置数据源bean的多种方式,包括:

  • 通过JDBC驱动程序定义的数据源
  • 通过JNDI查找的数据源
  • 连接池的数据源
使用JNDI数据源

好处在于数据源完全可以在应用程序之外进行管理,这样应用程序只需在访问数据库的时候查找数据源就可以了。另外,在应用服务器中管理的数据源通常以池的方式组织,从而具备更好的性能,并且还支持系统管理员对其进行热切换。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
    <jee:jndi-lookup jndi-name="/jdbc/Spittr" id="dataSource"/>
</beans>
@Bean
public DataSource dataSource() {
    final JndiDataSourceLookup jndiDataSourceLookup = new JndiDataSourceLookup();
    // 自动添加java:comp/env/前缀
    jndiDataSourceLookup.setResourceRef(true);
    return jndiDataSourceLookup.getDataSource("jdbc/Spittr");
}

@Bean
public JndiObjectFactoryBean dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/Spittr");
    // 自动添加java:comp/env/前缀
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return jndiObjectFactoryBean;
}
使用数据源连接池

Spring并没有提供数据源连接池实现,但是我们有多项可用的方案,包括如下开源的实现:

  • Apache Commons DBCP (http://jakarta.apache.org/commons/dbcp)
  • c3p0 (http://sourceforge.net/projects/c3p0/)
  • BoneCP (http://jolbox.com/)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
          p:driverClassName="org.h2.Driver"
          p:url="jdbc:h2:tcp://localhost/~/spittr"
          p:username="sa"
          p:password="sa"
          p:initialSize="5"
          p:maxTotal="10"
    />
</beans>
@Bean
public DataSource dataSource() {
    BasicDataSource basicDataSource = new BasicDataSource();
    basicDataSource.setDriverClassName("org.h2.Driver");
    basicDataSource.setUrl("jdbc:h2:tcp://localhost/~/spittr");
    basicDataSource.setUsername("sa");
    basicDataSource.setPassword("sa");
    basicDataSource.setInitialSize(5);
    basicDataSource.setMaxTotal(10);
    return basicDataSource;
}

BasicDataSource的池配置属性:

池配置属性 所指定的内容
initialSize 池启动时创建的连接数量
maxActive 同一时间可从池中分配的最多连接数。如果设置为0,表示无限制
maxIdle 池里不会被释放的最多空闲连接数。如果设置为0,表示无限制
maxOpenPreparedStatements 在同一时间能够从语句池中分配的预处理语句(prepared statement)的最大数量。如果设置为0,表示无限制
maxWait 在抛出异常之前,池等待连接回收的最大时间(当没有可用连接时)。如果设置为-1,表示无限等待
minEvictableIdleTimeMillis 连接在池中保持空闲而不被回收的最大时间
minIdle 在不创建新连接的情况下,池中保持空闲的最小连接数
poolPreparedStatements 是否对预处理语句(prepared statement)进行池管理(布尔值)
基于JDBC驱动的数据源

在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了三个这样的数据源类(均位于org.springframework.jdbc.datasource包中)供选择:

  • DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接并没有进行池化管理
  • SimpleDriverDataSource:与DriverManagerDataSource的工作方式类似,但是它直接使用JDBC驱动,来解决在特定环境下的类加载问题,这样的环境包括OSGi容器
  • SingleConnectionDataSource:在每个连接请求时都会返回同一个的连接。尽管SingleConnectionDataSource不是严格意义上的连接池数据源,但是你可以将其视为只有一个连接的池
@Bean
public DataSource dataSource() {
    DriverManagerDataSource managerDataSource = new DriverManagerDataSource();
    managerDataSource.setDriverClassName("org.h2.Driver");
    managerDataSource.setUrl("jdbc:h2:tcp://localhost/~/spittr");
    managerDataSource.setUsername("sa");
    managerDataSource.setPassword("sa");
    return managerDataSource;
}
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
	p:driverClassName="org.h2.Driver"
	p:url="jdbc:h2:tcp://localhost/~/spittr"
	p:username="sa"
	p:password="sa"
/>

与具备池功能的数据源相比,唯一的区别在于这些数据源bean都没有提供连接池功能,所以没有可配置的池相关的属性。

使用嵌入式的数据源
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScripts("classpath:spittr/db/jdbc/schema.sql", "classpath:spittr/db/jdbc/test-data.sql")
            .build();
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<!-- type such as HSQL, H2 or Derby. Defaults to HSQL. -->
	<jdbc:embedded-database id="dataSource" type="H2">
		<jdbc:script location="classpath:spittr/db/jdbc/schema.sql" />
		<jdbc:script location="classpath:spittr/db/jdbc/test-data.sql" />
	</jdbc:embedded-database>
</beans>
使用profile选择数据源
@Profile("develop")
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScripts("classpath:spittr/db/jdbc/schema.sql", "classpath:spittr/db/jdbc/test-data.sql")
            .build();
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<beans profile="develop">
        <jdbc:embedded-database id="dataSource" type="H2">
            <jdbc:script location="classpath:spittr/db/jdbc/schema.sql"/>
            <jdbc:script location="classpath:spittr/db/jdbc/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
</beans>

使用JDBC模板

JDBC不要求我们掌握其他框架的查询语言。它是建立在SQL之上的,而SQL本身就是数据访问语言。此外,与其他的技术相比,使用JDBC能够更好地对数据访问的性能进行调优。JDBC允许你使用数据库的所有特性,而这是其他框架不鼓励甚至禁止的。

Spring为JDBC提供了三个模板类供选择:

  • JdbcTemplate:最基本的Spring JDBC模板,这个模板支持简单的JDBC数据库访问功能以及基于索引参数的查询
  • NamedParameterJdbcTemplate:使用该模板类执行查询时可以将值以命名参数的形式绑定到SQL中,而不是使用简单的索引参数
  • SimpleJdbcTemplate(Spring3.1中废弃,特性被转移至JdbcTemplate):该模板类利用Java 5的一些特性如自动装箱、泛型以及可变参数列表来简化JDBC模板的使用

Tips:当需要使用命名参数时使用NamedParameterJdbcTemplate,否则使用JdbcTemplate

JdbcTemplate
原理

JdbcTemplate的类图:

jdbctemplate-hierarchy

  • 在基类JdbcAccessor的设计中,对DataSource数据源进行管理和配置。
  • 在JdbcOperation接口中,定义了通过JDBC操作数据库的基本操作方法,而JdbcTemplate提供这些接口方法的实现,比如方法、方法、方法等。

execute方法的设计时序:

jdbctemplate-execute

query的设计时序:

jdbctemplate-query

Spring JDBC中RDBMS操作对象的实现

RDBMS对象的基本继承关系:

rdbms-operation

SqlQuery的子类MappingSqlQuery调用时序:

mapping-sql-query

SqlUpdate的设计时序:

sql-update

MappingSqlQuery子类SqlFunction的设计时序:

sql-function

应用
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

线程安全的

// JdbcTemplate实现了JdbcOperations接口,注入是应选择JdbcOperations
JdbcTemplate jdbcTemplate = new JdbcTemplate(JdbcUtils.getDataSource());

User findUser(String name) {
    String sql = "select id, name, money, birthday  from user where name=?";
    Object[] args = new Object[] { name };
    // 使用RowMapper
    Object user = jdbcTemplate.queryForObject(sql, args, new RowMapper() {
        public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setMoney(rs.getFloat("money"));
            user.setBirthday(rs.getDate("birthday"));
            return user;
        }
    });
    // 使用反射直接获得
    user = jdbcTemplate.queryForObject(sql, args, new BeanPropertyRowMapper(User.class));
    // 返回list
    List users = jdbcTemplate.query(sql, args, argTypes, new BeanPropertyRowMapper(
                User.class));

    return (User) user;
}

int getUserCount() {
    String sql = "select count(*) from user";
    return jdbcTemplate.queryForInt(sql);
}

Map getData(int id) {
    String sql = "select id , name, money from user where id="+ id;
    //return jdbcTemplate.queryForList(sql);
    return jdbcTemplate.queryForMap(sql);
}

String getUserName(int id) {
    String sql = "select name from user where id=" + id;
    Object name = jdbcTemplate.queryForObject(sql, String.class);
    return (String) name;
}

int addUser(final User user) {
    int result = (int)jdbcTemplate.execute(new ConnectionCallback() {
        // doInConnection完成特殊操作
        public Object doInConnection(Connection con) throws SQLException,
                DataAccessException {
            String sql = "insert into user(name,birthday, money) values (?,?,?) ";
            PreparedStatement ps = con.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            ps.setString(1, user.getName());
            ps.setDate(2, new java.sql.Date(user.getBirthday().getTime()));
            ps.setFloat(3, user.getMoney());
            ps.executeUpdate();

            ResultSet rs = ps.getGeneratedKeys();
            if (rs.next())
                return rs.getInt(1);
            return -1;
        }
    });
    return result;
}
NamedParameterJdbcTemplate

Spring 3时使用SimpleJdbcTemplate支持可变参数和泛型,Spring 4后废除了SimpleJdbcTemplate,在NamedParameterJdbcTemplate和JdbcTemplate中提供这些特性

@Bean
public NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) {
    return new NamedParameterJdbcTemplate(dataSource);
}
NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(JdbcUtils.getDataSource());

User findUser(User user) {
	String sql = "select id, name, money from user where money > :m and id < :id";
	// Map参数映射
	Map params = new HashMap();
	params.put("m", user.getMoney());
	params.put("id", user.getId());
	Object user = namedParameterJdbcTemplate.queryForObject(sql, params, new BeanPropertyRowMapper(User.class));
	return (User)user;
}

User findUser(User user) {
	String sql = "select id, name, money from user where money > :money and id < :id";
	// BeanPropertySqlParameterSource转换参数,需要与参数名对应
	SqlParameterSource ps = new BeanPropertySqlParameterSource(user);
	// BeanPropertyRowMapper转换返回值
	Object user = namedParameterJdbcTemplate.queryForObject(sql, ps, new BeanPropertyRowMapper(User.class));
	return (User) user;
}

void addUser(User user) {
	// 冒号加列名对应参数名
	String sql = "insert into user(name,birthday, money) values (:name,:birthday,:money) ";
	SqlParameterSource ps = new BeanPropertySqlParameterSource(user);// 转换bean为参数
	KeyHolder keyHolder = new GeneratedKeyHolder();
	namedParameterJdbcTemplate.update(sql, ps, keyHolder);
	int id = keyHolder.getKey().intValue();// 获取返回值
	user.setId(id);
	// 多主键
	//Map map = keyHolder.getKeys();
}

dbutils

增删改

QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDatasource());

String sql = "delete from users where id = ?";
Object[] params = new Object[]{11};

String sql = "insert into users (id) values (?)";
Object[] params = new Object[]{11};

String sql = "update users set name = ? where id = ?";
Object[] params = new Object[]{"smith", 11};

int rows = queryRunner.update(sql, params);
// 这个方法不管理connection,适用于事务处理
// int rows = queryRunner.update(connection, sql, params);
System.out.println(rows);

查询

QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDatasource());

String sql = "select * from users where id = ?";
Object[] params = new Object[]{11};

// 使用bean解析器
User user = queryRunner.query(sql, new BeanHandler<User>(User.class), params);
// 多行解析器
List<User> user = queryRunner.query(sql, new BeanListHandler<User>(User.class), params);
// map解析器
Map user = queryRunner.query(sql, new MapHandler(), params);
// 多行map解析器
List<Map<String, Object>> user = queryRunner.query(sql, new MapListHandler(), params);
// 单行单列解析器 select count(*) from users;
Number rows = (Number) queryRunner.query(sql, new ScalarHandler(), params);
System.out.println(rows.intValue());
// 自定义解析器
ResultSetHandler<User> resultSetHandler = new ResultSetHandler<User>() {
    @Override
    public User handle(ResultSet resultSet) throws SQLException {
        if (!resultSet.next()) {
            return null;
        }
        User user = new User();
        user.setId(resultSet.getInt("id"));
        user.setMoney(resultSet.getDouble("money"));
        user.setBirthday(resultSet.getDate("birthday"));
        user.setName(resultSet.getString("name"));
        return user;
    }
};

User user = queryRunner.query(sql, resultSetHandler, params);
System.out.println(user);

JNDI

jndi方式配置数据源交个容器管理,如tomcat的context.xml中配置连接池属性

参考工具Spring的JNDI部分

Tomcat JNDI配置和使用

  • Context配置
<!-- 标准数据源 -->
<Context>
    <Resource auth="Container" 
        name="jdbc/dataSource"
        type="javax.sql.DataSource"

        driverClassName="oracle.jdbc.driver.OracleDriver"
        url="jdbc:oracle:thin:@127.0.0.1:1521/orcl"
        username="username"
        password="password"

        maxActive="10"
        maxIdle="3"
        maxWait="5000"
        defaultAutoCommit="false"
        />
</Context>
<!-- C3P0数据源 -->
<Context>
    <Resource auth="Container" 
        name="jdbc/dataSource"
        factory="org.apache.naming.factory.BeanFactory"

        type="com.mchange.v2.c3p0.ComboPooledDataSource"
        driverClassName="oracle.jdbc.driver.OracleDriver"
        url="jdbc:oracle:thin:@127.0.0.1:1521/orcl"
        username="username"
        password="password"

        acquireIncrement="5"
        initialPoolSize="10"
        maxIdleTime="30"
        maxPoolSize="25"
        minPoolSize="5"
        defaultAutoCommit="false"
        />
</Context>
  • 获取数据源
try {
    Context context = new InitialContext();
    Context envContext = (Context) context.lookup("java:comp/env");
    DataSource dataSource = (DataSource) envContext.lookup("jdbc/dataSource");
    // 简便写法
    dataSource = (DataSource) context.lookup("java:comp/env/jdbc/dataSource");
} catch (NamingException e) {
    e.printStackTrace();
}

以上概念总结于传智播客JavaWeb课程,Spring In Action,Spring技术内幕

Search

    Post Directory