背景:
问题:在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;
}