ThreadLocal实现同一业务多SQL之间的事务

背景:

问题:在DML的封装中,每调用一次Dao层方法就操作一次数据库,每次操作数据库创建一个连接对象conn,这个连接对象在数据库操作结束后会被销毁。这使得同一业务的多次数据库操作不在同一个事务内,如何解决。

分析:我们应该让一个业务对应一个连接对象,我们知道,一个业务对应于一个线程,我们只要实现了一个线程中对应于一个连接对象,就实现了一个业务对应一个连接对象。

解决:我们可以使用ThreadLocal实现一个线程内只有一个连接对象的需求。

实现

1、在工具类中初始化一个静态的ThreadLocal对象,泛型为Connection。

2、在getConnection()的方法中,通过ThreadLocal对象调用get()方法尝试获取连接,获取到则直接返回连接对象,获取不到则创建连接对象再返回。

3、DML封装中,不开启、不关闭、不回滚事务,不关闭连接,这些操作放在service层。封装的方法异常向上抛出。

4、DQL封装中,也不关闭连接,方法内的异常外抛。

5、service层,先获取连接对象,开启事务,执行业务的所有SQL(通过到调用封装的DML或者DQL),SQL执行失败回滚,然后再关闭连接对象。

 

工具类

package com.util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * 			com.util包下其他三个工具类对单SQL的增删改查关流进行了封装,但是同一业务多SQL之间的事务问题,
 * 无法通过那三个工具类进行封装,本类的设计初衷即是为了解决这一问题。
 * 
 * 本类要求service层在获取连接后应该手动开启事务管理,执行SQL语句后手动提交事务,事务执行失败回滚事务,
 * 事务结束关闭Connection对象流 
 * 
 * @author 二百四十九先森
 *
 */
public  final class AffairDBUtil {
	/*
	 * 声明ThreadLocal对象,用一解决同一业务中多SQL之间的事务问题
	 */
	private static  ThreadLocal<Connection> thread=new ThreadLocal<>();
	/*
	 * jdbc变量
	 */
	private  static String driver;
	private  static String url;
	private  static String user;
	private  static String password;
	/*
	 * 构造器私有,不允许外部创建对象,外部只能够通过类名调用本类的方法
	 */
	private AffairDBUtil() {
		
	}
	/**
	 *  静态代码块为jdbc变量赋值
	 * 因为静态代码块最先执行,所以调用getConnection()方法时,
	 * 该方法内部的jdbc变量就完成了赋值操作
	 */
	static {
		InputStream in=null;//声明一个字节输入流
		Properties ps=null;//声明Properties对象,读取文件中的键值对		
		try {
			in=AffairDBUtil.class.getResourceAsStream("/db.properties");//动态获取配置文件的路径
			ps=new Properties();//创建Properties对象
			ps.load(in);//加载键值对信息
			/*
			 * 获取配置文件中键值对的value值,getProperty(String key)方法,传入key,返回value
			 */
			driver=ps.getProperty("driver");
			url=ps.getProperty("url");
			user=ps.getProperty("user");
			password=ps.getProperty("password");
			/*
			 * 加载驱动,静态代码块只执行一次,驱动只加载一次(加载驱动很耗性能的)
			 */
			Class.forName(driver);//加载驱动			
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}finally {
			//关流
			AffairDBUtil.close(in);
		}
	}
	
