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

1.需求场景

在执行某些数据库操作时,经常要用到事务处理。

比如修改排序值,除了修改当前记录的排序值外,还要同步更新其他记录的排序值。要实现这样的修改,一条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;
}

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

你可能感兴趣的:(太极开发框架)