6.6.利用封装的通用类DbManager,如何使用事务操作数据库。

1.需求场景 【6.6.利用封装的通用类DbManager,如何使用事务操作数据库。】在执行某些数据库操作时,经常要用到事务处理。
比如修改排序值,除了修改当前记录的排序值外,还要同步更新其他记录的排序值。要实现这样的修改,一条SQL语句是无法实现的。必须要同时执行多条SQL语句,才能正确修改排序值。再比如删除数据时,还需要同步处理其他数据,那么也涉及到多个业务数据同步更新。
在同步执行多条SQL语句时,如果不采用事务处理,就有可能导致数据的更新异常。比如有10条语句执行,在执行第6条时发生异常,此时就会返回执行失败。如果采用了事务,前面5条成功执行的数据,就会回滚还原,保持数据跟执行前一样。如果没采用事务,那么就会导致前面5条执行生效,这样数据就会发生错误。
那么如何方便的使用事务执行呢?
太极平台框架封装的数据库通用类DbManager,除了很多常规的增删改查方法外,也提供了事务执行方法。
事务执行分两种情况:
1、无需控制连接。直接将多条语句按顺序执行即可。如果中途有错误,自动回滚。
2、需要控制连接。需要手动控制数据库连接,手动回滚和关闭。比如需要先插入数据,再根据新插入的数据主键id,进行下一步的处理。
2.使用样例 2.1.无需控制连接 框架封装了事务调用方法,直接执行事务方法即可。将多条SQL语句存入列表队列,再进行批量执行。
2.1.1.源码
事务执行方法分为无参数版本和有参数版本。无参数版本源码如下。

//事务方式执行多条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; }

2.1.2.使用样例
从源码可以看到,只需要将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); }

2.2.需要控制连接 如果需要在事务执行的中途,根据某条SQL语句执行返回结果决定执行流程,那么就需要控制该数据库连接。
数据库操作类DbManager提供获取数据库连接的方法,可以获取连接后,自己控制事务执行流程。
2.2.1.源码
获取数据库连接的源码如下。会从数据库连接池中获取连接,数据库连接池则是从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(); }

2.2.2.使用样例
控制数据库连接的事务操作,样例如下。
该事务执行的功能是:复制一个页面数据,以及该页面下的所有字段。
执行过程:先执行复制页面数据。如果页面复制成功,再复制该页面的所有字段;如果页面复制失败,则需要回滚。由于新复制字段需要关联到新页面的主键id,所以必须要插入页面数据成功后,才能再复制字段。
使用方法:
  1. 使用DbManager.getConnection()获取数据库连接;
  2. 关闭自动提交功能:conn.setAutoCommit(false);
  3. 调用QueryRunner的各种方法,执行相应的功能。QueryRunner有各种增删改查功能;
  4. 如果执行失败,则执行回滚:conn.rollback(),再开启自动提交功能:conn.setAutoCommit(true);
  5. 如果执行异常,也会回滚,在还原自动提交设置,最后会抛出异常。
  6. 如果顺利执行成功。则提交事务,还原自动提交设置。
  7. 最后无论执行成功还是失败,都会调用finally里面的关闭连接。(这里我不太确定从数据库连接池中获取的连接,关闭后是否是真实关闭连接?关闭后数据库连接池是否会重新发起新的连接?但不管是否真实关闭,不会影响到数据库功能)
//复制页面 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; }

以上样例,说明了使用框架封装的数据库通用类,如何使用事务操作。

    推荐阅读