草庐IT

【JAVA高级】——吃透JDBC中的事务及事务的封装

Java Fans 2023-10-04 原文

✅作者简介:热爱国学的Java后端开发者,修心和技术同步精进。
🍎个人主页:乐趣国学的博客
🍊个人信条:不迁怒,不贰过。小知识,大智慧。
💞当前专栏:JAVA开发者成长之路
✨特色专栏:国学周更-心性养成之路
🥭本文内容:【JAVA高级】——吃透JDBC中的事务及事务的封装
更多内容点击👇
【JAVA高级】——初识JDBC中DAO数据访问对象

文章目录


💖 事务

在JDBC中,获得Connection对象来处理事务–提交或回滚事务–关闭连接。其事务策略是

  • connection.setAutoCommit(false):true等价于1,false等于0
  • connection.commit():手动提交事务
  • connection.rollback():手动回滚事务

✨ service层控制事务

package com.cxyzxc.examples07;

import java.sql.Connection;
import java.sql.SQLException;

public class AccountServiceImpl {

	/**
	 * 转账业务
	 * 
	 * @param fromNo
	 *            转账人账号
	 * @param password
	 *            转账人账号密码
	 * @param toNo
	 *            收款人账号
	 * @param money
	 *            转账金额
	 */
	public String transfer(String fromNo, String password, String toNo,
			double money) {
		String result = "转账失败";
		AccountDaoImpl accountDaoImpl = new AccountDaoImpl();

		// 创建一个连接对象
		Connection connection = null;

		try {
			// 获取连接对象
			connection = DBUtils.getConnection();
			// 开启事务,关闭事务的自动提交,改为手动提交
			connection.setAutoCommit(false);
			// 1.验证fromNo账号是否存在
			Account fromAccount = accountDaoImpl.select(fromNo);
			if (fromAccount == null) {
				throw new RuntimeException("卡号不存在");
			}

			// 2.验证fromNo的密码是否正确
			if (!fromAccount.getPassword().equals(password)) {
				throw new RuntimeException("密码错误");
			}

			// 3.验证余额是否充足
			if (fromAccount.getBalance() < money) {
				throw new RuntimeException("余额不足");
			}

			// 4.验证toNo账号是否存在
			Account toAccount = accountDaoImpl.select(toNo);
			if (toAccount == null) {
				throw new RuntimeException("对方卡号不存在");
			}
			// 5.减少fromNo账号的余额
			fromAccount.setBalance(fromAccount.getBalance() - money);
			accountDaoImpl.update(fromAccount);

			// 程序出现异常
			int num = 10 / 0;

			// 6.增加toNo账号的余额
			toAccount.setBalance(toAccount.getBalance() + money);
			accountDaoImpl.update(toAccount);
			// 代码执行到这里,说明转账成功,提交事务
			connection.commit();
			result = "转账成功";
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			try {
				// 出现异常,回滚整个事务
				System.out.println("出现异常,回滚整个事务,转账失败");
				connection.rollback();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
		}finally{
			DBUtils.closeAll(connection, null, null);
		}
		return result;
	}
}

✨ service层控制事务失败的原因

执行这个代码,观察account表中的数据发现,当程序出现异常,转账账号余额减少了,但是收款账户余额没有增加,事务控制失败了。失败的原因是:

