在执行某些数据库操作时,经常要用到事务处理。
比如修改排序值,除了修改当前记录的排序值外,还要同步更新其他记录的排序值。要实现这样的修改,一条SQL语句是无法实现的。必须要同时执行多条SQL语句,才能正确修改排序值。再比如删除数据时,还需要同步处理其他数据,那么也涉及到多个业务数据同步更新。
在同步执行多条SQL语句时,如果不采用事务处理,就有可能导致数据的更新异常。比如有10条语句执行,在执行第6条时发生异常,此时就会返回执行失败。如果采用了事务,前面5条成功执行的数据,就会回滚还原,保持数据跟执行前一样。如果没采用事务,那么就会导致前面5条执行生效,这样数据就会发生错误。
那么如何方便的使用事务执行呢?
太极平台框架封装的数据库通用类DbManager,除了很多常规的增删改查方法外,也提供了事务执行方法。
事务执行分两种情况:
1、无需控制连接。直接将多条语句按顺序执行即可。如果中途有错误,自动回滚。
2、需要控制连接。需要手动控制数据库连接,手动回滚和关闭。比如需要先插入数据,再根据新插入的数据主键id,进行下一步的处理。
框架封装了事务调用方法,直接执行事务方法即可。将多条SQL语句存入列表队列,再进行批量执行。
事务执行方法分为无参数版本和有参数版本。无参数版本源码如下。
//事务方式执行多条SQL语句,无参数。
public static int[] executeTransaction(List listSql) throws SQLException {
Connection conn = getConnection();
if (conn == null) {
return new int[]{0};
}
int[] rows = new int[listSql.size()];
try {
//开启事务
conn.setAutoCommit(false);
QueryRunner queryRunner = new QueryRunner();
for (int i = 0; i < listSql.size(); i++) {
String sql = listSql.get(i);
SystemOutPrintDebug(sql);
rows[i] = queryRunner.update(conn, sql);
}
//正常执行后,提交事务
conn.commit();
conn.setAutoCommit(true);
} catch (Exception e) {
//异常回滚
conn.rollback();
conn.setAutoCommit(true);
//抛出异常
throw e;
} finally {
//关闭连接
conn.close();
}
return rows;
}
有参数的事务执行,源码如下。参数要以二维数组的方式传入。
//事务方式执行多条SQL语句,每条语句可指定参数。
public static int[] executeTransaction(List listSql, Object[][] params) throws SQLException {
Connection conn = getConnection();
if (conn == null) {
return new int[]{0};
}
int[] rows = new int[listSql.size()];
try {
//开启事务
conn.setAutoCommit(false);
QueryRunner queryRunner = new QueryRunner();
for (int i = 0; i < listSql.size(); i++) {
String sql = listSql.get(i);
Object[] param = params[i];
SystemOutPrintDebug(sql, param);
rows[i] = queryRunner.update(conn, sql, param);
}
//正常执行后,提交事务
conn.commit();
conn.setAutoCommit(true);
} catch (Exception e) {
//异常回滚
conn.rollback();
conn.setAutoCommit(true);
//抛出异常
throw e;
} finally {
//关闭连接
conn.close();
}
return rows;
}
事务执行返回的是数组,代表每条SQL语句执行影响的行数。如果需要获取影响的总行数,可以调用下面的方法,总计总行数。
//将数组求和。此处也主要用于事务返回的结果求和。
public static int sumIntArray(int[] values) {
if (values == null) {
return 0;
}
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
从源码可以看到,只需要将SQL语句列表传入该方法即可。如果发生异常,会执行回滚,也会将该异常向上抛出。
如下代码样例,实现的是删除数据后,要同步更新该条数据后面数据的排序值(都减1)。在其实现方法上,则是先更新排序值,再删除数据。采用事务进行执行,将两条SQL语句放入列表,并且组织参数。
如果不需要参数,则调用无参数方法。
//删除普通列表数据,之后要更新后面的排序值
public int deleteWidgetDataUpdateOrderNum(NoCodePage noCodePage, int dataId) throws SQLException {
if (noCodePage == null || dataId <= 0) {
return 0;
}
List listSql = new ArrayList<>();
Object[][] params = {
{dataId},
{dataId}
};
// 更新排序值(所有在其后面的节点,排序值减去一)
listSql.add("update " + noCodePage.getTableName() + " set OrderNum = OrderNum - 1 where OrderNum > (select t1.OrderNum from (select OrderNum from "
+ noCodePage.getTableName() + " where Id=?) t1)");
// 删除节点,及其所有子孙节点
listSql.add("delete from " + noCodePage.getTableName() + " where Id=?");
// 事务批量执行
int[] rows = DbManager.executeTransaction(listSql, params);
return DbManager.sumIntArray(rows);
}
如果需要在事务执行的中途,根据某条SQL语句执行返回结果决定执行流程,那么就需要控制该数据库连接。
数据库操作类DbManager提供获取数据库连接的方法,可以获取连接后,自己控制事务执行流程。
获取数据库连接的源码如下。会从数据库连接池中获取连接,数据库连接池则是从dbconn.properties文件中加载数据库配置信息后,自动创建数据源。
//获取conn连接。如果为null,那么从数据源中获取
public static Connection getConnection() throws SQLException {
//已存在,直接返回
if (mConnection != null) {
return mConnection;
}
//初始化
if (mDataSource == null) {
getDataSource();
}
//初始化失败
if (mDataSource == null) {
return null;
}
return mDataSource.getConnection();
}
控制数据库连接的事务操作,样例如下。
该事务执行的功能是:复制一个页面数据,以及该页面下的所有字段。
执行过程:先执行复制页面数据。如果页面复制成功,再复制该页面的所有字段;如果页面复制失败,则需要回滚。由于新复制字段需要关联到新页面的主键id,所以必须要插入页面数据成功后,才能再复制字段。
使用方法:
//复制页面
public int duplicatePage(int pageId) throws SQLException {
//获取数据库连接
Connection conn = DbManager.getConnection();
if (conn == null) {
return 0;
}
int newPageId;
try {
//开启事务
conn.setAutoCommit(false);
QueryRunner queryRunner = new QueryRunner();
//插入页面数据
Object[] paramsPage = new Object[]{pageId};
String sqlPage = "insert into qd_taiji_widget (WidgetName,WidgetType,TableName,DataName,CheckAuthority,AuthorityTag,AllowAdd,AllowEdit,AllowDelete,AllowView,AllowSearch,\n" +
"AllowOrder,AllowExport,AllowImport,HasTrigger,CustomToolbar,PageSize,LimitCount,ListShowTitle,ListShowNo,ListShowCheckBox,ListTableCss,PageStyle,\n" +
"ListOrderBy,ListSqlWhere,ListLeftJoin,AllowCustomSQL,CustomSQLQuery,ListAppendCode,ModalWidth,ModalHeight,AddShowTitle,\n" +
"AddPageStyle,AddAppendCode,EditShowTitle,EditPageStyle,EditCondition,EditAppendCode," +
"ViewShowTitle,ViewPageStyle,ViewAppendCode,DeleteCondition,DeleteActionUrl,ImportNeedTruncate,ImportSameData,ImportActionUrl)" +
" select '复制页面',WidgetType,TableName,DataName,CheckAuthority,AuthorityTag,AllowAdd,AllowEdit,AllowDelete,AllowView,AllowSearch,\n" +
"AllowOrder,AllowExport,AllowImport,HasTrigger,CustomToolbar,PageSize,LimitCount,ListShowTitle,ListShowNo,ListShowCheckBox,ListTableCss,PageStyle,\n" +
"ListOrderBy,ListSqlWhere,ListLeftJoin,AllowCustomSQL,CustomSQLQuery,ListAppendCode,ModalWidth,ModalHeight,AddShowTitle,\n" +
"AddPageStyle,AddAppendCode,EditShowTitle,EditPageStyle,EditCondition,EditAppendCode," +
"ViewShowTitle,ViewPageStyle,ViewAppendCode,DeleteCondition,DeleteActionUrl,ImportNeedTruncate,ImportSameData,ImportActionUrl" +
" from qd_taiji_widget where Id=?";
BigInteger rowId = queryRunner.insert(conn, sqlPage, new ScalarHandler<>(), paramsPage);
newPageId = rowId.intValue();
//执行失败,就回滚。还原自动提交设置。
if (newPageId <= 0) {
conn.rollback();
conn.setAutoCommit(true);
return 0;
}
//插入字段数据
Object[] paramsField = new Object[]{newPageId, pageId};
String sqlField = "insert into qd_taiji_widgetfield (FieldTitle,FieldName,WidgetId,OrderNum,FieldType,FieldRemark,FieldFormat,FieldParam," +
"ListItem,TotalItem,AliasItem,AddItem,EditItem,ViewItem,ReadOnlyItem,RequiredItem,ForbidRepeatItem,SearchItem,ExportItem,ImportItem," +
"ListMinWidth,ListMaxLength,ListAlign,InputLength,InputWidth,InputHeight,InputLabelWidth,ExportWidth,Placeholder,InputTips,DefaultValue)\n" +
" select FieldTitle,FieldName,?,OrderNum,FieldType,FieldRemark,FieldFormat,FieldParam," +
"ListItem,TotalItem,AliasItem,AddItem,EditItem,ViewItem,ReadOnlyItem,RequiredItem,ForbidRepeatItem,SearchItem,ExportItem,ImportItem," +
"ListMinWidth,ListMaxLength,ListAlign,InputLength,InputWidth,InputHeight,InputLabelWidth,ExportWidth,Placeholder,InputTips,DefaultValue\n" +
" from qd_taiji_widgetfield where WidgetId=?";
queryRunner.insert(conn, sqlField, new ScalarHandler<>(), paramsField);
//正常执行后,提交事务
conn.commit();
conn.setAutoCommit(true);
} catch (Exception e) {
//异常回滚
conn.rollback();
conn.setAutoCommit(true);
//抛出异常
throw e;
} finally {
//关闭连接
conn.close();
}
return newPageId;
}
以上样例,说明了使用框架封装的数据库通用类,如何使用事务操作。