	/**
	 * 本方法采用的是PreparedStatement方法对数据库进行操作,位置参数用?充当占位符进行占位
	 * 注意传入的参数与占位符的位置要一一对应
	 * 
	 * 该方法只能对已知表进行操作,及在DML的SQL语句中应该有表名
	 * 
	 * @param sql   要执行的SQL语句
	 * @param objs  SQL语句中的参数
	 * @return   成功执行返回1,否则返回null
	 * @throws SQLException 
	 */
	public static Integer executeDML(String sql,Object...objs) throws SQLException{
		//声明jdbc变量
		Connection conn =null;
		PreparedStatement ps =null;
		Integer i =null;
		try{			
			conn = AffairDBUtil.getConnection();
			ps = conn.prepareStatement(sql);
			//给占位符赋值
			if(objs!=null) {
				for(int j=0;j<objs.length;j++) {
					ps.setObject((j+1), objs[j]);
				}
			}
			//执行SQL语句
			i = ps.executeUpdate();
		}finally {
			/*
			 * 关流
			 * Connection流不能关,关了多个SQL之间使用的就不是同一个Connection对象
			 * 也就无法实现同一业务中多SQL之间的事务操作了
			 */
			AffairDBUtil.close(ps);
		}			
		return i;
		
	}
	/**
	 * 本方法中调用Date类型变量的setter方法时使用的是java.sql.Date,
	 * 所以实体类在声明Date类型变量时一定声明成java.sql.Date
	 * 至少Date类型变量对应的setter方法的形参必须是java.sql.Date,否则报错
	 * 
	 * @param sql   要执行的查询语句
	 * @param t      实体类对象
	 * @param objs    SQL语句中的参数
	 * @return       装有实体类对象的list集合
	 * @throws Exception    
	 */
	public static <T> List<T>  executeQuery(String sql,T t,Object...objs) throws Exception{
		//声明jdbc变量
		List<T> list=new ArrayList<>();
		Connection conn = null;
		PreparedStatement ps =null;
		ResultSet rs =null;
		try {
			conn = AffairDBUtil.getConnection();
			ps = conn.prepareStatement(sql);
			//给占位符赋值
			if(ps!=null) {
				for(int i=0;i<objs.length;i++) {
						ps.setObject((i+1), objs[i]);
				}
			}
			//执行sql语句
			rs = ps.executeQuery();
			//获取结果集中字段的所有信息
			ResultSetMetaData rm = rs.getMetaData();
			int columnCount = rm.getColumnCount();//获取字段数
			//遍历结果集
			while(rs.next()) {
					Class<? extends Object> cla = t.getClass();//获取类对象
					T newInstance=(T)cla.newInstance();//获取类的对象
					//一个for循环封装一条记录的所有值
					for(int i=0;i<columnCount;i++) {
						 String columnName = rm.getColumnName((i+1));//获取字段名
						//获取字段对应的setter方法
						 String methodName="set"+columnName.substring(0, 1).toUpperCase()+columnName.substring(1);
						 String columnClassName = rm.getColumnClassName((i+1));//获取字段java类型的完全限定名	
						 //创建方法对象
						 Method method = cla.getDeclaredMethod(methodName, Class.forName(columnClassName));
						 method.invoke(newInstance,rs.getObject(columnName));//调用setter方法,执行对象属性赋值
					}
					list.add(newInstance);//将对象加入集合
			}
		} finally {
			/*
			 * 关流
			 * Connection流不能关,关了多个SQL之间使用的就不是同一个Connection对象
			 * 也就无法实现同一业务中多SQL之间的事务操作了
			 */
			AffairDBUtil.close(rs,ps);
		}
		return list;
		
	}	

	/**
	 * 通过本方法客获取一个MySQL数据库的Connection对象
	 * 
	 * @return Connection对象
	 */
	public static Connection getConnection() {
		Connection conn = thread.get();//尝试获取ThreadLocal对象中存储的Connection对象
		try {
			if(conn==null) {//如果ThreadLocal中没有Connection对象,则创建并加入ThreadLocal对象中
				conn = DriverManager.getConnection(url, user, password);
				thread.set(conn);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return conn;//返回jdbc连接
	}
	/**
	 * 关流的方法,接收任意多个任意类型的流对象
	 * 如果关闭的流对象有关闭的先后顺序
	 * 请将要先关闭的流对象放在前方
	 * 
	 * 所有流对象的顶级父接口都是AutoCloseable
	 * @param t    要关闭的流对象,可以是一个或多个(也可以是零个)
	 * 						
	 */
	public static <T>void close(T...t){
			//循环关流
			for(T tmp:t) {
							//关闭流对象
							if(tmp instanceof AutoCloseable) {			
									try {
										((AutoCloseable)tmp).close();
									} catch (Exception e) {
										e.printStackTrace();
									}
							}											
			}
	}
}

Service层代码

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Scanner;

import com.dao.Dao;
import com.dao.DaoImpl;
import com.pojo.Mobile;
import com.util.AffairDBUtil;

public class ServiceImpl implements Service {
	private Dao dao=new DaoImpl();
	private Scanner sc=new Scanner(System.in);
	/*
	 * 1、手机信息录入
	 */
	@Override
	public String signInService() {
		Connection conn = AffairDBUtil.getConnection();
		String result="录入失败";
		try {
			conn.setAutoCommit(false);
			Mobile mobile=new Mobile();
			//获取用户输入并赋值给mobile对象的属性
			System.out.println("请输入手机品牌:");
			mobile.setBrand(sc.next());
			System.out.println("请输入手机型号:");
			mobile.setModel(sc.next());
			System.out.println("请输入手机价格:");
			mobile.setPrice(sc.nextDouble());
			System.out.println("请输入手机数量:");
			mobile.setCount(sc.nextInt());
			System.out.println("请输入手机版本:");
			mobile.setVersion(sc.next());
			/*
			 * 测试第一次能否成功插入
			 */
			Integer i=dao.signInDao(mobile);
			int m=5/0;//报错
			Integer j=dao.signInDao(mobile);
			//提交事务
			conn.commit();
			//事务提交成功则返回录入成功
			result="录入成功";
		} catch (SQLException e) {
			e.printStackTrace();
			//回滚
			try {
				conn.rollback();
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}finally {
			//事务执行完毕才能关闭conn流对象
			AffairDBUtil.close(conn);
		}	
		return result;	
	}

Dao层

public interface Dao {
	/*
	 * 1、手机信息录入
	 */
	Integer signInDao(Mobile mobile)throws SQLException;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二百四十九先森

你的打赏是我努力的最大动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值