对Hibernate默认的JDBC事务的尝试
作者:http://blog.csdn.net/fenglibing,转载请保留。
Hibernate的默认事务为JDBC事务,它其是就旧通过设置JDBC的Connection.setAutoCommit(false)来做点这一点,默认是不允许自动提交的,因为用户如果需要将对库表的修改提交到数据库,需要用户手工增加事务,否则不会将对数据库的更改提交到数据库。因为是对默认的JDBC事务的尝试,为了省去配置文件及简洁明了,以下我全部都是通过获取Connection的方式去操作数据库。
一、最简单的事务:单表操作
这种情况通常都是出现在对单表的操作,如下简单示例:
/*起事务的提交*/
public static void simpleAction(int value) throws SQLException{
/*Hibernate Session*/
Session session=null;
/*获取当前连接*/
session=HibernateUtil.getCurrentSession();
/*打开事务*/
Transaction tx=session.beginTransaction();
/*获取Connection连接*/
Connection conn=session.connection();
/*执行SQL语句*/
conn.createStatement().execute("insert into test1 values("+value+")");
/*提交事务*/
tx.commit();
/*关闭Hibernate Session*/
session.close();
}
这种情况执行完后,就立即关闭session。
二、同一批操作多个数据表,并且都不在同一个方法内,这样的事务用于控制要么全部提交,要么全部回滚。
1、在同一个方法内操作多个表的和这个类型,更好处理,全部都放到:
Transaction tx=session.beginTransaction();
/*这里进行对多个表的操作*/
tx.commit();
2、分别在不同的方法内的操作,在最外层调用的方法里面起事务,并且在内部被调用的方法中不可以起事务,也不可以做关闭SESSION的动作,如A方法为统一入口,其中调用B和C方法,B会调用D和E方法,在C和D、E方法中都有对数据表的操作,如下结构:
A
B C
D E
示例代码如下:
/*插入多个表的事务控制*/
public static long A(){
long nextMxsq=0;
Session session=null;
Transaction tx=null;
try{
session=HibernateUtil.getCurrentSession();
tx=session.beginTransaction();
C(1);
B();
tx.commit();
}catch(Exception e){
e.printStackTrace();
tx.rollback();
}finally{
HibernateUtil.closeCurrentSession();
}
return nextMxsq;
}
public static void B() throws Exception{
int dValue=D(2);
E(dValue,3);
}
public static void C(int value) throws Exception{
Session session=null;
session=HibernateUtil.getCurrentSession();
Connection conn=session.connection();
conn.createStatement().execute("insert into test1 values("+value+")");
}
public static int D(int value) throws Exception{
Session session=null;
session=HibernateUtil.getCurrentSession();
Connection conn=session.connection();
conn.createStatement().execute("insert into test1 values("+value+")");
return 1;
}
public static void E(int dValue,int value) throws Exception{
Session session=null;
session=HibernateUtil.getCurrentSession();
Connection conn=session.connection();
conn.createStatement().execute("insert into test2 values("+value+")");
}
三、同一批操作多个数据表,并且都不在同一个方法内,但是这个时候并不是所有的表都要么全部成功、要么全部回滚的状态;假如E表为操作记录表,它是以插入时间为主键或者是没有键,其中保存的为我操作的报文(如我是操作报文传入数据库的存储过程执行),我后面要看是否操作成功,如果失败是为什么失败,并且我需要将这些报文在后台重复执行,所以这个时候就需要保留D方法中插入的数据,不管是否操作成功。我的E方法必须被C方法调用才可以,不能够独立到A方法外面另起事务,因为这时候可能需要用到C方法中处理后的数据。
这个时候要怎么实现呢,我有一个同事说可以采用大事务中包括小事务,在那个E方法内提交事务,最外层A方法中再提交事务,但是这种方式是不是成功呢?
1、对大事务包括小事务的测试
看如下测试代码:
/*插入多个表的事务控制*/
public static void doTest(){
Session session=null;
Transaction tx=null;
try{
session=HibernateUtil.getCurrentSession();
tx=session.beginTransaction();
simpleAction("test2",2);
test("test1",2);
tx.commit(); }catch(Exception e){
e.printStackTrace();
tx.rollback();
}finally{
HibernateUtil.closeCurrentSession();
}
}
/*无事务,指定表名和值进行插入操作*/
public static void test(String table,int value) throws Exception{
Session session=null;
session=HibernateUtil.getCurrentSession();
Connection conn=session.connection();
conn.createStatement().execute("insert into "+table+" values("+value+")");
}
/*起事务的提交*/
public static void simpleAction(String table,int value) throws SQLException{
/*Hibernate Session*/
Session session=null;
/*获取当前连接*/
session=HibernateUtil.getCurrentSession();
/*打开事务*/
Transaction tx=session.beginTransaction();
/*获取Connection连接*/
Connection conn=session.connection();
/*执行SQL语句*/
conn.createStatement().execute("insert into "+table+" values("+value+")");
/*提交事务*/
tx.commit();
}
经过我的测试,同一个SESSION只支持一次显示事务提交,再次显示提交事务的时候就会报错,如上示例就会在方doTest法的tx.commit(); 处报错,如果去掉tx.commit(); 则test方法的执行也没有意义。
此时我把方法doTest改成下,test也修改一下:
/*插入多个表的事务控制*/
public static void doTest(){
Session session=null;
Transaction tx=null;
try{
session=HibernateUtil.getCurrentSession();
tx=session.beginTransaction();
simpleAction("test2",2);
test("test1",2);
//tx.commit();//去掉显示事务的提交 }catch(Exception e){
e.printStackTrace();
tx.rollback();
}finally{
HibernateUtil.closeCurrentSession();
}
}
/*无事务,指定表名和值进行插入操作*/
public static void test(String table,int value) throws Exception{
Session session=null;
session=HibernateUtil.getCurrentSession();
Connection conn=session.connection();
conn.setAutoCommit(true);//设置自动提交
conn.createStatement().execute("insert into "+table+" values("+value+")");
conn.setAutoCommit(false);
}
这个时候调用doTest方法是不会报错,并且可以把数据给插入,这个时候采用的是自动提交。
此时如果在插入第二个表的时候报异常,则第一个表的数据是不能够回滚的,因为第一个表的数据已经通过事务提交给了数据库了。这种情况是不可以满足大事务中的小事务的,即天事务只能够自动提交一次,后面如果再提交事务就会报错,因为他们是同一个SESSION,当然如果关闭了SESSION再重新打开也不符合事务的要求,因为都不是同一个SESSION了,怎么回滚;另外就是如果需要保留数据的是D方法中插入的,如果D方法提交事务,则C方法中插入的数据就一起提交了,因为他们是同一个SESSION,如果后面处理发生问题,C方法中插入的数据是不可以回滚的。
测试结果:不满足要求。
2、解决方案。
这只是一种替换性的解决方案,并不是很完美,但是在不出问题的情况下,可以解决上面的问题。
自定义事务
将操作表的记录都记录到ThreadLocal代表的ArrayList中存储,放于ThreadLocal中这样就可以避免多线程问题,每个线程都会只记录当前的操作。ArrayList存放字符串数据,该字符串数组包括"表名、字段名S(S表示可以多个字段,以逗号分开,其它相同)、字段值S、字段类型(只分两种:String及Number)",需要回滚的每个交易都按要求存放(如上面的C、E方法都保存进来,而D是不需要回滚的就不加入),在发生异常的时候,在最外层的异常中增加回滚的调用,系统会自动将其生成SQL语句,循环执行删除操作。我上面说的“不出问题的情况”就是表示在删除的时候不报错就可以保证,如果删除的时候报错,这个也不能够解决问题。以下是一个经过测试的实现类:
import java.util.ArrayList;
/**
* @version 创建时间:2009-6-22 下午08:24:55
* 类说明:事务保证类,在执行失败的情况下,删除已经保存到数据表中的数据
* 示例:
* 1、增加需要回滚的操作:TransactionMgmt.newDBAction("table1", "mesgid,origrc,origsd", "A,B,C", "String,String,String");
* 2、最外层捕获异常调用:TransactionMgmt.rollbackDBAction();
*/
public class TransactionMgmt {
/**
* newActions用于存放字符串数组,该字符串数组包括"表名、字段名S(S表示可以多个字段,以逗号分开,其它相同)、字段值S、字段类型(只分两种:String及Number)":
* String[] action={"table1","field1","valu1","String"};//单字段的情况
* 或
* String[] action={"table1","field1,field2","valu1,value2","String,Number"};//多字段及值用逗号分开
* 代表意义分别为:
* action[0]:表名
* action[1]:字段名称S
* action[2]:字段的值S
* action[3]:字段的类型S(String及Number)
* 将其存放于ArrayList中,取出的时候,以上示例分别生成SQL语句:
* delete table1 where field1='value1'
* delete table1 where field1='value1' and field2=value2
*/
private static ArrayList newActions;
private static ThreadLocal localNewActions = new ThreadLocal(){
public void set(Object action){
if(newActions==null){
newActions=new ArrayList();
}
newActions.add(action);
}
public Object get(){
return newActions;
}
};
public static void newDBAction(String tablename,String fields,String values,String types){
String[] action={tablename,fields,values,types};
newDBAction(action);
}
/**
* 新增交易,按"表名、字段名S(S表示可以多个字段,以逗号分开,其它相同)、字段值S、字段类型(只分两种:String及Number)"数组传入参数
* 将交易放于ThreadLocal的ArrayList中,以区别于其它线程的操作
* @param action
*/
private static void newDBAction(String[] action){
localNewActions.set(action);
}
/**
* 回滚事务:删除当前事务已经插入表中的数据
* @throws Exception
*/
public static void rollbackDBAction() throws Exception{
MsgFunction.writeLog("原事务操作失败,进行事务回滚操作开始,当前时间:"+MsgFunction.getNow(null));
if(localNewActions.get()!=null){
ArrayList list=(ArrayList)localNewActions.get();
for(int i=0;i<list.size();i++){
String[] action=(String[])list.get(i);
String delSql=genDelSql(action);
delAction(delSql);
}
localNewActions.remove();
}
MsgFunction.writeLog("事务回滚操作成功,当前时间:"+MsgFunction.getNow(null));
}
/**
* 根据传入的交易数组,生成删除的SQL语句
* @param action
* @return
* @throws Exception
*/
private static String genDelSql(String[] action) throws Exception{
String sql="";
if(action!=null){
String table=action[0];
String fields=action[1];
String values=action[2];
String types=action[3];
sql="delete "+table+" where ";
String[] field=fields.split(",");
String[] value=values.split(",");
String[] type=types.split(",");
String condSql="";
for(int i=0;i<field.length;i++){
if(i>0){
condSql+="and ";
}
if(type[i].equals("Number")){
condSql+=field[i]+"="+value[i]+" ";
}else{
condSql+=field[i]+"='"+value[i]+"' ";
}
}
sql=sql+condSql;
}
MsgFunction.writeLog("生成删除SQL语句:"+sql+",当前时间:"+MsgFunction.getNow(null));
return sql;
}
/**
* 根据SQL语句,删除当前action
* @param sql
* @throws Exception
*/
private static void delAction(String sql) throws Exception{
MsgFunction.writeLog("开始执行删除,当前时间:"+MsgFunction.getNow(null));
/*以下方法由用户自己实现*/
DBUtil.executeSql(sql);
}
}
四、后记
事务控制还可以采用JTA的实现控制,JTA主要用于分布式的多个数据源的两阶段提交的事务,主要用于基于EJB的控制,而EJB本身就是基于分布式操作的;而我当前的应用是非分布式的应用,作为报文的收发及处理,应该用不到JTA事务控制,就算用到并不一定跑得起来。
但是我现在有一个问题就是当前的报文服务需要和多个系统同时交互,即收到一个系统的报文就转给另外一个系统处理,有时一笔交易会涉及到五个系统,这怎么样保证这个事务的完整性,并且有些系统是自己内部本身有事务,所以到目前为止还没有想到一个好的方案。
本文出自:冯立彬的博客