  • AccountServiceImpl类中的connection连接对象与AccountDaoImpl类中给各个方法里的connection连接对象是不同的connection连接对象。
  • AccountServiceImpl类中的connection连接对象控制事务,只能控制AccountServiceImpl类中的事务,不能控制该类之外的类里的事务

✨ 解决方案一:传递Connection

为了解决AccountServiceImpl类中的connection连接对象与AccountDaoImpl类中给各个方法里的connection连接对象是不同步的问题,可以将Connection对象通过service传递给AccountDaoImpl类中的各个方法

💫 AccountDaoImpl类代码

AccountDaoImpl类中的每个方法参数列表里都要添加一个Connection类型的参数

package com.cxyzxc.examples08;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class AccountDaoImpl {

	// 新增:插入一个Account对象到数据库中
	public int insert(Account account,Connection connection) {
		PreparedStatement preparedStatement = null;

		String sql = "insert into account values(?,?,?,?)";

		try {
			preparedStatement = connection.prepareStatement(sql);

			// 绑定参数
			preparedStatement.setString(1, account.getCardNo());
			preparedStatement.setString(2, account.getPassword());
			preparedStatement.setString(3, account.getName());
			preparedStatement.setDouble(4, account.getBalance());

			// 执行SQL
			int result = preparedStatement.executeUpdate();
			return result;
		} catch (SQLException e) {
			e.printStackTrace();
		} 
		return 0;
	}

	// 删除:根据卡号,删除账号
	public int delete(String cardNo,Connection connection) {
		PreparedStatement preparedStatement = null;

		String sql = "delete from account where cardNo = ?;";
		try {
			preparedStatement = connection.prepareStatement(sql);
			// 绑定参数
			preparedStatement.setString(1, cardNo);
			// 执行SQL
			int result = preparedStatement.executeUpdate();
			return result;
		} catch (SQLException e) {
			e.printStackTrace();
		} 
		return 0;
	}

	// 修改
	public int update(Account account,Connection connection) {
		PreparedStatement preparedStatement = null;

		String sql = "update account set password = ?,name = ?,balance = ? where cardNo=?;";

		try {
			preparedStatement = connection.prepareStatement(sql);

			// 绑定参数
			preparedStatement.setString(1, account.getPassword());
			preparedStatement.setString(2, account.getName());
			preparedStatement.setDouble(3, account.getBalance());
			preparedStatement.setString(4, account.getCardNo());

			// 执行SQL
			int result = preparedStatement.executeUpdate();
			return result;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return 0;
	}

	// 查询单个
	public Account select(String cardNo,Connection connection) {
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;
		Account account = null;


		String sql = "select * from account where cardNo = ?";

		try {
			preparedStatement = connection.prepareStatement(sql);

			// 绑定参数
			preparedStatement.setString(1, cardNo);

			// 执行SQL
			resultSet = preparedStatement.executeQuery();

			if (resultSet.next()) {
				String cardNumber = resultSet.getString("cardNo");
				String password = resultSet.getString("password");
				String name = resultSet.getString("name");
				double balance = resultSet.getDouble("balance");

				account = new Account(cardNumber, password, name, balance);
			}

			return account;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return null;
	}

	// 查询所有
	public List<Account> selectAll(Connection connection) {
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;
		Account account = null;
		List<Account> accountList = new ArrayList<Account>();

		String sql = "select * from account;";

		try {
			preparedStatement = connection.prepareStatement(sql);

			// 执行SQL
			resultSet = preparedStatement.executeQuery();

			while (resultSet.next()) {
				String cardNumber = resultSet.getString("cardNo");
				String password = resultSet.getString("password");
				String name = resultSet.getString("name");
				double balance = resultSet.getDouble("balance");

				account = new Account(cardNumber, password, name, balance);
				accountList.add(account);
			}
			return accountList;
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return null;
	}
}

💫 AccountServiceImpl类代码

package com.cxyzxc.examples08;

import java.sql.Connection;
import java.sql.SQLException;

public class AccountServiceImpl {

	/**
	 * 转账业务
	 * 
	 * @param fromNo
	 *            转账人账号
	 * @param password
	 *            转账人账号密码
	 * @param toNo
	 *            收款人账号
	 * @param money
	 *            转账金额
	 */
	public String transfer(String fromNo, String password, String toNo,
			double money) {
		String result = "转账失败";
		AccountDaoImpl accountDaoImpl = new AccountDaoImpl();

		// 创建一个连接对象
		Connection connection = null;

		try {
			// 获取连接对象
			connection = DBUtils.getConnection();
			// 开启事务,关闭事务的自动提交,改为手动提交
			connection.setAutoCommit(false);
			// 1.验证fromNo账号是否存在
			Account fromAccount = accountDaoImpl.select(fromNo,connection);
			if (fromAccount == null) {
				throw new RuntimeException("卡号不存在");
			}

			// 2.验证fromNo的密码是否正确
			if (!fromAccount.getPassword().equals(password)) {
				throw new RuntimeException("密码错误");
			}

			// 3.验证余额是否充足
			if (fromAccount.getBalance() < money) {
				throw new RuntimeException("余额不足");
			}

			// 4.验证toNo账号是否存在
			Account toAccount = accountDaoImpl.select(toNo,connection);
			if (toAccount == null) {
				throw new RuntimeException("对方卡号不存在");
			}
			// 5.减少fromNo账号的余额
			fromAccount.setBalance(fromAccount.getBalance() - money);
			accountDaoImpl.update(fromAccount,connection);

			// 程序出现异常
			int num = 10 / 0;

			// 6.增加toNo账号的余额
			toAccount.setBalance(toAccount.getBalance() + money);
			accountDaoImpl.update(toAccount,connection);
			// 代码执行到这里,说明转账成功,提交事务
			connection.commit();
			result = "转账成功";
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			try {
				// 出现异常,回滚整个事务
				System.out.println("出现异常,回滚整个事务,转账失败");
				connection.rollback();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
		}finally{
			DBUtils.closeAll(connection, null, null);
		}
		return result;
	}
}

💫 测试

测试发现,这种方法可以解决service层控制事务失败的问题。

💫 解决方案的弊端

(1)如果使用传递Connection,更容易造成接口污染(BadSmell)。

(2)定义接口是为了更容易更换实现,而将Connection定义在接口中(XxxDao接口,XXXDaoImpl实现XxxDao接口)中,会造成污染当前接口。因为在当前代码中连接对象叫Connection,而在其它数据库连接框架中,连接对象不叫Connection(Mybatis框架中数据库连接对象叫SqlSession,Hibernate框架中数据库连接对象叫session),这时候,你需要重新定义接口,重新传递连接对象。

✨ 解决方案二:ThreadLocal

(1)在整个线程中(单线程),存储一个共享值(Connection对象)。

(2)线程类中拥有一个类似Map的属性(),以键值对的结构<ThreadLocal对象,值>存储数据。

💫 ThreadLocal应用

一个线程中所有的操作共享一个ThreadLocal,ThreadLocal里存储的是Connection连接对象,在整个操作流程中任何一个操作环节都可以设置或者获取值。

💫 ThreadLocal代码实现

在DBUtils类中,将当前Connection对象添加到ThreadLocal中。其它类代码保持不变。

package com.cxyzxc.examples09;

import java.sql.*;

public class DBUtils {
	// 声明一个ThreadLocal<Connection>对象用来存储数据库连接对象
	private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

	static {// 类加载,执行一次!
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	// 1.获取连接
	public static Connection getConnection() {
		// 将当前线程中绑定的Connection对象赋值给connection变量
		Connection connection = threadLocal.get();
		try {
			// 如果连接对象为null,则创建一个连接对象
			if (connection == null) {
				connection = DriverManager.getConnection(
						"jdbc:mysql://localhost:3306/java22182", "root",
						"123456");
				// 将创建的连接对象存储到当前线程中共享
				threadLocal.set(connection);
			}

		} catch (SQLException e) {
			e.printStackTrace();
		}
		return connection;
	}

	// 2.释放资源
	public static void closeAll(Connection connection, Statement statement,
			ResultSet resultSet) {
		try {
			if (resultSet != null) {
				resultSet.close();
			}
			if (statement != null) {
				statement.close();
			}
			if (connection != null) {
				connection.close();
				// 将connection从threadLocal中移除
				threadLocal.remove();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

💖 事务的封装

✨ 问题描述

(1)XXXDaoImpl类是专门用来操作数据表的,在这个类中只存在对数据表增删改查的方法,没有其它的内容。这是我们需要的。但是在XXXServiceImpl类中,既有业务需求,还有获取数据库连接对象以及释放资源的代码,在XXXServiceImpl类中,应该只有业务逻辑需求,除此,没有其它操作代码,这才是我们需要的。

(2)因此我们需要将对事务的开启、提交、回滚都封装到DBUtils工具类中。业务层调用DBUtils类中的与事务相关的代码即可。

✨ 完善工具类

package com.cxyzxc.examples10;

import java.sql.*;

public class DBUtils {
	// 声明一个ThreadLocal<Connection>对象用来存储数据库连接对象
	private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

	static {// 类加载,执行一次!
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	// 1.获取连接
	public static Connection getConnection() {
		// 将当前线程中绑定的Connection对象赋值给connection变量
		Connection connection = threadLocal.get();
		try {
			// 如果连接对象为null,则创建一个连接对象
			if (connection == null) {
				connection = DriverManager.getConnection(
						"jdbc:mysql://localhost:3306/java22182", "root",
						"123456");
				// 将创建的连接对象存储到当前线程中共享
				threadLocal.set(connection);
			}

		} catch (SQLException e) {
			e.printStackTrace();
		}
		return connection;
	}

	// 2.释放资源
	public static void closeAll(Connection connection, Statement statement,
			ResultSet resultSet) {
		try {
			if (resultSet != null) {
				resultSet.close();
			}
			if (statement != null) {
				statement.close();
			}
			if (connection != null) {
				connection.close();
				// 将connection从threadLocal中移除
				threadLocal.remove();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	// 3、开启事务
	public static void startTransaction() {
		Connection connection = null;
		try {
			connection = getConnection();
			connection.setAutoCommit(false);
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	// 4、提交事务
	public static void commitTransaction() {
		Connection connection = getConnection();
		try {
			connection.commit();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			DBUtils.closeAll(connection, null, null);
		}
	}

	// 5、回滚事务
	public static void rollbackTransaction() {
		Connection connection = getConnection();
		try {
			connection.rollback();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			DBUtils.closeAll(connection, null, null);
		}
	}
}

✨ AccountServiceImpl类代码修改

package com.cxyzxc.examples10;

public class AccountServiceImpl {

	/**
	 * 转账业务
	 * 
	 * @param fromNo
	 *            转账人账号
	 * @param password
	 *            转账人账号密码
	 * @param toNo
	 *            收款人账号
	 * @param money
	 *            转账金额
	 */
	public String transfer(String fromNo, String password, String toNo,
			double money) {
		String result = "转账失败";
		AccountDaoImpl accountDaoImpl = new AccountDaoImpl();

		try {

			// 开启事务,关闭事务的自动提交,改为手动提交
			DBUtils.startTransaction();
			// 1.验证fromNo账号是否存在
			Account fromAccount = accountDaoImpl.select(fromNo);
			if (fromAccount == null) {
				throw new RuntimeException("卡号不存在");
			}

			// 2.验证fromNo的密码是否正确
			if (!fromAccount.getPassword().equals(password)) {
				throw new RuntimeException("密码错误");
			}

			// 3.验证余额是否充足
			if (fromAccount.getBalance() < money) {
				throw new RuntimeException("余额不足");
			}

			// 4.验证toNo账号是否存在
			Account toAccount = accountDaoImpl.select(toNo);
			if (toAccount == null) {
				throw new RuntimeException("对方卡号不存在");
			}
			// 5.减少fromNo账号的余额
			fromAccount.setBalance(fromAccount.getBalance() - money);
			accountDaoImpl.update(fromAccount);

			// 程序出现异常
			@SuppressWarnings("unused")
			int num = 10 / 0;

			// 6.增加toNo账号的余额
			toAccount.setBalance(toAccount.getBalance() + money);
			accountDaoImpl.update(toAccount);
			// 代码执行到这里,说明转账成功,提交事务
			DBUtils.commitTransaction();
			result = "转账成功";
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			// 出现异常,回滚整个事务
			System.out.println("出现异常,回滚整个事务,转账失败");
			DBUtils.rollbackTransaction();
		}
		return result;
	}
}

💖 投票传送门

有关【JAVA高级】——吃透JDBC中的事务及事务的封装的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  9. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

  10. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

随机推荐