JavaWeb开发与代码的编写(十七)
编写JDBC框架
元数据介绍
元数据指的是"数据库"、"表"、"列"的定义信息。
DataBaseMetaData元数据
Connection.getDatabaseMetaData()获得代表DatabaseMetaData元数据的DatabaseMetaData对象。 DataBaseMetaData对象的常用方法:
- getURL():返回一个String类对象,代表数据库的URL。
- getUserName():返回连接当前数据库管理系统的用户名。
- getDatabaseProductName():返回数据库的产品名称。
- getDatabaseProductVersion():返回数据库的版本号。
- getDriverName():返回驱动驱动程序的名称。
- getDriverVersion():返回驱动程序的版本号。
- isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。
/**
* @Method: testDataBaseMetaData
* @Description: 获取数据库的元信息
* @throws SQLException
*/
@Test
public void testDataBaseMetaData() throws SQLException {
Connection conn = JdbcUtils.getConnection();
DatabaseMetaData metadata = conn.getMetaData();
//getURL():返回一个String类对象,代表数据库的URL
System.out.println(metadata.getURL());
//getUserName():返回连接当前数据库管理系统的用户名
System.out.println(metadata.getUserName());
//getDatabaseProductName():返回数据库的产品名称
System.out.println(metadata.getDatabaseProductName());
//getDatabaseProductVersion():返回数据库的版本号
System.out.println(metadata.getDatabaseProductVersion());
//getDriverName():返回驱动驱动程序的名称
System.out.println(metadata.getDriverName());
//getDriverVersion():返回驱动程序的版本号
System.out.println(metadata.getDriverVersion());
//isReadOnly():返回一个boolean值,指示数据库是否只允许读操作
System.out.println(metadata.isReadOnly());
JdbcUtils.release(conn, null, null);
}
运行结果如下:
ParameterMetaData元数据
PreparedStatement.getParameterMetaData() 获得代表PreparedStatement元数据的ParameterMetaData对象。 Select * from user where name=? And password=? ParameterMetaData对象的常用方法:
- getParameterCount(): 获得指定参数的个数
- getParameterType(int param):获得指定参数的sql类型,MySQL数据库驱动不支持
/**
* @Method: testParameterMetaData
* @Description: 获取参数元信息
* @throws SQLException
*/
@Test
public void testParameterMetaData() throws SQLException {
Connection conn = JdbcUtils.getConnection();
String sql = "select * from user wherer name=? and password=?";
//将SQL预编译一下
PreparedStatement st = conn.prepareStatement(sql);
ParameterMetaData pm = st.getParameterMetaData();
//getParameterCount() 获得指定参数的个数
System.out.println(pm.getParameterCount());
//getParameterType(int param):获得指定参数的sql类型,MySQL数据库驱动不支持
System.out.println(pm.getParameterType(1));
JdbcUtils.release(conn, null, null);
}
ResultSetMetaData元数据
ResultSet. getMetaData() 获得代表ResultSet对象元数据的ResultSetMetaData对象。 ResultSetMetaData对象的常用方法:
- getColumnCount() 返回resultset对象的列数
- getColumnName(int column) 获得指定列的名称
- getColumnTypeName(int column)获得指定列的类型
/**
* @Method: testResultSetMetaData
* @Description: 结果集的元数据
* @throws Exception
*/
@Test
public void testResultSetMetaData() throws Exception {
Connection conn = JdbcUtils.getConnection();
String sql = "select * from account";
PreparedStatement st = conn.prepareStatement(sql);
ResultSet rs = st.executeQuery();
//ResultSet.getMetaData()获得代表ResultSet对象元数据的ResultSetMetaData对象
ResultSetMetaData metadata = rs.getMetaData();
//getColumnCount() 返回resultset对象的列数
System.out.println(metadata.getColumnCount());
//getColumnName(int column) 获得指定列的名称
System.out.println(metadata.getColumnName(1));
//getColumnTypeName(int column)获得指定列的类型
System.out.println(metadata.getColumnTypeName(1));
JdbcUtils.release(conn, st, rs);
}
使用元数据封装简单的JDBC框架
系统中所有实体对象都涉及到基本的CRUD操作 所有实体的CUD操作代码基本相同,仅仅发送给数据库的SQL语句不同而已,因此可以把CUD操作的所有相同代码抽取到工具类的一个update方法中,并定义参数接收变化的SQL语句。 实体的R操作,除SQL语句不同之外,根据操作的实体不同,对ResultSet的映射也各不相同,因此可义一个query方法,除以参数形式接收变化的SQL语句外,可以使用策略模式由qurey方法的调用者决定如何把ResultSet中的数据映射到实体对象中。
封装通用的update方法和qurey方法
定义一个JdbcUtils工具类,工具类负责获取数据库连接,释放资源,执行SQL的update和query操作,代码如下:
1 package me.gacl.util;
2
3 import java.io.InputStream;
4 import java.sql.Connection;
5 import java.sql.DriverManager;
6 import java.sql.PreparedStatement;
7 import java.sql.ResultSet;
8 import java.sql.SQLException;
9 import java.sql.Statement;
10 import java.util.Properties;
11
12 public class JdbcUtils {
13
14 private static String driver = null;
15 private static String url = null;
16 private static String username = null;
17 private static String password = null;
18
19 static{
20 try{
21 //读取db.properties文件中的数据库连接信息
22 InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("db.properties");
23 Properties prop = new Properties();
24 prop.load(in);
25
26 //获取数据库连接驱动
27 driver = prop.getProperty("driver");
28 //获取数据库连接URL地址
29 url = prop.getProperty("url");
30 //获取数据库连接用户名
31 username = prop.getProperty("username");
32 //获取数据库连接密码
33 password = prop.getProperty("password");
34
35 //加载数据库驱动
36 Class.forName(driver);
37
38 }catch (Exception e) {
39 throw new ExceptionInInitializerError(e);
40 }
41 }
42
43 /**
44 * @Method: getConnection
45 * @Description: 获取数据库连接对象
47 *
48 * @return Connection数据库连接对象
49 * @throws SQLException
50 */
51 public static Connection getConnection() throws SQLException{
52 return DriverManager.getConnection(url, username,password);
53 }
54
55 /**
56 * @Method: release
57 * @Description: 释放资源,
58 * 要释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
60 *
61 * @param conn
62 * @param st
63 * @param rs
64 */
65 public static void release(Connection conn,Statement st,ResultSet rs){
66 if(rs!=null){
67 try{
68 //关闭存储查询结果的ResultSet对象
69 rs.close();
70 }catch (Exception e) {
71 e.printStackTrace();
72 }
73 rs = null;
74 }
75 if(st!=null){
76 try{
77 //关闭负责执行SQL命令的Statement对象
78 st.close();
79 }catch (Exception e) {
80 e.printStackTrace();
81 }
82 }
83
84 if(conn!=null){
85 try{
86 //关闭Connection数据库连接对象
87 conn.close();
88 }catch (Exception e) {
89 e.printStackTrace();
90 }
91 }
92 }
93
94 /**
95 * @Method: update
96 * @Description: 万能更新
97 * 所有实体的CUD操作代码基本相同,仅仅发送给数据库的SQL语句不同而已,
98 * 因此可以把CUD操作的所有相同代码抽取到工具类的一个update方法中,并定义参数接收变化的SQL语句
100 * @param sql 要执行的SQL
101 * @param params 执行SQL时使用的参数
102 * @throws SQLException
103 */
104 public static void update(String sql,Object params[]) throws SQLException{
105 Connection conn = null;
106 PreparedStatement st = null;
107 ResultSet rs = null;
108 try{
109 conn = getConnection();
110 st = conn.prepareStatement(sql);
111 for(int i=0;i clazz;
public BeanListHandler(Class clazz){
this.clazz = clazz;
}
public Object handler(ResultSet rs) {
try{
List list = new ArrayList();
while(rs.next()){
Object bean = clazz.newInstance();
ResultSetMetaData metadata = rs.getMetaData();
int count = metadata.getColumnCount();
for(int i=0;i0?list:null;
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
当框架自身提供的结果集处理器不满足用户的要求时,那么用户就可以自己去实现ResultSetHandler接口,编写满足自己业务要求的结果集处理器。
有了上述的JdbcUtils框架之后,针对单个实体对象CRUD操作就非常方便了,如下所示:
package me.gacl.dao;
import java.sql.SQLException;
import java.util.List;
import me.gacl.domain.Account;
import me.gacl.util.BeanHandler;
import me.gacl.util.BeanListHandler;
import me.gacl.util.JdbcUtils;
public class AccountDao {
public void add(Account account) throws SQLException{
String sql = "insert into account(name,money) values(?,?)";
Object params[] = {account.getName(),account.getMoney()};
JdbcUtils.update(sql, params);
}
public void delete(int id) throws SQLException{
String sql = "delete from account where id=?";
Object params[] = {id};
JdbcUtils.update(sql, params);
}
public void update(Account account) throws SQLException{
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
JdbcUtils.update(sql, params);
}
public Account find(int id) throws SQLException{
String sql = "select * from account where id=?";
Object params[] = {id};
return (Account) JdbcUtils.query(sql, params, new BeanHandler(Account.class));
}
public List getAll() throws SQLException{
String sql = "select * from account";
Object params[] = {};
return (List) JdbcUtils.query(sql, params,new BeanListHandler(Account.class));
}
}
编写的这个JDBC框架就是模拟Apache的DBUtils框架的实现,下一篇将具体介绍Apache的DBUtils框架。
Apache的DBUtils框架
commons-dbutils
commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库,它是对JDBC的简单封装,学习成本极低,并且使用dbutils能极大简化jdbc编码的工作量,同时也不会影响程序的性能。因此dbutils成为很多不喜欢hibernate的公司的首选。
commons-dbutilsAPI介绍:
- org.apache.commons.dbutils.QueryRunner
- org.apache.commons.dbutils.ResultSetHandler
工具类
- org.apache.commons.dbutils.DbUtils
QueryRunner类使用讲解
该类简单化了SQL查询,它与ResultSetHandler组合在一起使用可以完成大部分的数据库操作,能够大大减少编码量。 QueryRunner类提供了两个构造方法:
- 默认的构造方法
- 需要一个 javax.sql.DataSource 来作参数的构造方法。
QueryRunner类的主要方法
public Object query(Connection conn, String sql, Object[] params, ResultSetHandler rsh) throws SQLException:执行一个查询操作,在这个查询中,对象数组中的每个元素值被用来作为查询语句的置换参数。该方法会自行处理 PreparedStatement 和 ResultSet 的创建和关闭。 public Object query(String sql, Object[] params, ResultSetHandler rsh) throws SQLException: 几乎与第一种方法一样;唯一的不同在于它不将数据库连接提供给方法,并且它是从提供给构造方法的数据源(DataSource) 或使用的setDataSource 方法中重新获得 Connection。 public Object query(Connection conn, String sql, ResultSetHandler rsh) throws SQLException : 执行一个不需要置换参数的查询操作。 public int update(Connection conn, String sql, Object[] params) throws SQLException:用来执行一个更新(插入、更新或删除)操作。 public int update(Connection conn, String sql) throws SQLException:用来执行一个不需要置换参数的更新操作。
使用QueryRunner类实现CRUD
1 package me.gacl.test;
2
3 import java.util.Date;
4 import java.util.List;
5 import java.io.File;
6 import java.io.FileReader;
7 import java.io.IOException;
8 import java.sql.SQLException;
9 import javax.sql.rowset.serial.SerialClob;
10 import me.gacl.domain.User;
11 import me.gacl.util.JdbcUtils;
12 import org.apache.commons.dbutils.QueryRunner;
13 import org.apache.commons.dbutils.handlers.BeanHandler;
14 import org.apache.commons.dbutils.handlers.BeanListHandler;
15 import org.junit.Test;
16
17 /**
18 * @ClassName: DBUtilsCRUDTest
19 * @Description:使用dbutils框架的QueryRunner类完成CRUD,以及批处理
22 *
23 */
24 public class QueryRunnerCRUDTest {
25
26 /*
27 *测试表
28 create table users(
29 id int primary key auto_increment,
30 name varchar(40),
31 password varchar(40),
32 email varchar(60),
33 birthday date
34 );
35 */
36
37 @Test
38 public void add() throws SQLException {
39 //将数据源传递给QueryRunner,QueryRunner内部通过数据源获取数据库连接
40 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
41 String sql = "insert into users(name,password,email,birthday) values(?,?,?,?)";
42 Object params[] = {"孤傲苍狼","123", "gacl@sina.com", new Date()};
43 //Object params[] = {"白虎神皇","123", "gacl@sina.com", "1988-05-07"};
44 qr.update(sql, params);
45 }
46
47 @Test
48 public void delete() throws SQLException {
49
50 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
51 String sql = "delete from users where id=?";
52 qr.update(sql, 1);
53
54 }
55
56 @Test
57 public void update() throws SQLException {
58 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
59 String sql = "update users set name=? where id=?";
60 Object params[] = { "ddd", 5};
61 qr.update(sql, params);
62 }
63
64 @Test
65 public void find() throws SQLException {
66 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
67 String sql = "select * from users where id=?";
68 Object params[] = {2};
69 User user = (User) qr.query(sql, params, new BeanHandler(User.class));
70 System.out.println(user.getBirthday());
71 }
72
73 @Test
74 public void getAll() throws SQLException {
75 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
76 String sql = "select * from users";
77 List list = (List) qr.query(sql, new BeanListHandler(User.class));
78 System.out.println(list.size());
79 }
80
81 /**
82 * @Method: testBatch
83 * @Description:批处理
86 * @throws SQLException
87 */
88 @Test
89 public void testBatch() throws SQLException {
90 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
91 String sql = "insert into users(name,password,email,birthday) values(?,?,?,?)";
92 Object params[][] = new Object[10][];
93 for (int i = 0; i < 10; i++) {
94 params[i] = new Object[] { "aa" + i, "123", "aa@sina.com",
95 new Date() };
96 }
97 qr.batch(sql, params);
98 }
99
100 //用dbutils完成大数据(不建议用)
101 /***************************************************************************
102 create table testclob
103 (
104 id int primary key auto_increment,
105 resume text
106 );
107 **************************************************************************/
108 @Test
109 public void testclob() throws SQLException, IOException{
110 QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
111 String sql = "insert into testclob(resume) values(?)"; //clob
112 //这种方式获取的路径,其中的空格会被使用“%20”代替
113 String path = QueryRunnerCRUDTest.class.getClassLoader().getResource("data.txt").getPath();
114 //将“%20”替换回空格
115 path = path.replaceAll("%20", " ");
116 FileReader in = new FileReader(path);
117 char[] buffer = new char[(int) new File(path).length()];
118 in.read(buffer);
119 SerialClob clob = new SerialClob(buffer);
120 Object params[] = {clob};
121 runner.update(sql, params);
122 }
123 }
ResultSetHandler接口使用讲解
该接口用于处理java.sql.ResultSet,将数据按要求转换为另一种形式。 ResultSetHandler接口提供了一个单独的方法:Object handle (java.sql.ResultSet .rs)
ResultSetHandler接口的实现类
- ArrayHandler:把结果集中的第一行数据转成对象数组。
- ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
- BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。
- BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。
- ColumnListHandler:将结果集中某一列的数据存放到List中。
- KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里,再把这些map再存到一个map里,其key为指定的key。
- MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。
- MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List
测试dbutils各种类型的处理器
1 package me.gacl.test;
2
3 import java.sql.SQLException;
4 import java.util.Arrays;
5 import java.util.List;
6 import java.util.Map;
7 import me.gacl.util.JdbcUtils;
8 import org.apache.commons.dbutils.QueryRunner;
9 import org.apache.commons.dbutils.handlers.ArrayHandler;
10 import org.apache.commons.dbutils.handlers.ArrayListHandler;
11 import org.apache.commons.dbutils.handlers.ColumnListHandler;
12 import org.apache.commons.dbutils.handlers.KeyedHandler;
13 import org.apache.commons.dbutils.handlers.MapHandler;
14 import org.apache.commons.dbutils.handlers.MapListHandler;
15 import org.apache.commons.dbutils.handlers.ScalarHandler;
16 import org.junit.Test;
17
18 /**
19 * @ClassName: ResultSetHandlerTest
20 * @Description:测试dbutils各种类型的处理器
23 *
24 */
25 public class ResultSetHandlerTest {
26
27 @Test
28 public void testArrayHandler() throws SQLException{
29 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
30 String sql = "select * from users";
31 Object result[] = (Object[]) qr.query(sql, new ArrayHandler());
32 System.out.println(Arrays.asList(result)); //list toString()
33 }
34
35 @Test
36 public void testArrayListHandler() throws SQLException{
37
38 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
39 String sql = "select * from users";
40 List list = (List) qr.query(sql, new ArrayListHandler());
41 for(Object[] o : list){
42 System.out.println(Arrays.asList(o));
43 }
44 }
45
46 @Test
47 public void testColumnListHandler() throws SQLException{
48 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
49 String sql = "select * from users";
50 List list = (List) qr.query(sql, new ColumnListHandler("id"));
51 System.out.println(list);
52 }
53
54 @Test
55 public void testKeyedHandler() throws Exception{
56 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
57 String sql = "select * from users";
58
59 Map map = (Map) qr.query(sql, new KeyedHandler("id"));
60 for(Map.Entry me : map.entrySet()){
61 int id = me.getKey();
62 Map innermap = me.getValue();
63 for(Map.Entry innerme : innermap.entrySet()){
64 String columnName = innerme.getKey();
65 Object value = innerme.getValue();
66 System.out.println(columnName + "=" + value);
67 }
68 System.out.println("----------------");
69 }
70 }
71
72 @Test
73 public void testMapHandler() throws SQLException{
74
75 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
76 String sql = "select * from users";
77
78 Map map = (Map) qr.query(sql, new MapHandler());
79 for(Map.Entry me : map.entrySet())
80 {
81 System.out.println(me.getKey() + "=" + me.getValue());
82 }
83 }
84
85
86 @Test
87 public void testMapListHandler() throws SQLException{
88 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
89 String sql = "select * from users";
90 List list = (List) qr.query(sql, new MapListHandler());
91 for(Map map :list){
92 for(Map.Entry me : map.entrySet())
93 {
94 System.out.println(me.getKey() + "=" + me.getValue());
95 }
96 }
97 }
98
99 @Test
100 public void testScalarHandler() throws SQLException{
101 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
102 String sql = "select count(*) from users"; //[13] list[13]
103 int count = ((Long)qr.query(sql, new ScalarHandler(1))).intValue();
104 System.out.println(count);
105 }
106 }
DbUtils类使用讲解
DbUtils :提供如关闭连接、装载JDBC驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法如下: public static void close(…) throws java.sql.SQLException: DbUtils类提供了三个重载的关闭方法。这些方法检查所提供的参数是不是NULL,如果不是的话,它们就关闭Connection、Statement和ResultSet。 public static void closeQuietly(…): 这一类方法不仅能在Connection、Statement和ResultSet为NULL情况下避免关闭,还能隐藏一些在程序中抛出的SQLEeception。 public static void commitAndCloseQuietly(Connection conn): 用来提交连接,然后关闭连接,并且在关闭连接时不抛出SQL异常。 public static boolean loadDriver(java.lang.String driverClassName):这一方装载并注册JDBC驱动程序,如果成功就返回true。使用该方法,你不需要捕捉这个异常ClassNotFoundException。
JDBC开发中的事务处理
在开发中,对数据库的多个表或者对一个表中的多条数据执行更新操作时要保证对多个更新操作要么同时成功,要么都不成功,这就涉及到对多个更新操作的事务管理问题了。比如银行业务中的转账问题,A用户向B用户转账100元,假设A用户和B用户的钱都存储在Account表,那么A用户向B用户转账时就涉及到同时更新Account表中的A用户的钱和B用户的钱,用SQL来表示就是:
update account set money=money-100 where name='A'
update account set money=money+100 where name='B'
在数据访问层(Dao)中处理事务
对于这样的同时更新一个表中的多条数据的操作,那么必须保证要么同时成功,要么都不成功,所以需要保证这两个update操作在同一个事务中进行。在开发中,我们可能会在AccountDao写一个转账处理方法,如下:
/**
* @Method: transfer
* @Description:这个方法是用来处理两个用户之间的转账业务
* 在开发中,DAO层的职责应该只涉及到CRUD,
* 而这个transfer方法是处理两个用户之间的转账业务的,已经涉及到具体的业务操作,应该在业务层中做,不应该出现在DAO层的
* 所以在开发中DAO层出现这样的业务处理方法是完全错误的
*
* @param sourceName
* @param targetName
* @param money
* @throws SQLException
*/
public void transfer(String sourceName,String targetName,float money) throws SQLException{
Connection conn = null;
try{
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
/**
* 在创建QueryRunner对象时,不传递数据源给它,是为了保证这两条SQL在同一个事务中进行,
* 我们手动获取数据库连接,然后让这两条SQL使用同一个数据库连接执行
*/
QueryRunner runner = new QueryRunner();
String sql1 = "update account set money=money-100 where name=?";
String sql2 = "update account set money=money+100 where name=?";
Object[] paramArr1 = {sourceName};
Object[] paramArr2 = {targetName};
runner.update(conn,sql1,paramArr1);
//模拟程序出现异常让事务回滚
int x = 1/0;
runner.update(conn,sql2,paramArr2);
//sql正常执行之后就提交事务
conn.commit();
}catch (Exception e) {
e.printStackTrace();
if(conn!=null){
//出现异常之后就回滚事务
conn.rollback();
}
}finally{
//关闭数据库连接
conn.close();
}
}
然后我们在AccountService中再写一个同名方法,在方法内部调用AccountDao的transfer方法处理转账业务,如下:
public void transfer(String sourceName,String targetName,float money) throws SQLException{
AccountDao dao = new AccountDao();
dao.transfer(sourceName, targetName, money);
}
上面AccountDao的这个transfer方法可以处理转账业务,并且保证了在同一个事务中进行,但是AccountDao的这个transfer方法是处理两个用户之间的转账业务的,已经涉及到具体的业务操作,应该在业务层中做,不应该出现在DAO层的,在开发中,DAO层的职责应该只涉及到基本的CRUD,不涉及具体的业务操作,所以在开发中DAO层出现这样的业务处理方法是一种不好的设计。
在业务层(BusinessService)处理事务
由于上述AccountDao存在具体的业务处理方法,导致AccountDao的职责不够单一,下面我们对AccountDao进行改造,让AccountDao的职责只是做CRUD操作,将事务的处理挪到业务层(BusinessService),改造后的AccountDao如下:
1 package me.gacl.dao;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import org.apache.commons.dbutils.QueryRunner;
6 import org.apache.commons.dbutils.handlers.BeanHandler;
7 import me.gacl.domain.Account;
8 import me.gacl.util.JdbcUtils;
9
10 /*account测试表
11 create table account(
12 id int primary key auto_increment,
13 name varchar(40),
14 money float
15 )character set utf8 collate utf8_general_ci;
16
17 insert into account(name,money) values('A',1000);
18 insert into account(name,money) values('B',1000);
19 insert into account(name,money) values('C',1000);
20
21 */
22
23 /**
24 * @ClassName: AccountDao
25 * @Description: 针对Account对象的CRUD
28 *
29 */
30 public class AccountDao {
31
32 //接收service层传递过来的Connection对象
33 private Connection conn = null;
34
35 public AccountDao(Connection conn){
36 this.conn = conn;
37 }
38
39 public AccountDao(){
40
41 }
42
43 /**
44 * @Method: update
45 * @Description:更新
47 *
48 * @param account
49 * @throws SQLException
50 */
51 public void update(Account account) throws SQLException{
52
53 QueryRunner qr = new QueryRunner();
54 String sql = "update account set name=?,money=? where id=?";
55 Object params[] = {account.getName(),account.getMoney(),account.getId()};
56 //使用service层传递过来的Connection对象操作数据库
57 qr.update(conn,sql, params);
58
59 }
60
61 /**
62 * @Method: find
63 * @Description:查找
65 *
66 * @param id
67 * @return
68 * @throws SQLException
69 */
70 public Account find(int id) throws SQLException{
71 QueryRunner qr = new QueryRunner();
72 String sql = "select * from account where id=?";
73 //使用service层传递过来的Connection对象操作数据库
74 return (Account) qr.query(conn,sql, id, new BeanHandler(Account.class));
75 }
76 }
接着对AccountService(业务层)中的transfer方法的改造,在业务层(BusinessService)中处理事务
package me.gacl.service;
import java.sql.Connection;
import java.sql.SQLException;
import me.gacl.dao.AccountDao;
import me.gacl.domain.Account;
import me.gacl.util.JdbcUtils;
/**
* @ClassName: AccountService
* @Description: 业务逻辑处理层
*
*/
public class AccountService {
/**
* @Method: transfer
* @Description:这个方法是用来处理两个用户之间的转账业务
* @Anthor:孤傲苍狼
*
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public void transfer(int sourceid,int tartgetid,float money) throws SQLException{
Connection conn = null;
try{
//获取数据库连接
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
//将获取到的Connection传递给AccountDao,保证dao层使用的是同一个Connection对象操作数据库
AccountDao dao = new AccountDao(conn);
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
dao.update(source);
//模拟程序出现异常让事务回滚
int x = 1/0;
dao.update(target);
//提交事务
conn.commit();
}catch (Exception e) {
e.printStackTrace();
//出现异常之后就回滚事务
conn.rollback();
}finally{
conn.close();
}
}
}
程序经过这样改造之后就比刚才好多了,AccountDao只负责CRUD,里面没有具体的业务处理方法了,职责就单一了,而AccountService则负责具体的业务逻辑和事务的处理,需要操作数据库时,就调用AccountDao层提供的CRUD方法操作数据库。
使用ThreadLocal进行更加优雅的事务处理
上面的在businessService层这种处理事务的方式依然不够优雅,为了能够让事务处理更加优雅,我们使用ThreadLocal类进行改造,ThreadLocal一个容器,向这个容器存储的对象,在当前线程范围内都可以取得出来,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了
ThreadLocal类的使用范例如下:
package me.gacl.test;
public class ThreadLocalTest {
public static void main(String[] args) {
//得到程序运行时的当前线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);
//ThreadLocal一个容器,向这个容器存储的对象,在当前线程范围内都可以取得出来
ThreadLocal t = new ThreadLocal();
//把某个对象绑定到当前线程上 对象以键值对的形式存储到一个Map集合中,对象的的key是当前的线程,如: map(currentThread,"aaa")
t.set("aaa");
//获取绑定到当前线程中的对象
String value = t.get();
//输出value的值是aaa
System.out.println(value);
}
}
使用使用ThreadLocal类进行改造数据库连接工具类JdbcUtils,改造后的代码如下:
1 package me.gacl.util;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import javax.sql.DataSource;
6 import com.mchange.v2.c3p0.ComboPooledDataSource;
7
8 /**
9 * @ClassName: JdbcUtils2
10 * @Description: 数据库连接工具类
14 */
15 public class JdbcUtils2 {
16
17 private static ComboPooledDataSource ds = null;
18 //使用ThreadLocal存储当前线程中的Connection对象
19 private static ThreadLocal threadLocal = new ThreadLocal();
20
21 //在静态代码块中创建数据库连接池
22 static{
23 try{
24 //通过代码创建C3P0数据库连接池
25 /*ds = new ComboPooledDataSource();
26 ds.setDriverClass("com.mysql.jdbc.Driver");
27 ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
28 ds.setUser("root");
29 ds.setPassword("XDP");
30 ds.setInitialPoolSize(10);
31 ds.setMinPoolSize(5);
32 ds.setMaxPoolSize(20);*/
33
34 //通过读取C3P0的xml配置文件创建数据源,C3P0的xml配置文件c3p0-config.xml必须放在src目录下
35 //ds = new ComboPooledDataSource();//使用C3P0的默认配置来创建数据源
36 ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置来创建数据源
37
38 }catch (Exception e) {
39 throw new ExceptionInInitializerError(e);
40 }
41 }
42
43 /**
44 * @Method: getConnection
45 * @Description: 从数据源中获取数据库连接
47 * @return Connection
48 * @throws SQLException
49 */
50 public static Connection getConnection() throws SQLException{
51 //从当前线程中获取Connection
52 Connection conn = threadLocal.get();
53 if(conn==null){
54 //从数据源中获取数据库连接
55 conn = getDataSource().getConnection();
56 //将conn绑定到当前线程
57 threadLocal.set(conn);
58 }
59 return conn;
60 }
61
62 /**
63 * @Method: startTransaction
64 * @Description: 开启事务
66 *
67 */
68 public static void startTransaction(){
69 try{
70 Connection conn = threadLocal.get();
71 if(conn==null){
72 conn = getConnection();
73 //把 conn绑定到当前线程上
74 threadLocal.set(conn);
75 }
76 //开启事务
77 conn.setAutoCommit(false);
78 }catch (Exception e) {
79 throw new RuntimeException(e);
80 }
81 }
82
83 /**
84 * @Method: rollback
85 * @Description:回滚事务
87 *
88 */
89 public static void rollback(){
90 try{
91 //从当前线程中获取Connection
92 Connection conn = threadLocal.get();
93 if(conn!=null){
94 //回滚事务
95 conn.rollback();
96 }
97 }catch (Exception e) {
98 throw new RuntimeException(e);
99 }
100 }
101
102 /**
103 * @Method: commit
104 * @Description:提交事务
106 *
107 */
108 public static void commit(){
109 try{
110 //从当前线程中获取Connection
111 Connection conn = threadLocal.get();
112 if(conn!=null){
113 //提交事务
114 conn.commit();
115 }
116 }catch (Exception e) {
117 throw new RuntimeException(e);
118 }
119 }
120
121 /**
122 * @Method: close
123 * @Description:关闭数据库连接(注意,并不是真的关闭,而是把连接还给数据库连接池)
125 *
126 */
127 public static void close(){
128 try{
129 //从当前线程中获取Connection
130 Connection conn = threadLocal.get();
131 if(conn!=null){
132 conn.close();
133 //解除当前线程上绑定conn
134 threadLocal.remove();
135 }
136 }catch (Exception e) {
137 throw new RuntimeException(e);
138 }
139 }
140
141 /**
142 * @Method: getDataSource
143 * @Description: 获取数据源
145 * @return DataSource
146 */
147 public static DataSource getDataSource(){
148 //从数据源中获取数据库连接
149 return ds;
150 }
151 }
对AccountDao进行改造,数据库连接对象不再需要service层传递过来,而是直接从JdbcUtils2提供的getConnection方法去获取,改造后的AccountDao如下:
package me.gacl.dao;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import me.gacl.domain.Account;
import me.gacl.util.JdbcUtils;
import me.gacl.util.JdbcUtils2;
/*
create table account(
id int primary key auto_increment,
name varchar(40),
money float
)character set utf8 collate utf8_general_ci;
insert into account(name,money) values('A',1000);
insert into account(name,money) values('B',1000);
insert into account(name,money) values('C',1000);
*/
/**
* @ClassName: AccountDao
* @Description: 针对Account对象的CRUD
*
*/
public class AccountDao2 {
public void update(Account account) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
//JdbcUtils2.getConnection()获取当前线程中的Connection对象
qr.update(JdbcUtils2.getConnection(),sql, params);
}
public Account find(int id) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "select * from account where id=?";
//JdbcUtils2.getConnection()获取当前线程中的Connection对象
return (Account) qr.query(JdbcUtils2.getConnection(),sql, id, new BeanHandler(Account.class));
}
}
对AccountService进行改造,service层不再需要传递数据库连接Connection给Dao层,改造后的AccountService如下:
package me.gacl.service;
import java.sql.SQLException;
import me.gacl.dao.AccountDao2;
import me.gacl.domain.Account;
import me.gacl.util.JdbcUtils2;
public class AccountService2 {
/**
* @Method: transfer
* @Description:在业务层处理两个账户之间的转账问题
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public void transfer(int sourceid,int tartgetid,float money) throws SQLException{
try{
//开启事务,在业务层处理事务,保证dao层的多个操作在同一个事务中进行
JdbcUtils2.startTransaction();
AccountDao2 dao = new AccountDao2();
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
dao.update(source);
//模拟程序出现异常让事务回滚
int x = 1/0;
dao.update(target);
//SQL正常执行之后提交事务
JdbcUtils2.commit();
}catch (Exception e) {
e.printStackTrace();
//出现异常之后就回滚事务
JdbcUtils2.rollback();
}finally{
//关闭数据库连接
JdbcUtils2.close();
}
}
}
这样在service层对事务的处理看起来就更加优雅了。ThreadLocal类在开发中使用得是比较多的,程序运行中产生的数据要想在一个线程范围内共享,只需要把数据使用ThreadLocal进行存储即可。
ThreadLocal + Filter 处理事务
上面介绍了JDBC开发中事务处理的3种方式,下面再介绍的一种使用ThreadLocal + Filter进行统一的事务处理,这种方式主要是使用过滤器进行统一的事务处理,如下图所示:
1、编写一个事务过滤器TransactionFilter
代码如下:
1 package me.gac.web.filter;
2
3 import java.io.IOException;
4 import java.sql.Connection;
5 import java.sql.SQLException;
6 import javax.servlet.Filter;
7 import javax.servlet.FilterChain;
8 import javax.servlet.FilterConfig;
9 import javax.servlet.ServletException;
10 import javax.servlet.ServletRequest;
11 import javax.servlet.ServletResponse;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14 import me.gacl.util.JdbcUtils;
15
16 /**
17 * @ClassName: TransactionFilter
18 * @Description:ThreadLocal + Filter 统一处理数据库事务
21 *
22 */
23 public class TransactionFilter implements Filter {
24
25 @Override
26 public void init(FilterConfig filterConfig) throws ServletException {
27
28 }
29
30 @Override
31 public void doFilter(ServletRequest request, ServletResponse response,
32 FilterChain chain) throws IOException, ServletException {
33
34 Connection connection = null;
35 try {
36 //1、获取数据库连接对象Connection
37 connection = JdbcUtils.getConnection();
38 //2、开启事务
39 connection.setAutoCommit(false);
40 //3、利用ThreadLocal把获取数据库连接对象Connection和当前线程绑定
41 ConnectionContext.getInstance().bind(connection);
42 //4、把请求转发给目标Servlet
43 chain.doFilter(request, response);
44 //5、提交事务
45 connection.commit();
46 } catch (Exception e) {
47 e.printStackTrace();
48 //6、回滚事务
49 try {
50 connection.rollback();
51 } catch (SQLException e1) {
52 e1.printStackTrace();
53 }
54 HttpServletRequest req = (HttpServletRequest) request;
55 HttpServletResponse res = (HttpServletResponse) response;
56 //req.setAttribute("errMsg", e.getMessage());
57 //req.getRequestDispatcher("/error.jsp").forward(req, res);
58 //出现异常之后跳转到错误页面
59 res.sendRedirect(req.getContextPath()+"/error.jsp");
60 }finally{
61 //7、解除绑定
62 ConnectionContext.getInstance().remove();
63 //8、关闭数据库连接
64 try {
65 connection.close();
66 } catch (SQLException e) {
67 e.printStackTrace();
68 }
69 }
70 }
71
72 @Override
73 public void destroy() {
74
75 }
76 }
我们在TransactionFilter中把获取到的数据库连接使用ThreadLocal绑定到当前线程之后,在DAO层还需要从ThreadLocal中取出数据库连接来操作数据库,因此需要编写一个ConnectionContext类来存储ThreadLocal,ConnectionContext类的代码如下:
1 package me.gac.web.filter;
2
3 import java.sql.Connection;
4
5 /**
6 * @ClassName: ConnectionContext
7 * @Description:数据库连接上下文
11 */
12 public class ConnectionContext {
13
14 /**
15 * 构造方法私有化,将ConnectionContext设计成单例
16 */
17 private ConnectionContext(){
18
19 }
20 //创建ConnectionContext实例对象
21 private static ConnectionContext connectionContext = new ConnectionContext();
22
23 /**
24 * @Method: getInstance
25 * @Description:获取ConnectionContext实例对象
27 *
28 * @return
29 */
30 public static ConnectionContext getInstance(){
31 return connectionContext;
32 }
33
34 /**
35 * @Field: connectionThreadLocal
36 * 使用ThreadLocal存储数据库连接对象
37 */
38 private ThreadLocal connectionThreadLocal = new ThreadLocal();
39
40 /**
41 * @Method: bind
42 * @Description:利用ThreadLocal把获取数据库连接对象Connection和当前线程绑定
45 * @param connection
46 */
47 public void bind(Connection connection){
48 connectionThreadLocal.set(connection);
49 }
50
51 /**
52 * @Method: getConnection
53 * @Description:从当前线程中取出Connection对象
55 *
56 * @return
57 */
58 public Connection getConnection(){
59 return connectionThreadLocal.get();
60 }
61
62 /**
63 * @Method: remove
64 * @Description: 解除当前线程上绑定Connection
66 *
67 */
68 public void remove(){
69 connectionThreadLocal.remove();
70 }
71 }
在DAO层想获取数据库连接时,就可以使用ConnectionContext.getInstance().getConnection()来获取,如下所示:
package me.gacl.dao;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import me.gac.web.filter.ConnectionContext;
import me.gacl.domain.Account;
/*
create table account(
id int primary key auto_increment,
name varchar(40),
money float
)character set utf8 collate utf8_general_ci;
insert into account(name,money) values('A',1000);
insert into account(name,money) values('B',1000);
insert into account(name,money) values('C',1000);
*/
/**
* @ClassName: AccountDao
* @Description: 针对Account对象的CRUD
*
*/
public class AccountDao3 {
public void update(Account account) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
//ConnectionContext.getInstance().getConnection()获取当前线程中的Connection对象
qr.update(ConnectionContext.getInstance().getConnection(),sql, params);
}
public Account find(int id) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "select * from account where id=?";
//ConnectionContext.getInstance().getConnection()获取当前线程中的Connection对象
return (Account) qr.query(ConnectionContext.getInstance().getConnection(),sql, id, new BeanHandler(Account.class));
}
}
businessService层也不用处理事务和数据库连接问题了,这些统一在TransactionFilter中统一管理了,businessService层只需要专注业务逻辑的处理即可,如下所示:
package me.gacl.service;
import java.sql.SQLException;
import me.gacl.dao.AccountDao3;
import me.gacl.domain.Account;
public class AccountService3 {
/**
* @Method: transfer
* @Description:在业务层处理两个账户之间的转账问题
*
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public void transfer(int sourceid, int tartgetid, float money)
throws SQLException {
AccountDao3 dao = new AccountDao3();
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
dao.update(source);
// 模拟程序出现异常让事务回滚
int x = 1 / 0;
dao.update(target);
}
}
Web层的Servlet调用businessService层的业务方法处理用户请求,需要注意的是:调用businessService层的方法出异常之后,继续将异常抛出,这样在TransactionFilter就能捕获到抛出的异常,继而执行事务回滚操作,如下所示:
package me.gacl.web.controller;
import java.io.IOException;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import me.gacl.service.AccountService3;
public class AccountServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
AccountService3 service = new AccountService3();
try {
service.transfer(1, 2, 100);
} catch (SQLException e) {
e.printStackTrace();
//注意:调用service层的方法出异常之后,继续将异常抛出,这样在TransactionFilter就能捕获到抛出的异常,继而执行事务回滚操作
throw new RuntimeException(e);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}