目录
一、JDBC优化及工具类封装
1.现有问题
2.JDBC工具类封装V1.0
3.ThreadLocal
4.JDBC工具类封装V2.0
二、DAO封装及BaseDAO工具类
1.BaseDAO概念
2.BaseDao层代码实现
① BaseDao层——通用的修改方法
② 通用的查询方法
③ 单行查询方法优化
三、事务
1、事务回顾
2.JDBC中事务的实现
关键代码
JDBC事务代码实现
别问、别说、别停留,去做
—— 24.9.11
本文中用到的反射相关知识点请看博主的另一博文:http://t.csdnimg.cn/2YVxD
一、JDBC优化及工具类封装
1.现有问题
我们在使用JDBC的过程中,发现部分代码存在冗余的问题
① 创建连接池。
没有必要让每个用户都独享一个连接池,而是要将连接池设置成一个整个项目中都可以共享的连接池
② 获取连接。
③ 连接的回收。
2.JDBC工具类封装V1.0
方法类
package JDBC3_Senior.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/*
JDBC工具类(V1.0)
1.维护一个连接池对象,该连接池在整个项目内都能使用
2.对外提供在连接池中获取链接的方法
3.对外提供回收链接的方法
注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法
*/
public class JDBC_util {
// 创建连接池引用,因为要提供当前项目的全局使用,所以创建成静态的
// 写成接口类型在更换连接池时,只需要更换实现而不用更换引用
private static DataSource ds;
// 在项目启动时,即创建连接池对象,赋值给ds
// 静态代码块
static{
try {
// 创建一个properties集合
Properties prop = new Properties();
// 创建一个类加载器,拿到一个字节输入流
InputStream input = JDBC_util.class.getClassLoader().getResourceAsStream("db.properties");
prop.load(input);
// 构建一个连接池对象
ds = DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 方法1:对外提供在连接池中获取链接的方法
// 维护连接池对象
public static Connection getConnection() {
try {
return ds.getConnection();
}catch (SQLException e) {
throw new RuntimeException(e);
}
}
// 方法2:对外提供回收链接的方法
// 回收链接
public static void release(Connection conn) {
try {
// 关闭链接对象
conn.close();
}catch (SQLException e){
throw new RuntimeException(e);
}
}
}
测试类
import org.junit.Test;
import java.sql.Connection;
public class JDBCUtilTest {
@Test
public void testGetConnection() {
Connection connection = JDBC_util.getConnection();
System.out.println(connection);
// CRUD
JDBC_util.release(connection);
}
}
3.ThreadLocal
JDK 1.2的版本中就提供java.lang;.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session等。
ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>,其key就是一个ThreadLocal,而Object即为该线程的共享变量。
而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get、set、remove自己的变量,而不会影响其他线程的变量。
① 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
② 线程间数据隔离。
③ 进行事务操作,用于存储线程事务信息。
④ 数据库连接, Session会话管理
1、ThreadLocal对象.get:获取ThreadLocal中当前线程共享变量的值,
2、ThreadLocal对象.set: 设置ThreadLocal中当前线程共享变量的值
3、ThreadLocal对象.remove:移除ThreadLocal中当前线程共享变量的值
4.JDBC工具类封装V2.0
方法类
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/*
JDBC工具类(V1.0)
1.维护一个连接池对象,该连接池在整个项目内都能使用,同时维护了一个线程绑定变量的ThreadLocal对象
2.对外提供在ThreadLocal中获取链接的方法
3.对外提供回收链接的方法,回收过程中,将要回收的链接从ThreadLocal之中移除
注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法
注意:使用ThreadLocal就是为了一个线程在多次数据库操作过程中,使用的是同一个链接!
*/
public class JDBC_utilV2 {
// 创建连接池引用,因为要提供当前项目的全局使用,所以创建成静态的
// 写成接口类型在更换连接池时,只需要更换实现而不用更换引用
private static DataSource ds;
// 创建一个静态的ThreadLocal对象
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// 方法1:维护一个连接池,在项目启动时,即创建连接池对象,赋值给ds
// 静态代码块
static{
try {
// 创建一个properties集合
Properties prop = new Properties();
// 创建一个类加载器,拿到一个字节输入流
InputStream input = JDBC_util.class.getClassLoader().getResourceAsStream("db.properties");
prop.load(input);
// 构建一个连接池对象
ds = DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 方法2:对外提供在连接池中获取链接的方法
// 维护连接池对象
public static Connection getConnection() {
try {
// 在ThreadLocal中获取Connection
Connection connection = threadLocal.get();
// 如果threadLocal里没有存储Connection,这就是第一次获取
if (connection == null) {
// 在连接池中获取一个对象,存储在连接池ThreadLocal里
connection = ds.getConnection();
threadLocal.set(connection);
}
return connection;
}catch (SQLException e) {
throw new RuntimeException(e);
}
}
// 方法3:对外提供回收链接的方法
// 回收链接
public static void release() {
try {
// 关闭链接对象
Connection connection = threadLocal.get();
if (connection != null) {
// 从threadLocal中移除当前已经存储的Connection对象
threadLocal.remove();
// 将Connection对象还给连接池
connection.close();
}
}catch (SQLException e){
throw new RuntimeException(e);
}
}
}
测试类
@Test
public void testJDBCV2(){
Connection con1 = JDBC_util.getConnection();
Connection con2 = JDBC_util.getConnection();
Connection con3 = JDBC_util.getConnection();
System.out.println(con1);
System.out.println(con2);
System.out.println(con3);
Connection connection1 = JDBC_utilV2.getConnection();
Connection connection2 = JDBC_utilV2.getConnection();
Connection connection3 = JDBC_utilV2.getConnection();
System.out.println(connection1);
System.out.println(connection2);
System.out.println(connection3);
}
二、DAO封装及BaseDAO工具类
DAO:Data Access 0bject,数据访问对象。
Java是面向对象语言,数据在Java中通常以对象的形式存在。一张表对应一个实体类,一张表的操作对应一个DAO对象
在Java操作数据库时,我们会将对同一张表的增删改查操作统一维护起来,维护的这个类就是DAO层。
DAO层只关注对数据库的操作,供业务层Service调用,将职责划分清楚
1.BaseDAO概念
基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,复用增删改查的基本操作,我们称为BaseDAO。
2.BaseDao层代码实现
通用的增删改的方法。
@param sql 调用者要执行的SQL语句
@param params SQL语句中的占位符要赋值的参数
@return 受影响的行数
① BaseDao层——通用的修改方法
// 通用的修改方法 数组中的可变长参数
public int executeUpdate(String sql,Object...params) throws Exception{
// 1.通过JDBCUtilV2获取数据库链接
Connection connection = JDBC_utilV2.getConnection();
// 2.预编译SQL语句,sql语句由外部传入
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.为占位符赋值,执行SQL,接收返回结果
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
// 循环操作给占位符赋值
// 占位符是从1开始的,参数数组下标从0开始
// 用setObject为类型进行赋值,object通用类型
// 参数顺序不同
preparedStatement.setObject(i+1,params[i]);
}
}
// 拿到受影响的行数
int i = preparedStatement.executeUpdate();
// 4.释放资源
preparedStatement.close();
// 如果事务开启
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
// 5.返回结果
return i;
}
② 通用的查询方法
/*
通用的查询:多行多列、单行多列、单行单列
多行多列:List<Employee>
单行多列:Employee
单行单列:封装的是一个结果:Double、Integer、......
封装过程:
1.返回的类型:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了
2.返回的结果,通用:List:存储多个结果,List.get(0):存储一个结果
3.结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class
*/
// 通用的查询方法
// 参数:Class类对象,sql语句,sql语句占位符要复制的参数
public <T> List <T> executeQuery(Class<T> clazz,String sql,Object...params) throws Exception{
// 1.获取链接
Connection connection = JDBC_utilV2.getConnection();
// 2.编译SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.设置占位符的值
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i+1,params[i]);
}
}
// 4.执行SQL,接收返回的结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 5.获取结果集中的元数据(列)对象(只在工具类中会用到)
// metadata对象中包含了列的数量,每个列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
// getColumnCount:从封装的metadata对象中拿到列数
int columnCount = metaData.getColumnCount();
// 6.准备集合存储遍历的对象
List<T> list = new ArrayList<>();
// 7.处理结果
while (resultSet.next()) {
// 循环一次代表一行数据,通过反射创建一个对象
T t = clazz.newInstance();
// 对对象的属性赋值
// getColumnCount()列的数量,循环遍历当前行的列,循环几次,就有多少列
for (int i = 1; i <= columnCount; i++) {
// 通过下标获取列的值
Object value = resultSet.getObject(i);
// 获取列的value值,这个值就是t这个对象中的某一个属性
// 获取当前拿到的列的名字 = 对象的属性
String fieldName = metaData.getColumnLabel(i);
// 通过类对象和fieldName获取封装的类对象的属性
Field field = clazz.getDeclaredField(fieldName);
// 突破封装的private,取消属性的封装检查
field.setAccessible(true);
// 以属性赋值
field.set(t,value);
}
// 将遍历得到的数据存储在集合中
list.add(t);
}
// 资源的关闭
resultSet.close();
preparedStatement.close();
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
return list;
}
③ 单行查询方法优化
/*
通用查询:在上面查询的集合结果中获取第一个结果,简化了单行单列的获取、单行多列的获取
*/
public <T> T executeQueryBean(Class<T> clazz,String sql,Object...params) throws Exception{
List<T> list = this.executeQuery(clazz,sql,params);
if (list != null || list.size() == 0) {
return null;
}
return list.get(0);
}
BaseDao层代码
package JDBC3_Senior.dao;
import JDBC3_Senior.util.JDBC_utilV2;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
// 将共性的数据库操作代码封装在BaseDao层
public class BaseDao {
/*
通用的增删改的方法。
@param sql 调用者要执行的SQL语句
@param params SQL语句中的占位符要赋值的参数
@return 受影响的行数
*/
// 通用的修改方法 数组中的可变长参数
public int executeUpdate(String sql,Object...params) throws Exception{
// 1.通过JDBCUtilV2获取数据库链接
Connection connection = JDBC_utilV2.getConnection();
// 2.预编译SQL语句,sql语句由外部传入
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.为占位符赋值,执行SQL,接收返回结果
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
// 循环操作给占位符赋值
// 占位符是从1开始的,参数数组下标从0开始
// 用setObject为类型进行赋值,object通用类型
// 参数顺序不同
preparedStatement.setObject(i+1,params[i]);
}
}
// 拿到受影响的行数
int i = preparedStatement.executeUpdate();
// 4.释放资源
preparedStatement.close();
// 如果事务开启
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
// 5.返回结果
return i;
}
/*
通用的查询:多行多列、单行多列、单行单列
多行多列:List<Employee>
单行多列:Employee
单行单列:封装的是一个结果:Double、Integer、......
封装过程:
1.返回的类型:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了
2.返回的结果,通用:List:存储多个结果,List.get(0):存储一个结果
3.结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class
*/
// 通用的查询方法
// 参数:Class类对象,sql语句,sql语句占位符要复制的参数
public <T> List <T> executeQuery(Class<T> clazz,String sql,Object...params) throws Exception{
// 1.获取链接
Connection connection = JDBC_utilV2.getConnection();
// 2.编译SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.设置占位符的值
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i+1,params[i]);
}
}
// 4.执行SQL,接收返回的结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 5.获取结果集中的元数据(列)对象(只在工具类中会用到)
// metadata对象中包含了列的数量,每个列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
// getColumnCount:从封装的metadata对象中拿到列数
int columnCount = metaData.getColumnCount();
// 6.准备集合存储遍历的对象
List<T> list = new ArrayList<>();
// 7.处理结果
while (resultSet.next()) {
// 循环一次代表一行数据,通过反射创建一个对象
T t = clazz.newInstance();
// 对对象的属性赋值
// getColumnCount()列的数量,循环遍历当前行的列,循环几次,就有多少列
for (int i = 1; i <= columnCount; i++) {
// 通过下标获取列的值
Object value = resultSet.getObject(i);
// 获取列的value值,这个值就是t这个对象中的某一个属性
// 获取当前拿到的列的名字 = 对象的属性
String fieldName = metaData.getColumnLabel(i);
// 通过类对象和fieldName获取封装的类对象的属性
Field field = clazz.getDeclaredField(fieldName);
// 突破封装的private,取消属性的封装检查
field.setAccessible(true);
// 以属性赋值
field.set(t,value);
}
// 将遍历得到的数据存储在集合中
list.add(t);
}
// 资源的关闭
resultSet.close();
preparedStatement.close();
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
return list;
}
/*
通用查询:在上面查询的集合结果中获取第一个结果,简化了单行单列的获取、单行多列的获取
*/
public <T> T executeQueryBean(Class<T> clazz,String sql,Object...params) throws Exception{
List<T> list = this.executeQuery(clazz,sql,params);
if (list != null || list.size() == 0) {
return null;
}
return list.get(0);
}
}
测试类
@Test
public void testEmployeeDao() throws Exception {
// 1.创建Dao实现类对象 多态的体现
EmployeeDao employeeDao = new EmployeeDaoImpl();
// 2.调用方法
List<Employee> employeeList = employeeDao.selectAll();
// 3.处理结果
for (Employee employee : employeeList) {
System.out.println("employee : " + employee);
}
}
三、事务
1、事务回顾
数据库事务就是一种SQL语句执行的缓存机制,不会单条执行完毕就更新数据库数据,最终根据缓存内的多条语句执行结果统一判定。一个事务内所有语句都成功即事务成功,
我们可以触发commit提交事务来结束事务,更新数据。一个事务内任意一条语句失败,即为事务失败
我们可以触发rolback回滚事务结束事务,数据回到事务之前状态
一个业务涉及多条修改数据库语句。
例如:
① 经典的转账案例,转账业务(A账户减钱和B账户加钱,要一起成功)
② 批量删除(涉及多个删除)
③ 批量添加(涉及多个插入)
事务的特性:
① 原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生, 要么都不发生。
② 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
③ 隔离性(lsolation):事务的隔离性是指一个事务的执行不能被其他事务干扰, 即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
④ 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的, 接下来的其他操作和数据库故障不应该对其有任何影响·
事务的影响
① 提交事务:让当前连接的操作,提交,对数据库产生影响,持久性的影响
② 回滚事务:让当前连接的操作,回滚到数据修改之前的状态
事务的提交方式:
① 自动提交:每条语句自动存储一个事务中,执行成功自动提交,执行失败自动回滚
② 手动提交:手动开启事务,添加语句,手动提交或者手动回滚即可。
--查看当前连接,事务的提交方式,(ON 自动提交、oFF 关闭自动提交,需要手动提交)
SHOW VARIABLES LIKE 'autocommit'
--0、false 都是设置当前连接的事务提交方式为手动提交
SET autocommit = FALSE:
--提交事务
COMMIT ;
--回滚事务
ROLLBACK;
--每一条DML语句,增删改语句都是一个事务
2.JDBC中事务的实现
关键代码
try{
connection.setAutoCommit(false); //关闭自动提交了
/*
connection.setAutoCommit(false)也就类似于SET autocommit = off
注意,只要当前connection对象,进行数据库操作,都不会自动提交事务
数据库动作:
prepareStatement - 单一的数据库动作crud
connection - 操作事务
所有操作执行正确,提交事务!
*/
connection.commit();
}catch(Execption e){
//出现异常,则回滚事务!
connection.roliback();
}
JDBC事务代码实现
设计一个ATM机存取款操作
① 数据库准备
use javawebjdbc;
CREATE TABLE t_bank(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
money INT UNSIGNED COMMENT '金额,不能为负值'
) ;
INSERT INTO t_bank(account,money) VALUES
('zhangsan',1000),('lisi',1000);
② 银行Dao层实现bankDao
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
// 将共性的数据库操作代码封装在BaseDao层
public class BaseDao {
/*
通用的增删改的方法。
@param sql 调用者要执行的SQL语句
@param params SQL语句中的占位符要赋值的参数
@return 受影响的行数
*/
// 通用的修改方法 数组中的可变长参数
public int executeUpdate(String sql,Object...params) throws Exception{
// 1.通过JDBCUtilV2获取数据库链接
Connection connection = JDBC_utilV2.getConnection();
// 2.预编译SQL语句,sql语句由外部传入
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.为占位符赋值,执行SQL,接收返回结果
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
// 循环操作给占位符赋值
// 占位符是从1开始的,参数数组下标从0开始
// 用setObject为类型进行赋值
// 参数顺序不同
preparedStatement.setObject(i+1,params[i]);
}
}
// 拿到受影响的行数
int i = preparedStatement.executeUpdate();
// 4.释放资源
preparedStatement.close();
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
// 5.返回结果
return i;
}
/*
通用的查询:多行多列、单行多列、单行单列
多行多列:List<Employee>
单行多列:Employee
单行单列:封装的是一个结果:Double、Integer、......
封装过程:
1.返回的类型,泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了
2.返回的结果,通用:List:存储多个结果,get(0):存储一个结果
3.结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class
*/
// 通用的查询方法
// 参数:Class类对象,sql语句,sql语句占位符要复制的参数
public <T> List <T> executeQuery(Class<T> clazz,String sql,Object...params) throws Exception{
// 1.获取链接
Connection connection = JDBC_utilV2.getConnection();
// 2.编译SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.设置占位符的值
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i+1,params[i]);
}
}
// 4.执行SQL,接收返回的结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 5.获取结果集中的元数据对象(只在工具类中会用到)
// metadata对象中包含了列的数量,每个列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 6.准备集合存储遍历的对象
List<T> list = new ArrayList<>();
// 7.处理结果
while (resultSet.next()) {
// 循环一次代表一行数据,通过反射创建一个对象
T t = clazz.newInstance();
// 对对象的属性赋值
// getColumnCount()列的数量,循环遍历当前行的列,循环几次,就有多少列
for (int i = 0; i < columnCount; i++) {
// 通过下标获取列的值
Object value = resultSet.getObject(i + 1);
// 获取列的value值,这个值就是t这个对象中的某一个属性
// 获取当前拿到的列的名字 = 对象的属性
String fieldName = metaData.getColumnLabel(i);
// 通过类对象和fieldName获取封装的类对象的属性
Field field = clazz.getDeclaredField(fieldName);
// 突破封装的private,取消属性的封装检查
field.setAccessible(true);
// 以属性赋值
field.set(t,value);
}
// 将遍历得到的数据存储在集合中
list.add(t);
}
// 资源的关闭
resultSet.close();
preparedStatement.close();
if (connection.getAutoCommit()){
JDBC_utilV2.release();
}
return list;
}
/*
通用查询:在上面查询的集合结果中获取第一个结果,简化了单行单列的获取、单行多列的获取
*/
public <T> T executeQueryBean(Class<T> clazz,String sql,Object...params) throws Exception{
List<T> list = this.executeQuery(clazz,sql,params);
if (list != null && list.size() == 0) {
return null;
}
return list.get(0);
}
}
测试用例
@Test
public void testTransaction() throws Exception {
// 接口引用实现链接对象
BankDao bankDao = new BankDaoImpl();
Connection con1 = null;
try{
// 1.获取链接,将连接的事务提交转变为手动提交
con1 = JDBC_utilV2.getConnection();
con1.setAutoCommit(false);// 开启事务
// 2.操作减钱
bankDao.subMoney(1,100);
// 3.操作加钱
bankDao.addMoney(2,100);
// 4.提交事务
con1.commit();
}catch (SQLException e){
try{
// 事务回滚
con1.rollback();
}catch (SQLException ex){
throw new RuntimeException(ex);
}
}finally {
JDBC_utilV2.release();
}
}
如果想在JDBC中实现事务必须用到ThreadLocal
注意:当开启事务后,一定要根据代码的执行结果try…catch来决定事务提交还是回滚,否则数据库看不到数据的操作结果