第一节 整理目的
当今orm等全自动针对对象持久化的框架越来越多并且也越来越成熟(ibatis,hibernate,ejb的jpa),但是无奈新东家需要使用jdbc(原始手工作坊)的模式和数据库打交道,用了几年的ibatis,再次使用jdbc发现有些细节和底层的东西自己并不是十分清楚,所以就啰理啰嗦的整理出一份学习笔记,第一作为自己对jdbc重新的复习,第二如果有可能希望给初学jdbc的朋友带来一定的便利,这样也不枉我点点滴滴的记录。
随着对jdbc整理和学习的逐渐深入,发现原先使用orm框架时忽略了那么多的细节,这样在出现问题或者学习orm更加深入知识时则会显得力不从心,在本文档将jdbc如何入门阐述清楚之后,增加了如下的内容:
Ø 数据库连接池,以及常用连接池的使用(dbcp,c3p0等)
Ø 编写一套基于jdbc轻量级的api,方便使用;
Ø 如何将查询结果封装为对象;
Ø 如何将查询结果封装为Map;
Ø 如何将查询结果封装为List;
Ø 如何在JDBC的使用中加入策略,模板等模式;
Ø 在后面的JDBC高级部分将会讲解到Dbutils源码,Spring对JDBC的强大封装
第二节 jdbc的概念
2.1概念
我最不喜欢替别人整理某个名词的概念了,只要是概念性的东西基本上在任何地方都可以查得到,所以我就通俗的写一些自己对jdbc的理解,所谓jdbc就是java与数据库之间进行通讯的api,也就是一个标准,所以如果一个java应用程序想要和数据库打交道基本上都离不开jdbc,众所周知,一些优秀的orm框架的底层也是采用jdbc进行封装的。
2.2 Jdbc与应用程序的关系
JdbcAPI所处的位置和它与应用程序之间的关系,下面的一张图再也明显不过了,其中绿色的部分代表jdbcAPI,它提供了很多接口,并且本身也实现了很多方法,可以看到蓝色的部分就是各个数据库厂商自己对jdbcAPI的一些实现,这就是我们常见的数据库连接驱动,这是使用jdbc程序进行开发必不可少的东西。
2.3 数据库的连接步骤
1. 注册驱动 (Driver)
2. 建立连接(创建Connection)
3. 创建执行sql语句(通常是创建Statement或者其子类)
4. 执行语句
5. 处理执行结果(在非查询语句中,该步骤是可以省略的)
6. 释放相关资源
在后文中,将会对上述几个步骤一一进行讲解,希望读者能够仔细阅读;
2.4 Quick Start
好了,了解了一下jdbc的基本概念,相比对jdbc已经有了一个感性的认识,现在我们为了直观期间,直接来上一段代码了解一下jdbc最简单的程序如何进行开发的。
在该小节中,我们以一个简单的增删改查为例进行说明,然后会将该章节中涉及的各个常用以及关键的API进行详细的讲解;
首先我们创建一个数据表,在test数据库下,见表语句为
create table user(id integer primary key, name varchar(30) , birthday date, money float); |
插入两条语句
l insert into user values(2,'zhangsan','2010-01-01',15000);
l insert into user values(1,'wangwenjun','1984-06-09',8500.00);
好了,数据准备好了,我们通过一个完整的例子讲上述中数据库的连接步骤进行一个演示,在本例子中,初学者可能有些地方会觉得陌生,看不明白,不用着急,在后文中会对涉及的知识点逐个进行讲解
@Test public void wholeExample(){ try { //1.注册驱动 Class.forName("com.mysql.jdbc.Driver"); //2.获取数据库连接 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); //3.创建执行句柄 Statement stmt = conn.createStatement(); //4.执行sql语句 ResultSet rs = stmt.executeQuery("select * from user"); //5.处理执行结果 while(rs.next()){ System.out.println("id:"+rs.getInt(1)+"\tname:"+rs.getString(2)+"\tbirthday:"+rs.getDate(3)+"\tmoney:"+rs.getFloat(4)); } //6.释放资源 rs.close(); stmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } |
执行结果如下
id:1 name:wangwenjun birthday:1984-06-09 money:8500.0
id:2 name:zhangsan birthday:2010-01-01 money:15000.0
第三节 如何与数据库建立连接
3.1 注册驱动
l 第一种注册方式
通常来说,注册驱动的方式有三种,下面我们将一一进行介绍,首先来看看直接调用DriverManager的registerDriver方法进行加载驱动,在本文中所有的程序均是在mysql数据库上进行演示的。
示例代码如下
@Test public void registDriver1(){ try { DriverManager.registerDriver(new com.mysql.jdbc.Driver()); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); }
} |
执行结果为
可以看到,当前我们的程序与数据库的连接是正常的。
l 第二种注册方式
@Test public void registDriver2(){ try { System.setProperty("jdbc.drivers", "com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); }
} |
执行结果为
可以看到,当前我们的程序与数据库的连接是正常的。
l 第三种注册方式
@Test public void registDriver3(){ try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); }
} |
执行结果为
可以看到,当前我们的程序与数据库的连接是正常的。
一般来说注册驱动的方式大致上有上述三个,但是最常用的是最后一个,通过我们的代码演示可以看出,数据库都是完全可以被访问成功的。
3.2 建立数据库的连接
其实在上文中的代码演示中,我们都会看到如何获取一个数据库连接,就是通过DriverManager.getConnection()方法获取数据库的链接,该方法大致有三个重载的方法,都是可以进行数据库连接的获取的,下面我们将会一一进行演示
Static Connection | getConnection(String url) |
static Connection | getConnection(String url, Properties info) |
static Connection | getConnection(String url,String user, String password) 试图建立到给定数据库 URL 的连接。 |
l getConnection(String url)
该实例中,登录数据库的所有信息都编写在url中,实例代码如下
@Test public void getConn1(){ try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?user=root&password=r66t"); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } |
l getConnection(String url, Properties info)
该方法则是将用户名和密码的信息存放在一个Properties键值对中,示例代码如下
@Test public void getConn2(){ try { Class.forName("com.mysql.jdbc.Driver"); Properties props = new Properties(); props.put("user", "root"); props.put("password", "r66t"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test",props); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } |
l getConnection(String url,String user, String password)
该方法则是我们在上文中演示了很多次的方式,也是最常用的一种方式,在这里再次进行一下赘述
@Test public void getConn3(){ try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); Assert.assertEquals(false, conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); }
} |
3.3 规范Quick Start中的例子
其中,我们在Quick Start中写了一个较为完整的代码示例,但是在该代码中存在很多的问题,我们通过本节的介绍,一一进行规范和优化,并且说明一下优化的好处是什么
使用数据库时,涉及数据库的资源都是非常奇缺的,我们在使用的过程中务必保证我们将使用过的资源释放,供别人再次使用或者自己下次再次使用,还有,创建数据库连接时可能存在各种各样的问题导致数据库连接获取失败,这个时候你的应用应该有义务告知上一层使用者到底出现了什么问题,这样就需要一个异常传递的过程(异常是一个比较复杂的机制,笔者在另一篇文章中有详细的讲解,希望读者能够关注)
@Test public void regularWhole() throws Exception{//抛出异常 Connection conn = null; Statement stmt = null; ResultSet rs = null; try { //1.注册驱动 Class.forName("com.mysql.jdbc.Driver"); //2.获取数据库连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","r66t"); //3.创建执行句柄 stmt = conn.createStatement(); //4.执行sql语句 rs = stmt.executeQuery("select * from user"); //5.处理执行结果 while(rs.next()){ System.out.println("id:"+rs.getInt(1)+"\tname:"+rs.getString(2)+"\tbirthday:"+rs.getDate(3)+"\tmoney:"+rs.getFloat(4)); } } finally{ try { //6.释放资源 if(null!=rs){ rs.close(); } if(null!=stmt){ stmt.close(); } if(null!=conn){ conn.close(); } } finally{ if(null!=rs){ rs.close(); } if(null!=stmt){ stmt.close(); } if(null!=conn){ conn.close(); } } } } |
在该实例中,可以看出确保了资源的完全释放,也将异常抛出告知上一层使用者,那块出现了问题,但是可以看到代码明显写的很罗嗦,而且有很多地方还是值得考究的
其中,注册驱动,数据库的驱动注册,只需要一次即可,重复注册是没有任何意义的,并且资源的释放,在每次使用的时候都进行资源的释放(写资源释放的代码)显得非常罗嗦,所以我们进行再一次的一个优化,代码如下
package com.wangwenjun.jdbc;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
public final class ConnCreate {
static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
public static Connection getConnection(String url, String user, String pwd) { Connection conn = null; try { conn = DriverManager.getConnection(url, user, pwd); } catch (SQLException e) { e.printStackTrace(); } return conn; }
public static void close(Connection conn, Statement stmt, ResultSet rs) throws SQLException {
if (null != rs) { rs.close(); } if (null != stmt) { stmt.close(); } if (null != conn) { conn.close(); } } } |
可以看出,驱动只会被注册一次,并且对资源释放的代码进行了抽取,在以后的使用过程中则会简单许多,当然上述的代码如果还需要追究问题,肯定还是存在的,在接下来的章节中我们也会进行深入的说明。
第四节 Statement 接口的使用详解
Statement 是应用与数据库打交道最关键的一个接口,该接口包括了我们常用的CRUD操作,还可以设置抓取策略,比如设置数据库的游标是多少,可以根据数据量进行调优,也可以进行批量处理等,总之,该接口是非常关键的一个接口,包括后文中的预处理命令接口以及执行存储过程的接口。
4.1 Statement 的常用方法
下面的列表是从jdk API文档上粘贴出来的,当然很多方法我们并不是都能碰到,但是了解一下还是会有好处的
方法摘要 | |
void | |
void | cancel() |
void | clearBatch() |
void | clearWarnings() |
void | close() |
boolean | |
boolean | execute(String sql, int autoGeneratedKeys) |
boolean | execute(String sql, int[] columnIndexes) |
boolean | execute(String sql, String[] columnNames) |
int[] | executeBatch() |
executeQuery(String sql) | |
int | executeUpdate(String sql) |
int | executeUpdate(String sql, int autoGeneratedKeys) |
int | executeUpdate(String sql, int[] columnIndexes) |
int | executeUpdate(String sql, String[] columnNames) |
getConnection() | |
int | getFetchDirection() |
int | getFetchSize() |
getGeneratedKeys() | |
int | getMaxFieldSize() |
int | getMaxRows() |
boolean | getMoreResults() |
boolean | getMoreResults(int current) |
int | getQueryTimeout() |
getResultSet() | |
int | getResultSetConcurrency() |
int | getResultSetHoldability() |
int | getResultSetType() |
int | getUpdateCount() |
getWarnings() | |
void | setCursorName(String name) |
void | setEscapeProcessing(boolean enable) |
void | setFetchDirection(int direction) |
void | setFetchSize(int rows) |
void | setMaxFieldSize(int max) |
void | setMaxRows(int max) |
void | setQueryTimeout(int seconds) |
其中API 上特别注明了一句话,是非常关键的,我们在使用的时候一定要注意,否则会出现很严重的问题
在默认情况下,同一时间每个 |
其实从Statement的原理来说,底层他还是从过游标的方式操作数据,尤其是进行查询的时候,并且还是显式游标,如果对其不能进行及时的资源释放,当运行到一定时间,数据库则会抛出异常给应用(打开的游标超过了最大值)
4.2 CRUD操作
我们还是通过上述创建的user表进行一下增删改查的操作,来看看通过Statement怎样进行数据的操作。
l 新增数据
现在想到数据库中新增一条数据,编号为3,名称为lisi,生日为2010-05-05,money为13000.00,代码示例如下
@Test public void insert() throws SQLException{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1.注册驱动 //Class.forName("com.mysql.jdbc.Driver"); // 2.获取数据库连接 conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); // 3.创建执行句柄 stmt = conn.createStatement(); // 4.执行sql语句 String sql="insert into user(id,name,birthday,money) values(3,'lisi','2010-05-05',13000.00)"; stmt.executeUpdate(sql); } finally { ConnCreate.close(conn,stmt,rs); } } |
执行结果对比为(下图为,sql执行前后的查询展示)
l 修改数据
物价上涨,待遇一直不涨,所以我们将lisi的money给再加上5000元,当然这只是一个演示,意思就是说更新一下数据表中的数据,如果真的可以这么做,大家可以随时Q我,给增加工资,呵呵,好了,直接上演示代码
@Test public void update() throws SQLException{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1.注册驱动 //Class.forName("com.mysql.jdbc.Driver"); // 2.获取数据库连接 conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); // 3.创建执行句柄 stmt = conn.createStatement(); // 4.执行sql语句 String sql="update user set money=money+5000 where id=3";
stmt.executeUpdate(sql); } finally { ConnCreate.close(conn,stmt,rs); } } |
执行结果为
可以看出,lisi的money字段被更新了
l 删除数据
刚才将lisi的money给上调了,结果被领导发现了,现在要求将lisi给开除了,所以我们只好进行删除操作了(所以说敏感数据不能随便修改哒),示例代码如下
@Test public void delete() throws SQLException{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1.注册驱动 //Class.forName("com.mysql.jdbc.Driver"); // 2.获取数据库连接 conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); // 3.创建执行句柄 stmt = conn.createStatement(); // 4.执行sql语句 String sql="delete from user where id=3";
stmt.executeUpdate(sql); } finally { ConnCreate.close(conn,stmt,rs); } } |
执行结果如下图所示
从上图可以看出,lisi已经被删除掉了。
l 查询数据
前面的章节中,关于查询的示例实在太多了,此处省略。
4.3 Statement有那些缺点
应用执行sql其实大体上可以分为两步,第一步是将sql交给数据库应用检查编译,然后再由数据库将执行结果返回给应用。好了我们看一个实例,来感受一下Statement会有哪些缺点
@Test public void conditionQuery() throws SQLException{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1.注册驱动 //Class.forName("com.mysql.jdbc.Driver"); // 2.获取数据库连接 conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); // 3.创建执行句柄 stmt = conn.createStatement(); // 4.执行sql语句 String sql="select * from user where name='wangwenjun' or 1 or ''"; System.out.println(sql); rs = stmt.executeQuery(sql); // 5.处理执行结果 while (rs.next()) { System.out.println("id:" + rs.getInt(1) + "\tname:" + rs.getString(2) + "\tbirthday:" + rs.getDate(3) + "\tmoney:" + rs.getFloat(4)); } } finally { ConnCreate.close(conn,stmt,rs); } } |
请看一下我们的代码,其中name的值很诡异,假设,这个值是由前台用户传递进来的,我们就不能保证他到底是什么,当然我们也可以通过编写过滤器来进行规避,但是我们怎么样通过jdbc来规避上述的问题呢,首先看看运行结果吧
select * from user where name='wangwenjun' or 1 or ''
id:1 name:wangwenjun birthday:1984-06-09 money:8500.0
id:2 name:zhangsan birthday:2010-01-01 money:15000.0
id:3 name:lisi birthday:2010-05-05 money:18000.0
可以看到将所有的语句都查询出来了。
简单总结一下Statement的缺点哈(纯属个人观点,可能有些不太全面)
l 执行时发送sql,影响效率.
l 同样的sql,每次都要发送,不能进行有效的缓存,是一种资源的浪费.
l 示例代码中演示可以看出,为了防止恶意数据我们还需要编写附加的程序(过滤器)带来不必要的开支.
l 拼接sql字符串很容易出现错误.
为了解决上述的问题,我们引入一个新的接口PreparedStatement,该接口是Statement的子接口,他们的主要区别是,在执行sql之前首先准备好sql语句,将其中的条件通过?进行占位,还有一个好处就是,同样的sql会被PreparedStatement有效的缓存,也就是说,数据库会减少校验的过程,减少消耗,这就是我们常说的预处理命令方式,在后文中也会涉及到。
第五节 ResultSet接口的使用详解
从字面意思上来了解,该接口是一个数据集合,是从数据库中获取的数据会存放在该集合当中,该集合提供了很多种数据获取的方法,详细信息,请看下表展示
方法摘要 | |
boolean | absolute(int row) |
void | afterLast() |
void | beforeFirst() |
void | cancelRowUpdates() |
void | clearWarnings() |
void | close() |
void | deleteRow() |
int | findColumn(String columnName) |
boolean | first() |
getArray(int i) | |
getArray(String colName) | |
getAsciiStream(int columnIndex) | |
getAsciiStream(String columnName) | |
getBigDecimal(int columnIndex) | |
getBigDecimal(int columnIndex, int scale) | |
getBigDecimal(String columnName) | |
getBigDecimal(String columnName, int scale) | |
getBinaryStream(int columnIndex) | |
getBinaryStream(String columnName) | |
getBlob(int i) | |
getBlob(String colName) | |
boolean | getBoolean(int columnIndex) |
boolean | getBoolean(String columnName) |
byte | getByte(int columnIndex) |
byte | getByte(String columnName) |
byte[] | getBytes(int columnIndex) |
byte[] | getBytes(String columnName) |
getCharacterStream(int columnIndex) | |
getCharacterStream(String columnName) | |
getClob(int i) | |
getClob(String colName) | |
int | getConcurrency() |
getCursorName() | |
getDate(int columnIndex) | |
getDate(int columnIndex, Calendar cal) | |
getDate(String columnName) | |
getDate(String columnName, Calendar cal) | |
double | getDouble(int columnIndex) |
double | getDouble(String columnName) |
int | getFetchDirection() |
int | getFetchSize() |
float | getFloat(int columnIndex) |
float | getFloat(String columnName) |
int | getInt(int columnIndex) |
int | getInt(String columnName) |
long | getLong(int columnIndex) |
long | getLong(String columnName) |
getMetaData() | |
getObject(int columnIndex) | |
getObject(int i, Map<String,Class<?>> map) | |
getObject(String columnName) | |
getObject(String colName, Map<String,Class<?>> map) | |
getRef(int i) | |
getRef(String colName) | |
int | getRow() |
short | getShort(int columnIndex) |
short | getShort(String columnName) |
getStatement() | |
getString(int columnIndex) | |
getString(String columnName) | |
getTime(int columnIndex) | |
getTime(int columnIndex, Calendar cal) | |
getTime(String columnName) | |
getTime(String columnName, Calendar cal) | |
getTimestamp(int columnIndex) | |
getTimestamp(int columnIndex, Calendar cal) | |
getTimestamp(String columnName) | |
getTimestamp(String columnName, Calendar cal) | |
int | getType() |
getUnicodeStream(int columnIndex) | |
getUnicodeStream(String columnName) | |
getURL(int columnIndex) | |
getURL(String columnName) | |
getWarnings() | |
void | insertRow() |
boolean | isAfterLast() |
boolean | isBeforeFirst() |
boolean | isFirst() |
boolean | isLast() |
boolean | last() |
void | moveToCurrentRow() |
void | moveToInsertRow() |
boolean | next() |
boolean | previous() |
void | refreshRow() |
boolean | relative(int rows) |
boolean | rowDeleted() |
boolean | rowInserted() |
boolean | rowUpdated() |
void | setFetchDirection(int direction) |
void | setFetchSize(int rows) |
void | updateArray(int columnIndex, Array x) |
void | updateArray(String columnName, Array x) |
void | updateAsciiStream(int columnIndex, InputStream x, int length) |
void | updateAsciiStream(String columnName, InputStream x, int length) |
void | updateBigDecimal(int columnIndex, BigDecimal x) |
void | updateBigDecimal(String columnName, BigDecimal x) |
void | updateBinaryStream(int columnIndex, InputStream x, int length) |
void | updateBinaryStream(String columnName, InputStream x, int length) |
void | updateBlob(int columnIndex, Blob x) |
void | updateBlob(String columnName, Blob x) |
void | updateBoolean(int columnIndex, boolean x) |
void | updateBoolean(String columnName, boolean x) |
void | updateByte(int columnIndex, byte x) |
void | updateByte(String columnName, byte x) |
void | updateBytes(int columnIndex, byte[] x) |
void | updateBytes(String columnName, byte[] x) |
void | updateCharacterStream(int columnIndex, Reader x, int length) |
void | updateCharacterStream(String columnName, Reader reader, int length) |
void | updateClob(int columnIndex, Clob x) |
void | updateClob(String columnName, Clob x) |
void | updateDate(int columnIndex, Date x) |
void | updateDate(String columnName, Date x) |
void | updateDouble(int columnIndex, double x) |
void | updateDouble(String columnName, double x) |
void | updateFloat(int columnIndex, float x) |
void | updateFloat(String columnName, float x) |
void | updateInt(int columnIndex, int x) |
void | |
void | updateLong(int columnIndex, long x) |
void | updateLong(String columnName, long x) |
void | updateNull(int columnIndex) |
void | updateNull(String columnName) |
void | updateObject(int columnIndex, Object x) |
void | updateObject(int columnIndex, Object x, int scale) |
void | updateObject(String columnName, Object x) |
void | updateObject(String columnName, Object x, int scale) |
void | |
void | |
void | updateRow() |
void | updateShort(int columnIndex, short x) |
void | updateShort(String columnName, short x) |
void | updateString(int columnIndex, String x) |
void | updateString(String columnName, String x) |
void | updateTime(int columnIndex, Time x) |
void | updateTime(String columnName, Time x) |
void | updateTimestamp(int columnIndex, Timestamp x) |
void | updateTimestamp(String columnName, Timestamp x) |
boolean | wasNull() |
有兴趣的读者可以逐个方法进厅junit测试,接口封装的很强大,所以使用起来很方便,几乎没有什么看不懂的地方,就不做其他罗嗦的讲述了。
第六节 JDBC 中数据类型详解
Jdbc中提供了我们能见到的所有数据类型,其中想String,int等,看一下PreparedStatement源码就可以查看得到,这里就不在讲述
6.1 基本数据类型
void | addBatch() |
void | clearParameters() |
boolean | execute() |
executeQuery() | |
int | executeUpdate() |
getMetaData() | |
getParameterMetaData() | |
void | setArray(int parameterIndex, Array x) |
void | setAsciiStream(int parameterIndex, InputStream x) |
void | setAsciiStream(int parameterIndex, InputStream x, int length) |
void | setAsciiStream(int parameterIndex, InputStream x, long length) |
void | setBigDecimal(int parameterIndex, BigDecimal x) |
void | setBinaryStream(int parameterIndex, InputStream x) |
void | setBinaryStream(int parameterIndex, InputStream x, int length) |
void | setBinaryStream(int parameterIndex, InputStream x, long length) |
void | setBlob(int parameterIndex, Blob x) |
void | setBlob(int parameterIndex, InputStream inputStream) |
void | setBlob(int parameterIndex, InputStream inputStream, long length) |
void | setBoolean(int parameterIndex, boolean x) |
void | setByte(int parameterIndex, byte x) |
void | setBytes(int parameterIndex, byte[] x) |
void | setCharacterStream(int parameterIndex, Reader reader) |
void | setCharacterStream(int parameterIndex, Reader reader, int length) |
void | setCharacterStream(int parameterIndex, Reader reader, long length) |
void | setClob(int parameterIndex, Clob x) |
void | setClob(int parameterIndex, Reader reader) |
void | setClob(int parameterIndex, Reader reader, long length) |
void | setDate(int parameterIndex, Date x) |
void | setDate(int parameterIndex, Date x, Calendar cal) |
void | setDouble(int parameterIndex, double x) |
void | setFloat(int parameterIndex, float x) |
void | setInt(int parameterIndex, int x) |
void | setLong(int parameterIndex, long x) |
void | setNCharacterStream(int parameterIndex, Reader value) |
void | setNCharacterStream(int parameterIndex, Reader value, long length) |
void | setNClob(int parameterIndex, NClob value) |
void | setNClob(int parameterIndex, Reader reader) |
void | setNClob(int parameterIndex, Reader reader, long length) |
void | setNString(int parameterIndex, String value) |
void | setNull(int parameterIndex, int sqlType) |
void | setNull(int parameterIndex, int sqlType, String typeName) |
void | |
void | setObject(int parameterIndex, Object x, int targetSqlType) |
void | setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) |
void | setRef(int parameterIndex, Ref x) |
void | setRowId(int parameterIndex, RowId x) |
void | setShort(int parameterIndex, short x) |
void | setSQLXML(int parameterIndex, SQLXML xmlObject) |
void | setString(int parameterIndex, String x) |
void | setTime(int parameterIndex, Time x) |
void | setTime(int parameterIndex, Time x, Calendar cal) |
void | setTimestamp(int parameterIndex, Timestamp x) |
void | setTimestamp(int parameterIndex, Timestamp x, Calendar cal) |
void | setUnicodeStream(int parameterIndex, InputStream x, int length) |
void | setURL(int parameterIndex, URL x) |
上述文件中存在很多个数据类型的set和get方法,有时间了可以仔细阅读研究一下。在本文中不再进行描述。
其中如果不知道数据表中数据类型或者不能确定是什么类型的情况下,可以直接使用setObject方法和getObject方法进行获取和设置。
6.2 日期类型
其中日期类型是一个比较特殊的数据类型,我们看一下getDate()方法
l getDate
Date getDate(int columnIndex)
throws SQLException以 Java 编程语言中 java.sql.Date 对象的形式获取此 ResultSet 对象的当前行中指定列的值。
参数:
columnIndex - 第一个列是 1,第二个列是 2,……
返回:
列值;如果值为 SQL NULL,则返回值为 null
抛出:
SQLException - 如果 columnIndex 无效;如果发生数据库访问错误或在已关闭的结果集上调用此方法
其中返回值Date类型是java.sql.Date类型,希望使用的时候不要弄混淆。
l Java.sql.Date是java.util.Date的子类
现在我们做一个简单的测试,来看看两者的区别
@Test public void compareDate(){ java.util.Date date1 = new java.util.Date(); System.out.println(date1); System.out.println("====================="); java.sql.Date date2 = new java.sql.Date(3434534); System.out.println(date2); } |
执行结果为:
Mon May 30 13:39:20 CST 2011
=====================
1970-01-01
其中可以看出,sql中的date是没有时间的,而util中的date是存在时间的,其次两者的差距是toString()方法中,当然这并不是多么重要的差距,再做一个测试来看一下,我们保存一个数据到数据库中,看是否能够将时间也保存进去呢
String sql = "insert into user(id,name,birthday,money) values(4,'lisi',?,13000.00)"; stmt = conn.prepareStatement(sql); stmt.setDate(1, new Date(System.currentTimeMillis())); stmt.execute(); |
执行完毕之后,我们查询数据库看到如下的结果
可以看到,时间还是没有被保存进去,如果我们要解决该问题应该如何处理呢,再来看一个函数
l setTimestamp
void setTimestamp(int parameterIndex,
Timestamp x)
throws SQLException
将指定参数设置为给定 java.sql.Timestamp
值。在将此值发送到数据库时,驱动程序将它转换成一个 SQL TIMESTAMP
值。
参数:
parameterIndex
- 第一个参数是 1,第二个参数是 2,……
x
- 参数值
抛出:
SQLException
- 如果 parameterIndex 不对应于 SQL 语句中的参数标记;如果发生数据库访问错误,或者在关闭的 PreparedStatement
上调用此方法
使用该函数就可以将日期时间一并保存在数据库当中,我们再次修改一下刚才的示例来演示一下
@Test public void insertTimeStamp() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into user(id,name,birthday,money) values(5,'zhaowu',?,13000.00)"; stmt = conn.prepareStatement(sql); stmt.setTimestamp(1, new Timestamp(System.currentTimeMillis())); stmt.execute(); } finally { ConnCreate.close(conn, stmt, rs); } } |
执行成功之后,可以看到日期保存时,时间也跟着保存了进去,读者可以自己进行试验,这里就不再演示。
l 日期类型的读取
如果想要读取完整的日期,包括时间,不要使用getDate()方法,否则后面的时间会被舍弃掉的,及时转换成java.util.Date后面的时间仍然会是0,这就要看具体需要完成什么,再来决定使用上述的哪几种方法。
6.3 CLOB类型
当我们存放大量的文本信息时,数据库中的varchar或者varchar2肯定是不能满足的,varchar2好像最多只能有4000个长度,存放一篇很长的文章或者一个文本信息,肯定是无法满足我们的需求,现在的大多数数据库都支持了CLOB的类型用于满足我们的需求,一般来说它所提供能容量肯定能够满足我们存放文本信息,当然jdbc也对其提供了相应的API文档支持。
l 创建数据表
create table clob_test(id integer primary key ,info text); |
接下来我们来演示一下,将一个文本文件存放在该字段中,并且再将存放的信息查询出来。
l 保存CLOB数据
package com.wangwenjun.jdbc;
import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;
import org.junit.Test;
public class ClobTest {
@Test public void insertText() throws SQLException, FileNotFoundException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into clob_test(id,info) values(?,?)"; stmt = conn.prepareStatement(sql); stmt.setInt(1, 3); File file = new File("E:\\execise\\jdbc\\jdbc1\\src\\com\\wangwenjun\\jdbc\\ClobTest.java"); FileReader reader = new FileReader(file); BufferedReader buffer = new BufferedReader(reader); stmt.setCharacterStream(2,buffer,3); stmt.execute(); } finally { ConnCreate.close(conn, stmt, rs); } } }
|
我们将运行的类作为文本文件给进行了保存,查看数据库,确实也进行了保存,为了能更加直观期间,我们通过CLOB的读取方式进行查看
l 查询CLOB数据
再上一个小节中,我们将一个文本文件保存在了数据库中,能存就能取,为了能说明问题,我们做一个小例子,将刚才的保存信息再给获取出来,在控制台上打印一份,并且保存在一个txt文件中。
package com.wangwenjun.jdbc;
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;
import org.junit.Test;
public class ClobTest {
@Test public void insertText() throws SQLException, FileNotFoundException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into clob_test(id,info) values(?,?)"; stmt = conn.prepareStatement(sql); stmt.setInt(1, 3); File file = new File("E:\\execise\\jdbc\\jdbc1\\src\\com\\wangwenjun\\jdbc\\ClobTest.java"); FileReader reader = new FileReader(file); BufferedReader buffer = new BufferedReader(reader); stmt.setCharacterStream(2,buffer,3); stmt.execute(); } finally { ConnCreate.close(conn, stmt, rs); } }
@Test public void queryText() throws SQLException, IOException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "select info from clob_test where id=3"; stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); if(rs.next()){ Reader r=rs.getCharacterStream(1); BufferedReader buffer = new BufferedReader(r); String temp = ""; File f = new File("d:\\reader.txt"); FileWriter fw = new FileWriter(f); BufferedWriter bw = new BufferedWriter(fw); while((temp=buffer.readLine())!=null){ System.out.println(temp); bw.write(temp+"\n"); bw.flush(); } bw.close(); fw.close(); buffer.close(); } } finally { ConnCreate.close(conn, stmt, rs); } } }
|
查看控制台可以看到和上面代码一模一样的输出,并且在D盘的根目录下有一个reader.txt的文件,其中内容也和上面的代码一模一样,读者可以亲自测试一下。
6.4 BLOB类型
BLOB类型是专门针对二进制文件进行的存取,比如图片,音频,等信息,当然了现在主流的数据库均会对其进行支持的,jdbc API同样也对其进行了支持,下面我们通过一个示例来进行演示
l 创建数据表
Create table blob_test(id integer primary key ,info blob); |
l 保存二进制文件至数据库
@Test public void insertBlob() throws SQLException, IOException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into blob_test(id,info) values(?,?)"; stmt = conn.prepareStatement(sql); stmt.setInt(1,3); File file = new File("C:\\Users\\Administrator\\Pictures\\Google Talk\\Alien 1.bmp"); FileInputStream fis = new FileInputStream(file); stmt.setBinaryStream(2, fis, (int)file.length()); stmt.execute(); fis.close(); } finally { ConnCreate.close(conn, stmt, rs); } } |
程序运行正常,并且查看数据库,数据库中存放了图片的信息。由于通过sql语句无法看到图片的信息,所以我们通过读取的方式展示一下。
l 读取二进制文件从数据库
@Test public void queryBlob() throws SQLException, IOException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "select info from blob_test where id=3"; stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); if(rs.next()){ InputStream is=rs.getBinaryStream(1); File file = new File("d:\\a.bmp"); OutputStream os = new FileOutputStream(file); int len = 0; byte[] buffers = new byte[1024]; while((len=is.read(buffers))>0){ os.write(buffers, 0, len); } os.flush(); os.close(); is.close(); } } finally { ConnCreate.close(conn, stmt, rs); } } |
程序运行结束之后,我们可以在D盘的根目录下面看到一个名字叫a.bmp的文件,阅读者可以自己运行示例中的代码。
6.5 其他数据类型
其他数据类型可以查看数据库和jdbc相关的文档,或者在java.sql.Types文件中可以查看到所涉及的所有类型信息;
第七节 DAO设计模式详解
7.1 实际项目中如何使用JDBC
实际项目中,我们经常会通过查询获取数据,然后将数据用于其他的用途,并非简单的打印或者展示,其次,在实际的应用中,和数据库打交道的jdbc代码会很少出现在业务逻辑之中,因为这样对代码的维护以及再扩展会带来极大的开销,所以会尽量的将数据层的东西抽取出来,让代码显得干净一些,这也就是DAO模式最需要解决的问题之一,当然DAO模式最大的好处是通过数据访问层的接口完全隐藏了数据层的实现细节,让业务层不需要关心具体的细节。
7.2 DAO设计模式简介
DAO的全称是data access object ,其中非常重要的一个概念是Domain对象,也就是说一个最常用的POJO对应数据库中的一个表与之对应,如下图所示,展示了DAO所处的位置和所起的作用,图画的有些难看,但是能够说明问题
7.3 DAO设计模式的实现
首先我们需要设计一个domain对象,还记得我们之前的那张数据表么?就是User表,我们针对这个数据表来设计一个domain对象,代码如下
package com.wangwenjun.jdbc;
import java.sql.Date;
public class User { private int id; private Date birthday; private String name; private float money; public int getId() { return id; } public void setId(int id) { this.id = id; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public String getName() { return name; } public void setName(String name) { this.name = name; } public float getMoney() { return money; } public void setMoney(float money) { this.money = money; } } |
好了我们的domain已经编写好了,可以看出,其中包括了数据表中的所有字段,并且提供了set和get方法,接下来我们需要定义一个DAO接口来供应用程序使用
package com.wangwenjun.jdbc.dao;
import java.util.List;
import com.wangwenjun.jdbc.User;
public interface UserDao {
/** * 增加一个用户 * @param user * @return */ public int addUser(User user);
/** * 删除一个用户信息 * @param user * @return */ public int deleteUser(User user);
/** * 查询用户信息通过id编号 * @param id * @return */ public User queryUserById(int id);
/** * 查询用户列表通过用户名 * @param name * @return */ public List<User> queryUserForList(String name);
/** * 更新用户信息,根据id编号来更新 * @param newInfo * @param id * @return */ public int updateUser(User newInfo,int id); }
|
一个简单的DAO接口的实现,代码如下
package com.wangwenjun.jdbc.dao;
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List;
import com.wangwenjun.jdbc.ConnCreate; import com.wangwenjun.jdbc.User;
public class UserDaoImpl implements UserDao {
private Connection conn = null;
public UserDaoImpl(Connection conn){ this.conn = conn; }
public int addUser(User user) { PreparedStatement stmt = null; int result = 0; try { String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)"; stmt = conn.prepareStatement(sql); stmt.setInt(1, user.getId()); stmt.setString(2, user.getName()); stmt.setDate(3, user.getBirthday()); stmt.setFloat(4, user.getMoney()); result=stmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { try { ConnCreate.close(null, stmt, null); } catch (SQLException e) { e.printStackTrace(); } } return result; }
public int deleteUser(User user) { PreparedStatement stmt = null; int result = 0; try { String sql = "delete from user where id=?"; stmt = conn.prepareStatement(sql); stmt.setInt(1, user.getId()); result=stmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { try { ConnCreate.close(null, stmt, null); } catch (SQLException e) { e.printStackTrace(); } } return result; }
public User queryUserById(int id) { PreparedStatement stmt = null; ResultSet rs = null; User user =null; try { String sql = "select * from user where id=?"; stmt = conn.prepareStatement(sql); stmt.setInt(1, id); rs = stmt.executeQuery(); if(rs.next()){ user = new User(); user.setId(rs.getInt(1)); user.setName(rs.getString(2)); user.setBirthday(rs.getDate(3)); user.setMoney(rs.getFloat(4)); } } catch (SQLException e) { e.printStackTrace(); } finally { try { ConnCreate.close(null, stmt, rs); } catch (SQLException e) { e.printStackTrace(); } } return user; }
public List<User> queryUserForList(String name) { PreparedStatement stmt = null; ResultSet rs = null; User user =null; List<User> lists = new ArrayList<User>(); try { String sql = "select * from user where name=?"; stmt = conn.prepareStatement(sql); stmt.setString(1, name); rs = stmt.executeQuery(); while(rs.next()){ user = new User(); user.setId(rs.getInt(1)); user.setName(rs.getString(2)); user.setBirthday(rs.getDate(3)); user.setMoney(rs.getFloat(4)); lists.add(user); } } catch (SQLException e) { e.printStackTrace(); } finally { try { ConnCreate.close(null, stmt, rs); } catch (SQLException e) { e.printStackTrace(); } } return lists; }
public int updateUser(User newInfo, int id) { PreparedStatement stmt = null; int result = 0; try { String sql = "update user set money=? where id=?"; stmt = conn.prepareStatement(sql); stmt.setString(1, newInfo.getName()); stmt.setInt(2, id); result=stmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { try { ConnCreate.close(null, stmt, null); } catch (SQLException e) { e.printStackTrace(); } } return result; } } |
最后我们来做一个简单的测试,对DAO中所涉及的接口逐个进行测试,来看看最后的执行效果
package com.wangwenjun.jdbc;
import java.sql.Connection; import java.sql.Date;
import org.junit.Test;
import com.wangwenjun.jdbc.dao.UserDao; import com.wangwenjun.jdbc.dao.UserDaoImpl;
public class UserDaoTest {
@Test public void addUser(){ Connection conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); UserDao dao = new UserDaoImpl(conn); User user = new User(); user.setId(7); user.setName("liudehua"); user.setBirthday(new Date(System.currentTimeMillis())); user.setMoney(456456); int result=dao.addUser(user); System.out.println("信息录入"+(result>0?"成功!":"失败!")); }
}
|
程序执行成功,信息也正确插入
信息录入成功!
查看数据库中的信息如下所示
可以看到liudehua的信息已经正确的录取了并且在数据库中存放了。
剩余的接口我就不再进行测试了,读者可以自己进行测试,总之通过这样的更改之后我们不难发现,在业务层的确精炼了许多,数据库操作的信息完全被隐藏了起来,如果我们要更改其中的细节,业务逻辑相关的代码则无需进行更改。如果想要增加新的接口,可以继续进行继承或者在原有接口基础之上进行更改,应用照样是不需要进行替换。
至此,我们实现了一个简单的Dao模式,可以看出,我们的代码显的越来越精简,并且可读性也越来越好,在下节中,将会结合工厂模式对DAO模式进行一下整合,进一步来完善程序的设计。
7.4 DAO设计模式与工厂模式的整合
在上一小节中设计的DAO模式还是存在一些问题,比如说当执行时出现异常,我们只是简单的打印了一下堆栈的信息,并没有通知上一层使用者或者展示层,来表示现在出现的错误,并且也没有想办法进行相应的容错,当然,如果sql语句出现错误,或者逐渐冲突,容错是非常难做的,所以一般情况下数据库执行sql语句的错误只需要通过异常的传递机制告知上一层使用者即可。
当然我们的异常可以直接使用SQLException这样的异常,但是会发现我们的应用还是和JDBC产生了关系,也就是产生了依赖,这样对分层是极大不利的。
那么我们自定义一个异常,该异常用于通知使用者或者业务逻辑来知道数据库操作时出现了错误,首先自定义一个异常,代码如下
package com.wangwenjun.jdbc.exception;
public class JDBCException extends Exception {
private static final long serialVersionUID = -914212712828534650L;
private String message="";
private Throwable throwable;
public JDBCException(){ super(); }
public JDBCException(String message){ super(message); this.message = message; }
public JDBCException(String message,Throwable throwable){ this(message); this.throwable = throwable; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Throwable getThrowable() { return throwable; }
public void setThrowable(Throwable throwable) { this.throwable = throwable; }
}
|
l 修改一下DAO接口
/** * 增加一个用户 * @param user * @return */ public int addUser(User user) throws JDBCException; ….其余的省略 |
l 修改一下DAO的实现类
public int addUser(User user) throws JDBCException { PreparedStatement stmt = null; int result = 0; try { String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)"; stmt = conn.prepareStatement(sql); stmt.setInt(1, user.getId()); stmt.setString(2, user.getName()); stmt.setDate(3, user.getBirthday()); stmt.setFloat(4, user.getMoney()); result=stmt.executeUpdate(); } catch (SQLException e) { throw new JDBCException("新增用户信息时出现那异常!", e); } finally { try { ConnCreate.close(null, stmt, null); } catch (SQLException e) { throw new JDBCException("释放资源时出现异常!", e); } } return result; } ….其余的省略 |
再次执行上述的测试代码出现了逐渐冲突,打印信息如下所示
录入信息时出现异常Duplicate entry '7' for key 1
信息录入失败!
很明显的在告知应用,7这个id已经存在了,不允许重复!如果我们做页面的话可以通过很友好的方式告知用户,然后记录错误信息,方便更改查询,而并不是出现一堆堆栈信息,自己不好查找原因,用户也不知道是什么东西。
虽然通过DAO模式将数据库的操作细节隐藏起来了,业务逻辑的处理虽然也是依赖接口但是同时也依赖了接口的实现。
并且随着多个DAO的产生,我们在使用起来就显得比较凌乱,所以试想一下,有没有一个创建DAO的工厂呢?他只产生相关DAO的接口,可以是UserDAO,也可是其他DAO的生产者。
DAOFactory.java
package com.wangwenjun.jdbc.dao;
public interface DAOFactory {
public UserDao createUserDao(); }
|
SimpleDAOFactory.java
package com.wangwenjun.jdbc.dao;
import java.sql.Connection;
import com.wangwenjun.jdbc.ConnCreate;
public class SimpleDAOFactoty implements DAOFactory{
public UserDao createUserDao() { Connection conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t");//当然数据库连接的获取通过这样的方式是不太合理的,在后文中将会讲解通过怎样的方式规避!
UserDao dao = new UserDaoImpl(conn); return dao; } }
|
修改一下之前的业务逻辑层
@Test public void addUser(){ DAOFactory daoFactory = new SimpleDAOFactoty(); UserDao dao = daoFactory.createUserDao(); User user = new User(); user.setId(7); user.setName("liudehua"); user.setBirthday(new Date(System.currentTimeMillis())); user.setMoney(456456); int result = 0; try { result = dao.addUser(user); } catch (JDBCException e) { System.out.println("录入信息时出现异常"+e.getThrowable().getMessage()); } System.out.println("信息录入"+(result>0?"成功!":"失败!")); } |
这样一来,业务逻辑的所有行为只会和数据层相关的接口打交道,实现了真正的面向接口编程,更换DAO的实现只需要修改工厂或者新增工厂中的接口即可。
7.5 DAO设计模式测试
相关的测试代码已经在7.3和7.4中有所演示,所以在本节中将不再进行赘述。
第八节 JDBC对事务的支持
数据库的事务是保证数据完整性的一种机制,简而言之,就是怎样确保数据的执行过程要么都成功,要么都失败,举例子为假设你要给银行还款,需要从你的银行卡中扣除相关的金额也需要在你的信用卡上加钱,这个流程务必是一个完整的流程,不能拆分,如果从你的银行卡中扣除了钱,但是加钱的流程是失败的,这个时候用户是吃亏的,反之银行则会亏本,所以这就涉及到了事务的机制。
l 原子性(atomicity):组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分。
l 一致性(consistency):在事务处理执行前后,数据库是一致的(两个账户要么都变,或者都不变)。
l 隔离性(isolcation):一个事务处理对另一个事务处理没有影响。
l 持续性(durability):事务处理的效果能够被永久保存下来 。
概括起来可以用ACID来表述
8.1 模拟转账
还记得之前的那张数据表user么,记得有一个字段叫做money,假设有这样一个需求,首先呢将wangwenjun用户的钱转给liudehua 1000元,然后判断liudehua的账户余额是否大于10000,如果大于10000则抛出一个异常(假设此时系统出现了故障),我们来看看,wangwenjun的钱是不是会减少,在做这样的操作之前我们先查看一下他们两个此时的信息
执行sql语句select * from user where name in('wangwenjun','liudehua');
现在来编写程序测试一下,刚才我们的需求,代码示例如下
@Test public void transcation1() throws Exception{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); stmt = conn.createStatement(); String sql = "update user set money=money-1000 where id=1";//wangwenjun的钱减少1000元 stmt.execute(sql); sql="select money from user where id=7"; rs = stmt.executeQuery(sql); float money = 0; if(rs.next()){ money = rs.getFloat(1); if(money>10000){ throw new Exception("钱已到上线,无法在增加"); } } sql = "update user set money=money+1000 where id=7"; stmt.execute(sql); } finally { ConnCreate.close(conn, stmt, rs); } } |
程序像我们当初设想的那样抛出了异常,这个时候我们再来查看一下wangwenjun用户的钱是否被减少了呢,继续执行sql语句
可以看到wangwenjun的钱已经变为7500元,这是系统的一个很严重的bug,也是wangwenjun不愿意看到的,上面的问题为什么会发生呢?下面的几个章节将会进行讲解
8.2 jdbc默认事务
Jdbc的事务默认是打开的,也就是说执行每一步操作的话,事务都会隐式的进行提交,在抛出异常之前,我们的更改操作已经同步到了数据库中去,此时只能看到wangwenjun减少了钱,却不能看到liudehua加了钱。
我们应该如何将jdbc的事务打开呢,执行下面的语句即可
connection.setAutoCommit(false);
8.3 事务提交与回滚
为了保证我们的程序能够正确的按照我们的意图来进行,现在修改一下代码,将代码中涉及的三个执行语句放在一个事务当中,统一进行提交或者回滚,修改后的代码如下
@Test public void transcation2() throws Exception{ Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); conn.setAutoCommit(false); stmt = conn.createStatement(); String sql = "update user set money=money-1000 where id=1";//wangwenjun的钱减少1000元 stmt.execute(sql); sql="select money from user where id=7"; rs = stmt.executeQuery(sql); float money = 0; if(rs.next()){ money = rs.getFloat(1); if(money>10000){ throw new Exception("钱已到上线,无法在增加"); } } sql = "update user set money=money+1000 where id=7"; stmt.execute(sql); } finally { ConnCreate.close(conn, stmt, rs); } conn.commit(); } |
在执行代码之前先来查看一下结果
执行代码,让异常发生,再来看看效果,同样我们的异常还是像预先设定的那样如约而至,为了明显期间,我们将运行了程序之后的查询粘贴一下,方便做对比。
可以看到,wangwenjun的money没有发生任何的变化,原因是因为我们将所有的sql操作放在了同一个事务之中,通过上端代码的演示,相信读者可以明显的看到,jdbc对事物的控制有两个函数来控制
l connection.commit();//提交事务。
l connection.rollback();//回滚事务。
8.4 设置保存点
如果我们的数据操作中涉及多个数据库操作,其中当出现问题的时候,并非需要将所有的操作都进行回滚,可能其中的某些细节是没有必要回滚的,此时,单纯的用提交或者回滚未免会显得有些武断,我们还可以通过设置保存点的方式来设置我们想要将事务回滚到某个点。
我们可以通过SavePoint API来解决上面的问题
可以在sql的执行过程中来设置SavePoint,然后再需要混滚的地方调用rollback的重载方法
Conn.rollback(SavePoint point);来进行回滚。
个人觉得一个业务流程不应该有太多的数据操作,如果关系非常密切的数据库操作,尽量让其事务保持一致,这样不至于破坏数据的一致性和原子性,所以我觉得使用设置保存点的程序是一种不良设计,只需要知道有这样的一个功能,也就不再对其进行演示。
8.5 JTA事务的介绍
随着分布式系统的普及,我们的数据库也可能是集群形式的,有时候一个应用程序需要操作两个或者两个以上的数据库,这个时候数据的事务控制将会是一个更加复杂和严峻的问题,这就是通常所说的跨库事务,要求几个数据库的事务在一个应用中保持一致,JTA就是为了解决这个问题而诞生的,在该文档中将不再进行介绍,如果有兴趣的读者可以自行研究或者留意作者后期推出的JTA学习总结我们一起学习,一起进步,说好了!不见不散哦。
8.6 数据库的隔离级别介绍
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(Read uncommitted) | V | V | V |
读已提交(Read committed) | x | V | V |
可重复读(Repeatable read) | x | x | V |
可串行化(Serializable ) | x | x | x |
l V:可能出现,X:不会出现
隔离级别的作用是能够保证多个线程同时操作一条数据时的正确性,它是一个非常重要的概念,也是一个不容易理解透的概念,希望阅读者能够掌握。
l 读未提交(Read uncommitted)
他是隔离性最差的一个级别,简单理解就是,当你修改了或者增加了一个数据,还没有进行提交,别人就可以看到,这个时候就会产生脏读,可以重复读,幻读了。
l 读已提交(Read committed)
他的隔离级别就相比之前的来说稍微高一些,意思就是说当一个线程的操作事务没有提交,别人是无法读取的得到的,但是他还是不能解决不可重复读和幻读。
l 可重复读(Repeatable read)
隔离级别更高了,它是mysql的默认隔离级别,它能保证不会读到其他线程未提交的数据,也能保证每次读到的数据是一样的,但是不能避免幻读的问题。
l 可串行化(Serializable )
这是级别最高的隔离级别,它可以避免脏读,不可重复读,幻读的问题。
但是隔离级别越高,对数据正确性的保证会越好,但同时牺牲很多的数据库性能,并发性会越差,具体的调整需要根据自己的需要进行,建议不要设置为可串行化与读未提交这样的级别,当然各个数据库提供对隔离级别的支持是不一样的,有些数据库压根就没有实现和支持相应的隔离级别。
l 隔离级别实验演示
为了能更好的理解上述的描述,我们在mysql数据库中进行一下隔离级别的测试,现在打开两个mysql客户端程序
1. 查询mysql隔离级别的方法,查询全局变量
select @@tx_isolation;
2. 设置隔离级别为未提交读
set transaction isolation level read uncommitted;
3. 启动事务
start transaction;
前期的准备已经就绪了,现在就开始测试一下我们的结果
8.6.1 未提交读
Ø 窗口一的演示
Ø 窗口二的演示
可以看到第二个客户端读到了未提交的数据,产生了脏读,
当第一个窗口将事务回滚,第二个独到的数据又变回去了,所以产生了不可重复读这样;
同样在第一个窗口中插入一条数据,第二个窗口也是可以看到的,所以就产生了幻读;
8.6.2 提交读
Ø 窗口一的演示
Ø 窗口二的演示
通过上图的对比,发现窗口一没有提交,读到的数据则是不一样的,避免了脏读的问题,但是当第一个窗口将事务提交,第二个窗口读到的数据又会是新的,所以没有避免不可重复读,幻读同样存在。
8.6.3 重复读
Ø 窗口一演示
Ø 窗口二演示
可以看到非但没有读到未提交的数据,就连其他程序提交事务之后的数据也没有读到,这样不仅避免了脏读,也支持了不可重复读,但是幻读同样未解决。
8.6.4 序列化读
Ø 窗口一演示
Ø 窗口二演示
序列化级别的数据不仅能解决幻读,还能保证重复读取数据,并且能将幻影数据过滤掉,我们在第一个窗口中插入了数据,并且进行了提交,在第二个窗口中都没有的到相应的数据。
8.7 小结
个人认为,这一个章节是我写的最好的一部分,因为在编写该章节中让我加深了对事务的理解,并且学习到了不少的东西,总结的也很认真,其中对隔离级别的实验让我加深了对数据库事务的更进一步认识,还有个人觉得设置保存点应该尽量避免使用。
第九节 PreparedStatement接口的使用
其实通过前面几个章节的介绍,大部分代码都涉及到了PreparedStatement接口的用法,详细的信息就不用过多介绍,只说几点总结
Ø PreparedStatement是Statement的子类;
Ø PreparedStatement是一个预处理命令的Statement实现;
Ø 在数据库操作中PreparedStatement会带来很大的方便,减少拼写sql字符串带来的麻烦;
Ø 具体的函数接口查看API文档的介绍;
第十节 CallableStatement接口的使用
写到这里,就会想JDBC如此之强大,它能否调用数据库中的存储过程么?当然了,如此之强大的JDBC,肯定能满足我们这样的需求,在本节中将会介绍JDBC如何调用数据库存储过程。
在本节中,将会通过三个方面介绍CallableStatement的使用
Ø 调用存储过程(无参数,无返回值)
Ø 调用存储过程(有参数,无返回值)
Ø 调用存储过程(有参数,有返回值)
9.1 无参无返回值存储过程调用
首先我们创建一个存储过程,创建脚本如下
create or replace procedure test1 as begin insert into user values(11,'hi',now(),1000); commit; end test1; |
是一个非常简单的存储过程,只需要在user表中新增一条数据即可,下面我们来使用JDBC来执行存储过程
@Test public void callProcedureWithOutParamWithOutResult() throws SQLException{ Connection conn = null; CallableStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "{call test1()}"; stmt = conn.prepareCall(sql); stmt.executeUpdate(); } finally { ConnCreate.close(conn, stmt, rs); } } |
执行完毕之后会在数据库中查看新增了一条记录;
9.2 有参无返回值存储过程调用
修改存储过程,将存储过程变为如下
create or replace procedure test1(in id integer,in name varchar(20),in money float) as begin insert into user values(id,name,now(),money); commit; end test1; |
可以看到在该存储过程中新增加了几个入参,这样我们可以通过使用callablestatement所提供的set方法对其进行赋值,代码演示如下
@Test public void callProcedureWithParamWithOutResult() throws SQLException{ Connection conn = null; CallableStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "{call test1(?,?,?)}"; stmt = conn.prepareCall(sql); stmt.setInt(1, 17); stmt.setString(2, "test"); stmt.setFloat(3, 6000); stmt.executeUpdate(); } finally { ConnCreate.close(conn, stmt, rs); } } |
执行之后,我们的数据库中又增加了一条记录
9.3 有参有返回值存储过程调用
再次修改存储过程,让其支持值返回
create or replace procedure test1(in id integer,in name varchar(20),in money float,out counter integer) as begin insert into user values(id,name,now(),money); select count(1) into counter from user; commit; end test1; |
编写测试代码,在需要将值返回时,需要对其进行注册,代码演示如下
@Test public void callProcedureWithParamWithResult() throws SQLException{ Connection conn = null; CallableStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "{call test1(?,?,?,?)}"; stmt = conn.prepareCall(sql); stmt.setInt(1, 17); stmt.setString(2, "test"); stmt.setFloat(3, 6000); stmt.registerOutParameter(1, Types.INTEGER); stmt.executeUpdate(); int counter = stmt.getInt(4); System.out.println(counter); } finally { ConnCreate.close(conn, stmt, rs); } } |
至此,关于存储过程的调用已经ok了,接口也比较简单,熟悉Statement或者PreparedStatement熟悉起来就非常简单,复杂的地方就是存储过程的编写,在笔者的后期札记中会详细的介绍,到时候希望关注。
9.4 JDBC其他API
假设,我们的表中有一个自动生成的主键,我们插入了一条数据,但是此时还想要将该主键的数值获取出来,这个时候怎么做呢?在查询一次么?不用了,JDBC API已经提供了这样的方法可以方便我们的使用,代码演示如下
@Test public void testQueryPK() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into user values(?,?,?,?)"; stmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS); stmt.setInt(1, 999); stmt.setString(2, "test2"); stmt.setDate(3, new Date(System.currentTimeMillis())); stmt.setFloat(4, 5000); stmt.executeUpdate(); rs = stmt.getGeneratedKeys(); if(rs.next()) System.out.println(rs.getInt(1)); } finally { ConnCreate.close(conn, stmt, rs); } } |
第十一节 元数据信息
元数据信息就是获取相关的信息参数,大致可以分为两个
Ø 数据库元数据信息;
Ø 参数元数据信息;
在本节中将会围绕着这两点进行讲解
11.1 数据库元数据信息
就是获取数据库的相关信息资源,这个是比较简单的,我们通过一个简单的小例子讲解一下即可
@Test public void connectionMeta() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); DatabaseMetaData dataBaseMeta=conn.getMetaData(); System.out.println("主版本:"+dataBaseMeta.getDatabaseMajorVersion()); System.out.println("次版本:"+dataBaseMeta.getDatabaseMinorVersion()); System.out.println("驱动名称:"+dataBaseMeta.getDriverName()); System.out.println("默认隔离级别:"+dataBaseMeta.getDefaultTransactionIsolation()); } finally { ConnCreate.close(conn, stmt, null); } } |
执行结果为:
主版本:5
次版本:0
驱动名称:MySQL-AB JDBC Driver
隔离级别:2
还有很多参数,读者可以自己进行尝试。
11.2 参数元数据信息
这是一个非常有用的API ,我们在讲解PreparedStatement接口的时候,会经常参入一些参数,参数元数据信息的作用就是获取参数的一些信息,我们通过一个示例来看一下怎样获取和使用
假设有这样一个需求,我们需要执行一个占位符这样的sql,我们在执行之前要看看,参数在数据库中的名字,类型,长度等信息
@Test public void paramMeta() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into user values(?,?,?,?)"; stmt = conn.prepareStatement(sql); ParameterMetaData meta=stmt.getParameterMetaData(); int count = meta.getParameterCount(); for(int i = 1;i<=count;i++){ System.out.print("ParameterClassName"+meta.getParameterClassName(i)+"\nParameterType"+meta.getParameterType(i)+"\nParameterTypeName"+meta.getParameterTypeName(i)+"\n"); } } finally { ConnCreate.close(conn, stmt, null); } } |
第十二节 批处理的使用
在本节中,我们来看看,JDBC对批处理的操作,首先简单说一下JDBC操作sql语句的简单机制。
JDBC执行数据库操作语句,首先需要将sql语句打包成为网络字节流,传递给数据库,数据库经过解包,然后编译sql语句,最后执行,然后将结果通过字节流的形式返回给JDBC API,简单的来说大致分为以下几点:
Ø JDBC打包sql语句;
Ø 发送字节流至数据库;
Ø 数据库解包;
Ø 检查sql语法,编译sql;
Ø 执行sql语句;
Ø 将sql语句返回给JDBC 接口;
如果我们需要插入成千上万甚至更多的数据库,如果采用传统的方式,势必每次都需要经历上述几个步骤,其中执行多少次sql语句就需要进行多少次通讯,网络数据通讯的开销也是一个很耗时的操作步骤,怎样才能减少网络的操作次数呢?我们可否将所有的需要执行的sql语句一次性传递给数据库,然后再将结果返回回来呢,这样不就减少了网络层的开销么?基于这样的原因JDBC API 提供了一个批处理的机制,方便我们的操作,稍等我会通过示例对其进行演示;
12.1 普通方式插入一千条数据
@Test public void statementInsert() throws SQLException{ long startTime = System.currentTimeMillis(); normalInsert(0, 1000); System.out.println("total time:"+(System.currentTimeMillis()-startTime)); } private static void normalInsert(int start,int end) throws SQLException{ Connection conn = null; PreparedStatement stmt = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)"; for(int i = start;i<end;i++){ stmt = conn.prepareStatement(sql); stmt.setInt(1, i); stmt.setString(2, "test"); stmt.setDate(3, new Date(System.currentTimeMillis())); stmt.setFloat(4, 6000); stmt.executeUpdate(); } } finally { ConnCreate.close(conn, stmt, null); } } |
执行结果为:
total time:27530
12.2 批处理方式插入一千条数据
@Test public void batchInsert() throws SQLException{ long startTime = System.currentTimeMillis(); batchInsert(1000, 2000); System.out.println("total time:"+(System.currentTimeMillis()-startTime)); } private static void batchInsert(int start,int end) throws SQLException{ Connection conn = null; PreparedStatement stmt = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)"; stmt = conn.prepareStatement(sql); for(int i = start;i<end;i++){ stmt.setInt(1, i); stmt.setString(2, "test"); stmt.setDate(3, new Date(System.currentTimeMillis())); stmt.setFloat(4, 6000); stmt.addBatch(sql); } int[] result=stmt.executeBatch(); System.out.println("result:"+result.length); } finally { ConnCreate.close(conn, stmt, null); } } |
执行结果为:
total time:22305
可以看到性能优化了一些,但是可能和大家的期望值有些差距,当然这和数据库的驱动有直接的关系,有时候甚至会降低性能,如果需要采取这样的方式做的时候需要提前进行一下测试,然后进行选择;
第十三节 JDBC其他API
截止目前,JDBC的API已经讲解的基本上差不多了,我们在本节中将剩下的一些技术做一个简单的介绍,方便读者使用,至于剩余的技术,读者可以自行上网研究查询资料。
Ø 可滚动结果集;
Ø 分页技术;
Ø 可更新结果集;
13.1 可滚动结果集
大家还有没有注意到,我们使用ResultSet的时候,获取结果的时候经常使用next方法,该指针的意思就是将指针指向下一个数据行,然后获取出来,那么我们可否访问某个指针上一行记录呢?这就是可滚动结果集的初衷
代码示例
package com.wangwenjun.jdbc.batch;
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;
import org.junit.Test;
import com.wangwenjun.jdbc.ConnCreate;
public class ScrollResultTest {
@Test public void scollResult() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "select * from user where id<10"; stmt = conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY); rs = stmt.executeQuery(); while(rs.next()) //向下滚动 { System.out.println("name:"+rs.getString(2)+"id:"+rs.getInt(1)); } System.out.println("---------------------------"); if(rs.previous()){//向上滚动 System.out.println("name:"+rs.getString(2)+"id:"+rs.getInt(1)); }
rs.absolute(5); System.out.println("---------------------------"); if(rs.next()){//定位到指定的行 System.out.println("name:"+rs.getString(2)+"id:"+rs.getInt(1)); } } finally { ConnCreate.close(conn, stmt, null); } } }
|
执行结果
name:testid:0
name:testid:1
name:testid:2
name:testid:3
name:testid:4
name:testid:5
name:testid:6
name:testid:7
name:testid:8
name:testid:9
---------------------------
name:testid:9
---------------------------
name:testid:5
可以看到分割线前后的打印信息一样,可以看出来,结果集向上滚动成功,并且能准确定位到指定的行,当然还可以直接到第一行,最后一行,判断是否是第一行或者最后一行,读者可以自己做testCase
13.2 分页技术
分页技术是一项很常见的技术,而且是一个非常重要的技术,如果数据总共有几十万几百万条,都获取出来给浏览者带来一定的麻烦不说,还会将服务器的内存消耗的很严重,所以我们就采用分页技术,分页技术的概念其实很简单,意思就是说我想从第几行开始再获取多少行记录,我们来看一个例子
@Test public void page() throws SQLException{ page(100,20); }
static void page(int start,int total) throws SQLException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "select * from user limit ?,?"; stmt = conn.prepareStatement(sql); stmt.setInt(1, start); stmt.setInt(2, total); rs = stmt.executeQuery(); while(rs.next()) //向下滚动 { System.out.println("name:"+rs.getString(2)+"id:"+rs.getInt(1)); } } finally { ConnCreate.close(conn, stmt, null); } } |
执行结果当然是将数据库中的数据从第100行开始到120的记录展示出来,其中使用了limit这个关键字,这是mysql特有的,其他数据库是没有这个关键字的,oracle需要使用伪列rowid来进行分页。
13.3 可更新结果集
这个API是一个不常使用的API,也是实际项目中极力反对的API,但是为了拓展读者的阅读视野,在本小节中做一个简单的演示即可。
@Test public void modifyReult() throws SQLException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnCreate.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); String sql = "select * from user where id=100"; stmt = conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE); rs = stmt.executeQuery(); while(rs.next()) //向下滚动 { System.out.println("name:"+rs.getString(2)+"id:"+rs.getInt(1)); rs.updateString("name", "update3"); rs.updateRow(); }
} finally { ConnCreate.close(conn, stmt, rs); } } |
演示结果为
可以看到结果已经正常被更新成功!
第十四节 编写一个简单的数据库连接池
通过前面十三个章节的讲解,我们的jdbc常用API基本上介绍完了,在本节中我们将通过由浅入深的方式介绍一下数据库连接池的原理,并尝试写一个简单的数据库连接池,加深读者对数据库连接池的理解;
14.1 为什么要使用数据库连接池
使用jdbc最大的开销之一就是创建数据库,当我们频繁的创建数据库时,势必影响应用的效率,或者在数据库关闭出现问题时,我们不能马上释放,时间长一些,整个数据库的 资源将会被我们的应用耗尽,这是一个很危险的讯息,怎样将数据库连接创建带来的开销降到最低呢,就是我们实现将一系列创建完毕的连接存放起来,等到使用的时候再来使用,这就是数据库连接池解决的最大问题和存在的最大必要。
14.2 数据库连接池雏形
编写SimpleDataSource.java
package com.wangwenjun.jdbc.dataSource;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.LinkedList;
public class SimpleDataSource {
private static LinkedList<Connection> connPool = null;
static { try { Class.forName("com.mysql.jdbc.Driver"); connPool = new LinkedList<Connection>(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
public SimpleDataSource(){ for(int i = 0;i<10;i++){ try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); connPool.addFirst(conn); System.out.println(conn); } catch (SQLException e) { e.printStackTrace(); } } }
public Connection getConnection(){ return connPool.removeLast(); }
public void closeConn(Connection conn){ connPool.addFirst(conn); } }
|
修改ConnCreate.java
package com.wangwenjun.jdbc;
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement;
import com.wangwenjun.jdbc.dataSource.SimpleDataSource;
public class ConnCreate {
private static SimpleDataSource dataSource = new SimpleDataSource();
public static Connection getConnection() { Connection conn = dataSource.getConnection(); return conn; }
public static void close(Connection conn, Statement stmt, ResultSet rs) throws SQLException {
if (null != rs) { rs.close(); }
if (null != stmt) { stmt.close(); }
if (null != conn) { dataSource.closeConn(conn); } } } |
开始进行单元测试
@Test public void createDataSource() throws SQLException{ for(int i = 0;i<10;i++){ Connection conn=ConnCreate.getConnection(); ConnCreate.close(conn, null, null); } } |
执行结果为:
com.mysql.jdbc.Connection@93dee9
com.mysql.jdbc.Connection@1ba34f2
com.mysql.jdbc.Connection@1cde100
com.mysql.jdbc.Connection@360be0
com.mysql.jdbc.Connection@6b97fd
com.mysql.jdbc.Connection@15ff48b
com.mysql.jdbc.Connection@1b90b39
com.mysql.jdbc.Connection@13e8d89
com.mysql.jdbc.Connection@1a8c4e7
com.mysql.jdbc.Connection@1729854
可以看出,获取了10个数据库连接,因为他们的hashcode不一样,我们再来测试一下我们的连接是否能够等到重用呢?修改一下测试代码
@Test public void createDataSource() throws SQLException{ for(int i = 0;i<20;i++){ Connection conn=ConnCreate.getConnection(); ConnCreate.close(conn, null, null); System.out.println(conn); } } |
执行结果如下所示
com.mysql.jdbc.Connection@affc70
com.mysql.jdbc.Connection@1e63e3d
com.mysql.jdbc.Connection@1004901
com.mysql.jdbc.Connection@1b90b39
com.mysql.jdbc.Connection@18fe7c3
com.mysql.jdbc.Connection@b8df17
com.mysql.jdbc.Connection@13e8d89
com.mysql.jdbc.Connection@1be2d65
com.mysql.jdbc.Connection@9664a1
com.mysql.jdbc.Connection@1a8c4e7
com.mysql.jdbc.Connection@affc70
com.mysql.jdbc.Connection@1e63e3d
com.mysql.jdbc.Connection@1004901
com.mysql.jdbc.Connection@1b90b39
com.mysql.jdbc.Connection@18fe7c3
com.mysql.jdbc.Connection@b8df17
com.mysql.jdbc.Connection@13e8d89
com.mysql.jdbc.Connection@1be2d65
com.mysql.jdbc.Connection@9664a1
com.mysql.jdbc.Connection@1a8c4e7
可以看出数据库连接被重复使用了,因为通过打印的语句可以看出,有相同hashcode的Connection
14.2 数据库连接池优化
在上一节中,我们简单实现了一个数据库连接池,做到了简单的数据库连接的重复应用,当然上述的代码还不能真的使用到项目之中去,因为考虑的因素是在太少了,并发,数据库连接的关闭等等,所以在本节中将会逐步进行一系列的优化
14.2.1 对线程池加锁
为了保证,每一个线程获取的数据库连接实例是不同的,我们需要对线程池进行加锁,保证多线程并发时获取的连接各不相干,修改其中的代码片段如下
public Connection getConnection(){ synchronized (connPool) { return connPool.removeLast(); } } |
14.2.2 连接不够用时抛出异常
在我们的代码中,没有体现出来,当连接不够用时怎么处理,这里我们做一个简单的处理,直接抛出一个异常,让调用者得知当前连接不够使用。
public Connection getConnection() throws Exception{ synchronized (connPool) { if(connPool.isEmpty()) throw new Exception("have no connection now!"); return connPool.removeLast(); } } |
14.3 数据库连接池之代理模式
在使用JDBC连接数据库的时候,最后需要用户释放资源,如果使用者按照传统的方式关闭连接,那么我们的连接池就没有存在的意义了,因为每一次使用者都会给关闭掉,导致连接池的连接会是无效的或者越来越少,为了防止这样的事情发生,我们需要保留使用者的使用习惯,也就是说允许使用者通过close方法释放连接,这个时候我们应该如何做既能起到用户的使用习惯,又能在进行关闭的时候不是真的关掉数据库连接,而是直接存放至数据库连接池中
14.3.1 静态代理
SimpleDataSource.java
package com.wangwenjun.jdbc.dataSource;
import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Savepoint; import java.sql.Statement; import java.util.Map;
public class SimpleConnection implements Connection{
private SimpleDataSource dataSource = null;
private Connection connection = null;
public SimpleConnection(SimpleDataSource dataSource,Connection connection) { this.dataSource = dataSource; this.connection = connection; }
public void clearWarnings() throws SQLException { connection.clearWarnings(); }
public void close() throws SQLException { dataSource.closeConn(this); }
public void commit() throws SQLException { connection.commit(); }
public Statement createStatement() throws SQLException { return connection.createStatement(); }
public Statement createStatement(int arg0, int arg1) throws SQLException { return connection.createStatement(arg0, arg1); }
public Statement createStatement(int arg0, int arg1, int arg2) throws SQLException { return connection.createStatement(arg0, arg1, arg2); }
public boolean getAutoCommit() throws SQLException { return connection.getAutoCommit(); }
public String getCatalog() throws SQLException { return connection.getCatalog(); }
public int getHoldability() throws SQLException { return connection.getHoldability(); }
public DatabaseMetaData getMetaData() throws SQLException { return connection.getMetaData(); }
public int getTransactionIsolation() throws SQLException { return connection.getTransactionIsolation(); }
public Map<String, Class<?>> getTypeMap() throws SQLException { return connection.getTypeMap(); }
public SQLWarning getWarnings() throws SQLException { return connection.getWarnings(); }
public boolean isClosed() throws SQLException { return connection.isClosed(); }
public boolean isReadOnly() throws SQLException { return connection.isReadOnly(); }
public String nativeSQL(String arg0) throws SQLException { return connection.nativeSQL(arg0); }
public CallableStatement prepareCall(String arg0) throws SQLException { return connection.prepareCall(arg0); }
public CallableStatement prepareCall(String arg0, int arg1, int arg2) throws SQLException { return connection.prepareCall(arg0, arg1, arg2); }
public CallableStatement prepareCall(String arg0, int arg1, int arg2, int arg3) throws SQLException { return connection.prepareCall(arg0, arg1, arg2, arg3); }
public PreparedStatement prepareStatement(String arg0) throws SQLException { return connection.prepareStatement(arg0); }
public PreparedStatement prepareStatement(String arg0, int arg1) throws SQLException { return null; }
public PreparedStatement prepareStatement(String arg0, int[] arg1) throws SQLException { return connection.prepareStatement(arg0, arg1); }
public PreparedStatement prepareStatement(String arg0, String[] arg1) throws SQLException { return connection.prepareStatement(arg0, arg1); }
public PreparedStatement prepareStatement(String arg0, int arg1, int arg2) throws SQLException { return connection.prepareStatement(arg0, arg1, arg2); }
public PreparedStatement prepareStatement(String arg0, int arg1, int arg2, int arg3) throws SQLException { return connection.prepareStatement(arg0, arg1, arg2, arg3); }
public void releaseSavepoint(Savepoint arg0) throws SQLException { connection.releaseSavepoint(arg0); }
public void rollback() throws SQLException { connection.rollback(); }
public void rollback(Savepoint arg0) throws SQLException { connection.rollback(arg0); }
public void setAutoCommit(boolean arg0) throws SQLException { connection.setAutoCommit(arg0); }
public void setCatalog(String arg0) throws SQLException { connection.setCatalog(arg0); }
public void setHoldability(int arg0) throws SQLException { connection.setHoldability(arg0); }
public void setReadOnly(boolean arg0) throws SQLException { connection.setReadOnly(arg0); }
public Savepoint setSavepoint() throws SQLException { return connection.setSavepoint(); }
public Savepoint setSavepoint(String arg0) throws SQLException { return connection.setSavepoint(arg0); }
public void setTransactionIsolation(int arg0) throws SQLException { connection.setTransactionIsolation(arg0); }
public void setTypeMap(Map<String, Class<?>> arg0) throws SQLException { connection.setTypeMap(arg0); }
} |
数据库连接池代码SimpleDataSource.java
package com.wangwenjun.jdbc.dataSource;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.LinkedList;
public class SimpleDataSource {
private static LinkedList<Connection> connPool = null;
private static int minCount = 5;
static { try { Class.forName("com.mysql.jdbc.Driver"); connPool = new LinkedList<Connection>(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
public SimpleDataSource(){ for(int i = 0;i<minCount;i++){ try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); Connection proxyConnection = new SimpleConnection(this, conn); connPool.addFirst(proxyConnection); } catch (SQLException e) { e.printStackTrace(); } } }
/** * 获取数据库连接 * @return * @throws Exception */ public Connection getConnection() throws Exception{ synchronized (connPool) { if(connPool.isEmpty()) throw new Exception("have no connection now!"); return connPool.removeLast(); } }
/** * 释放数据库连接 * @param conn * @throws SQLException */ void closeConn(Connection conn) throws SQLException{ synchronized (connPool) { connPool.addFirst(conn); } }
/** * 获取当前数据库连接数 * @return */ public int getCurrentCount(){ synchronized (connPool) { return connPool.size(); } } } |
测试代码
@Test public void createDataSource1() throws Exception{ for(int i = 0;i<20;i++){ Connection conn=ConnCreate.getConnection(); conn.close(); System.out.println(conn); } } |
在上述代码中我们可以看出我们自定义了一个Connection的实现类,并且在连接池中存放的是我们自定义的Connection类,这样在进行关闭的时候我们可以将数据库连接存放至连接池中。
14.3.2 动态代理
可以看出,静态代理的机制,在实现上很死板,并且我们需要重复写的东西实在太多了,从JDK1.3开始有了一个动态代理机制,我们可以利用该机制来实现我们刚才想要的功能。
DynSimpleDataSource.java
package com.wangwenjun.jdbc.dataSource;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.LinkedList;
public class DynSimpleDataSource { private static LinkedList<Connection> connPool = null;
private static int minCount = 5;
static { try { Class.forName("com.mysql.jdbc.Driver"); connPool = new LinkedList<Connection>(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
public DynSimpleDataSource(){ for(int i = 0;i<minCount;i++){ try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "r66t"); DynSimpleConnection dynSimpleConnection = new DynSimpleConnection(this); Connection connWarp=dynSimpleConnection.bind(conn); connPool.addFirst(connWarp); } catch (SQLException e) { e.printStackTrace(); } } }
/** * 获取数据库连接 * @return * @throws Exception */ public Connection getConnection() throws Exception{ synchronized (connPool) { if(connPool.isEmpty()) throw new Exception("have no connection now!"); return connPool.removeLast(); } }
/** * 释放数据库连接 * @param conn * @throws SQLException */ void closeConn(Connection conn) throws SQLException{ synchronized (connPool) { connPool.addFirst(conn); } }
/** * 获取当前数据库连接数 * @return */ public int getCurrentCount(){ synchronized (connPool) { return connPool.size(); } } } |
DynSimpleConnection.java
package com.wangwenjun.jdbc.dataSource;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection;
public class DynSimpleConnection implements InvocationHandler {
private Connection realConnection = null;// Connection real
private Connection warpConnection = null;
private DynSimpleDataSource dataSource = null;
public DynSimpleConnection(DynSimpleDataSource dataSource) { this.dataSource = dataSource; }
public Connection bind(Connection obj) { this.realConnection = obj; this.warpConnection= (Connection) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj .getClass().getInterfaces(), this); return warpConnection; }
public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable { String methodName = arg1.getName(); Object obj = null; if(methodName.equals("close")){ dataSource.closeConn(warpConnection); }else{ obj = arg1.invoke(realConnection, arg2); }
return obj; } } |
可以发现代码简化了好多,并且实现了我们所需要的功能,测试代码如下所示
@Test public void createDataSource2() throws Exception{ for(int i = 0;i<20;i++){ Connection conn=ConnCreate.getConnection(); conn.close(); System.out.println(conn); } } |
14.4 DBCP数据库连接池的使用
写到这里,我们的连接池已经有些接近真实的数据库连接池了,但是其功能还是很单一,健壮性远远不够,不能直接在实际项目中应用,但是通过前面几个小节的讲解和逐步改进,我们最起码了解了什么是数据库连接池,以及连接池或与应该具备怎样的功能,在开源界,连接池框架实在太多了,并且健壮性足够而且还有专门的优秀团队维护,比如DBCP,C3P0,DbPool等等,在本节中我们做一个抛砖引玉的工作,简单的介绍一下DBCP的示例代码,希望读者有兴趣可以自己再研究一下。
@Test public void simpleTest() throws Exception{ BasicDataSource dataSource = new BasicDataSource(); dataSource.setUsername("root"); dataSource.setPassword("r66t"); dataSource.setUrl("jdbc:mysql://localhost:3306/test"); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); System.out.println(dataSource.getConnection()); } |
第十五节 jdbc轻量级封装
传统的JDBC使用方式在使用起来非常的麻烦,需要创建一系列的资源,然后又要释放一系列的资源,还要为了重复的异常信息进行处理,在本章中我们将对JDBC的使用做一个轻量级的封装,在本章中将会展示极少数部分的代码,如果大家需要剩余的代码可以给我发QQ跟我进行索要。
15.1 将结果集封装为Map
15.1.1 ResultSetMetaData演示
在进行Map封装的开始,我们首先来介绍一个API,并且进行一下简单的演示,先简单说一下该API的作用,如果我们不知道我们的一个sql语句查询了几列结果集,并且每列的列名,类型等信息,这个时候我们应该怎么去做呢?
ResultSetMetaData接口刚好给我们提供了这样的一个功能,我们可以通过他的若干个方法来获取我们关心的信息,在开始将结果集分装为Map对象的时候我们来进行一下演示。
@Test public void resultMeta() throws SQLException{ String sql="select * from user"; Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); for(int i=1;i<=count;++i){ System.out.println("Type:"+rsmd.getColumnType(i)); System.out.println("ColumnName:"+rsmd.getColumnName(i)); System.out.println("ColumnLable:"+rsmd.getColumnLabel(i)); } } finally{ ConnFactory.close(conn, stmt, rs); }
} |
执行结果为:
Type:4
ColumnName:id
ColumnLable:id
Type:12
ColumnName:name
ColumnLable:name
Type:91
ColumnName:birthday
ColumnLable:birthday
Type:7
ColumnName:money
ColumnLable:money
15.1.2解决多行记录的问题
我们的需求是这样的,我们需要将列名作为Key值,查询出来的记录作为Value值,如果当我们查询出来的记录超过一行,这个时候怎么做呢?我们不能武断的随便将一行记录封装成Map结果集给使用者,这是一种不合理的设计,我们需要告知使用者,当前的查询语句会查询出来一行以上的记录,不能使用Map这样的结果集,通过代码演示如何得到记录数超过一行并且告知使用者
@Test public void resultExceed() throws SQLException{ String sql="select * from user"; Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); if(rs.next()){ if(!rs.isLast()) throw new SQLException("查询记录超过一行,不能使用Map结果集"); } } finally{ ConnFactory.close(conn, stmt, rs); } } |
执行结果为
15.1.3 Map结果集的封装
好了,知识点具备了,并且知道了怎么去解决多行记录的问题,我们开始将结果集封装成Map的形式
@Test public void warp2Map() throws SQLException{ Map<String,Object> map = warp2Map("select * from user where id=1"); System.out.println(map); }
static Map<String,Object> warp2Map(String sql) throws SQLException{ Map<String,Object> map = null; Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); if(rs.next()){ if(!rs.isLast()) throw new SQLException("查询记录超过一行,不能使用Map结果集"); map = new HashMap<String, Object>(); for(int i=1;i<=count;++i){ map.put(rsmd.getColumnLabel(i), rs.getObject(i)); } } } finally{ ConnFactory.close(conn, stmt, rs); }
return map; } |
执行结果为:
{money=6000.0, name=test, id=1, birthday=2011-06-02}
可以看出我们已经顺利完成了任务
15.2 将结果集封装为对象
这个需求相对来说就显得有些复杂了,我们需要用到反射的技术,如果大家对反射不是很了解,可以阅读我的另外一本札记《反射(reflection)学习整理》
15.2.1 user表POJO的编写
package com.wangwenjun.jdbc.template;
import java.sql.Date;
public class User { //-----------------------------------------字段的定义 private Integer id; private String name; private Date birthday; private Float money;
//-----------------------------------------提供默认构造函数 public User(){
}
//-----------------------------------------生成setter getter方法 public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Date getBirthday() { return birthday; }
public void setBirthday(Date birthday) { this.birthday = birthday; }
public Float getMoney() { return money; }
public void setMoney(Float money) { this.money = money; } }
|
15.2.2 Bean结果集的封装
package com.wangwenjun.jdbc.template;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException;
import org.junit.Test;
public class ResultBean {
@Test public void warp2Object() throws SecurityException, IllegalArgumentException, SQLException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException{ User user =(User) warp2Class("select * from user where id=0",User.class); System.out.println(user.getName()); System.out.println(user.getBirthday()); System.out.println(user.getId()); System.out.println(user.getMoney()); }
static Object warp2Class(String sql,Class<?> clazz) throws SQLException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; Object destObject = clazz.newInstance(); try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); if(rs.next()){ if(!rs.isLast()) throw new SQLException("查询记录超过一行,不能使用Map结果集"); Method method = null; for(int i=1;i<=rsmd.getColumnCount();i++){ method = clazz.getMethod(parseMethodName(rsmd.getColumnLabel(i)),new Class[]{rs.getObject(i).getClass()}); method.invoke(destObject, new Object[]{rs.getObject(i)}); } } } finally{ ConnFactory.close(conn, stmt, rs); } return destObject; }
static String parseMethodName(String columnName){ String temp = "set"; temp+=columnName.substring(0, 1).toUpperCase(); temp+=columnName.substring(1); return temp; } }
|
执行结果为:
test
2011-06-02
0
6000.0
15.3 将结果集封装为List
封装为List相比之下较为复杂,需要用到15.2节中的一些知识,如果认真看过15.2节的讲解我们会比较容易的读懂这一小节中的代码
package com.wangwenjun.jdbc.template;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List;
import org.junit.Test;
public class ResultList {
@Test public void warp2Object() throws SecurityException, IllegalArgumentException, SQLException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException{ List<Object> userList =warp2Class("select * from user",User.class); for(Iterator<Object> it = userList.iterator();it.hasNext();){ User user = (User) it.next(); System.out.println(user.getName()+"|"+user.getBirthday()+"|"+user.getMoney()+"|"+user.getId()); } }
static List<Object> warp2Class(String sql,Class<?> clazz) throws SQLException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException{ Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; List<Object> lists = new ArrayList<Object>(); try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); while(rs.next()){ Object destObject = clazz.newInstance(); Method method = null; for(int i=1;i<=rsmd.getColumnCount();i++){ method = clazz.getMethod(parseMethodName(rsmd.getColumnLabel(i)),new Class[]{rs.getObject(i).getClass()}); method.invoke(destObject, new Object[]{rs.getObject(i)}); } lists.add(destObject); } } finally{ ConnFactory.close(conn, stmt, rs); } return lists; }
static String parseMethodName(String columnName){ String temp = "set"; temp+=columnName.substring(0, 1).toUpperCase(); temp+=columnName.substring(1); return temp; } } |
执行结果为:
test|2011-06-02|6000.0|0
test|2011-06-02|6000.0|1
15.4 策略模式的应用
前面的三个小节中,我们通过将结果集封装成三种不同的结构体演示了如何通过反射,结果集的元数据将数据库对象转换成Java对象,基本功能均已实现,但是存在一个问题,就是我们大多时候使用了强类型转换,并且没有使用统一的接口去管理三个不同的封装形式,如果要我们自己编写API供别人使用,这将是一个很不太好的方式。
在本节中我们通过一个统一的接口来作为抽象,三种不同的实现作为继承,这样使用者只需要通过接口进行编程即可。
接口ResultHandler.java
package com.wangwenjun.jdbc.template;
public interface ResultHandler<T> {
public T handler(String sql,Class<?> clazz) throws SQLException; }
|
分别修改上述三种封装代码
15.4.1 Map结果集策略模式应用
package com.wangwenjun.jdbc.template;
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.HashMap; import java.util.Map;
public class ResultMap implements ResultHandler<Map<String, Object>> {
public Map<String, Object> handler(String sql, Class<?> clazz) throws SQLException { Map<String, Object> map = null; Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); if (rs.next()) { if (!rs.isLast()) throw new SQLException("查询记录超过一行,不能使用Map结果集"); map = new HashMap<String, Object>(); for (int i = 1; i <= count; ++i) { map.put(rsmd.getColumnLabel(i), rs.getObject(i)); } } } finally { ConnFactory.close(conn, stmt, rs); }
return map; }
} |
15.4.2 Bean结果集策略模式应用
package com.wangwenjun.jdbc.template;
import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException;
public class ResultBean<T> implements ResultHandler<T>{
private String parseMethodName(String columnName){ String temp = "set"; temp+=columnName.substring(0, 1).toUpperCase(); temp+=columnName.substring(1); return temp; }
@SuppressWarnings("unchecked") public T handler(String sql, Class<?> clazz) throws Exception { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; T t = (T) clazz.newInstance(); try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); if(rs.next()){ if(!rs.isLast()) throw new SQLException("查询记录超过一行,不能使用Map结果集"); Method method = null; for(int i=1;i<=rsmd.getColumnCount();i++){ method = clazz.getMethod(parseMethodName(rsmd.getColumnLabel(i)),new Class[]{rs.getObject(i).getClass()}); method.invoke(t, new Object[]{rs.getObject(i)}); } } } finally{ ConnFactory.close(conn, stmt, rs); } return t; } } |
15.4.3 List结果集策略模式应用
package com.wangwenjun.jdbc.template;
import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List;
public class ResultList<T> implements ResultHandler<List<T>>{
private String parseMethodName(String columnName){ String temp = "set"; temp+=columnName.substring(0, 1).toUpperCase(); temp+=columnName.substring(1); return temp; }
@SuppressWarnings("unchecked") public List<T> handler(String sql, Class<?> clazz) throws Exception { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; List<T> lists = new ArrayList<T>(); try { conn = ConnFactory.getConnection(); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); while(rs.next()){ T destObject = (T) clazz.newInstance(); Method method = null; for(int i=1;i<=rsmd.getColumnCount();i++){ method = clazz.getMethod(parseMethodName(rsmd.getColumnLabel(i)),new Class[]{rs.getObject(i).getClass()}); method.invoke(destObject, new Object[]{rs.getObject(i)}); } lists.add(destObject); } } finally{ ConnFactory.close(conn, stmt, rs); } return lists; } } |
15.4.4 单元测试
package com.wangwenjun.jdbc.template;
import java.util.List; import java.util.Map;
import org.junit.Assert; import org.junit.Test;
public class ContextTest {
@Test public void mapWarp() throws Exception{ ResultHandler<Map<String, Object>> handler = new ResultMap(); Map<String,Object> maps=handler.handler("select * from user where id=1", null); System.out.println(maps); }
@Test public void beanWarp() throws Exception{ ResultHandler<User> handler = new ResultBean<User>(); User user = handler.handler("select * from user where id=1", User.class); Assert.assertEquals("test", user.getName()); }
@Test public void ListWarp() throws Exception{ ResultList<User> handler = new ResultList<User>(); List<User> lists = handler.handler("select * from user", User.class); Assert.assertEquals(2, lists.size()); } }
|