day04.连接池-DBUtils-事务
课前回顾:
1.函数:
数学函数
流程控制函数
日期函数
2.JDBC:
a.注册驱动:Class.forName("com.mysql.cj.jdbc.Driver")
b.获取连接:DriverManager.getConnection(url,name,password)
url: jdbc:mysql://localhost:3306/数据库名字?时区&批量添加
name:mysql用户名
password:mysql密码
c.获取执行平台:Connection接口中的方法:
Statement createStatement()
d.执行sql:Statement中的方法:
int executeUpdate(sql)->针对于增删改操作
ResultSet executeQuery(sql)->针对于查询操作
e.处理结果集:
针对于增删改操作不用处理结果集
针对于查询操作用处理结果集
boolean next()-> 判断结果集中有没有下一个元素
getxxx(列名)->获取指定列名的数据
getxxx(第几列)->获取第几列数据
f.关闭资源:统一用close方法
3.预处理对象:PreparedStatement 是 Statement的子接口
a.特点:支持占位符?
b.setxxx(第几个?,赋值)
c.获取:Connection中的方法
preparedStatement(sql)
d.执行sql:
int executeUpdate()->针对于增删改操作
ResultSet executeQuery()->针对于查询操作
今日重点:
1.会使用Druid(德鲁伊)连接池,C3p0连接池
2.会使用DBUtils工具包操作数据库
3.会使用DBUtils中的ResultSetHandlder下面的子类处理结果集
4.会为代码添加事务
第一章.PreparedStatement预处理对象
CREATE TABLE category(
cid INT PRIMARY KEY AUTO_INCREMENT,
cname VARCHAR(10)
);
1.mysql批量添加数据
properties配置文件中的url加上:rewriteBatchedStatements=true
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/220227_java4?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=root
1.在设置完所有要添加的参数,调用PreparedStatement中的addBatch(),将SQL语句添加到PreparedStatement中
2.调用PreparedStatement中的executeBatch()方法批处理sql语句
/**
* 批量添加
*/
@Test
public void batchAdd()throws Exception{
//获取连接
Connection conn = JDBCUtils.getConn();
//准备sql
String sql = "insert into category (cname) values (?)";
//获取执行平台
PreparedStatement pst = conn.prepareStatement(sql);
for (int i = 0; i < 100; i++) {
pst.setObject(1,"箱包"+i);
/*
void addBatch()
将一组参数添加到此 PreparedStatement 对象的批处理命令中
*/
pst.addBatch();
}
/*
批量执行
executeBatch() 将一批命令提交给数据库来执行,如果全部命令执行成功
*/
pst.executeBatch();
//关闭资源
JDBCUtils.close(conn,pst,null);
}
第二章.连接池
1.问题:
之前的jdbc操作,每执行一个操作,就要获取一条连接对象(Connection对象),用完还要销毁,这样会耗费内存资源
2.过程:
池子创建之后,如果来了任务,池子中有连接对象,就直接用,用完还回去
不用频繁的去创建连接对象,销毁连接对象
1.连接池之C3p0
1.java为连接池提供了一个标准,公共接口:javax.sql.DataSource,不同的厂商如果想要实现自己的连接池,就需要实现DataSource接口
2.常用的连接池:
C3p0, Druid(最常用)
1.导jar包:c3p0-0.9.1.2.jar
2.在resources下面创建c3p0-config.xml->名字不能错
3.在xml中配置相关信息:
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/220212_java4?serverTimezone=UTC</property>
<property name="user">root</property>
<property name="password">root</property>
<!--
连接池参数
初始连接数(initialPoolSize):刚创建好连接池的时候准备的连接数量
最大连接数(maxPoolSize):连接池中最多可以放多少个连接
最大等待时间(checkoutTimeout):连接池中没有连接时最长等待时间
最大空闲回收时间(maxIdleTime):连接池中的空闲连接多久没有使用就会回收
-->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">2000</property>
<property name="maxIdleTime">1000</property>
</default-config>
</c3p0-config>
初始连接数(initialPoolSize):刚创建好连接池的时候准备的连接数量
最大连接数(maxPoolSize):连接池中最多可以放多少个连接
最大等待时间(checkoutTimeout):连接池中没有连接时最长等待时间
最大空闲回收时间(maxIdleTime):连接池中的空闲连接多久没有使用就会回收
4.编写工具类:
接口:DataSource
c3p0连接池实现类:ComboPooledDataSrouce
获取连接,是在连接池中获取:ComboPooledDataSrouce.getConnection()
/*
读取配置文件方式的工具类
*/
public class JDBCUtils_C3P0 {
//定义一个DataSource接口
private static DataSource dataSource;
static {
//创建DataSource接口的实现类,此实现类是C3P0的连接池实现类对象
/*
C3P0的连接池实现类对象会自动扫描resources资源包或者src下的c3p0-config.xml
自动解析
*/
dataSource = new ComboPooledDataSource();
}
/**
* 获取连接
*/
public static Connection getConn() {
//从连接池中获取连接
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
/**
* 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
*/
public static void close(Connection connection, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
@Test
public void add()throws Exception{
//获取连接->从连接池中获取的
Connection conn = JDBCUtils_C3P0.getConn();
//准备sql
String sql = "insert into category(cname) values (?)";
//获取预处理对象
PreparedStatement pst = conn.prepareStatement(sql);
//为?赋值
pst.setObject(1,"蔬菜");
//执行sql
int i = pst.executeUpdate();
//关闭资源
JDBCUtils_C3P0.close(conn,pst,null);
}
2.连接池之Druid(德鲁伊)
1.概述:Druid连接池是阿里巴巴开发的
2.好处:性能好,抗造
3.使用:
a.到jar包:druid-1.1.10.jar
b.在resources或者src下编写properties配置文件->druid.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/220227_java4?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=root
c.编写属于Druid的工具类
DruidDataSourceFactory.createDataSource(Properties集合);返回的是DataSource接口的实现类
public class DruidUtils {
//定义一个DataSource接口
private static DataSource dataSource;
static {
try {
//创建Properties集合
Properties properties = new Properties();
InputStream in = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(in);
//利用DruidDataSourceFactory,根据properties集合创建属于 Druid的连接池对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接
*/
public static Connection getConn() {
//从连接池中获取连接
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
/**
* 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
*/
public static void close(Connection connection, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
@Test
public void add()throws Exception{
//获取连接->从连接池中获取的
Connection conn = DruidUtils.getConn();
//准备sql
String sql = "insert into category(cname) values (?)";
//获取预处理对象
PreparedStatement pst = conn.prepareStatement(sql);
//为?赋值
pst.setObject(1,"水果");
//执行sql
int i = pst.executeUpdate();
//关闭资源
DruidUtils.close(conn,pst,null);
}
第三章.DBUtils工具包
1.准备工作
2.DBUtils的介绍
1.为什么要学DBUtils:使用原生的jdbc开发,代码很多,比较恶心,会影响我们的开发效率,而DBUtils可以大大的简化JDBC的开发
2.什么是DBUtils:是简化jdbc开发步骤的一个开发工具包
3.使用:
a.导jar包:commons-dbutils-1.6.jar
4.DBUtils工具包中三大核心对象:
a.QueryRunner:定义了执行sql的方法-> 主要用的
b.ResultSetHandlder:有很多处理结果集的对象 -> 主要用的
c.Dbutils:是DBUtils工具包中的一个类,里面定义了很多关闭资源以及操作事务等方法
3.QueryRunner
3.1.空参的QueryRunner的介绍以及使用
1.QueryRunner()
a.特点:需要我们自己维护连接对象(需要自己获取连接对象,释放连接对象),支持sql中使用占位符?
2.方法:
a.int update(Connection conn, String sql, Object... params)->执行sql,针对于增删改操作
conn:连接对象
sql:sql语句
params:可变参数,给sql中的?赋值
第一个值,就是给第一个?赋值,自动匹配
第二个值,就是给第二个?赋值,自动匹配
比如:update(conn,"insert into category(cid,cname) values (?,?)",1,"蔬菜")
Object[] obj = {1,"蔬菜"}
update(conn,"insert into category(cid,cname) values (?,?)",obj)
b. T query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)->针对于查询语句执行
conn:连接对象
sql:sql语句
rsh:处理结果集的方式
params:可变参数,给sql中的?赋值
第一个值,就是给第一个?赋值,自动匹配
第二个值,就是给第二个?赋值,自动匹配
public class Test01 {
@Test
public void insert()throws Exception{
//创建空参的QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
//准备sql
String sql = "insert into category(cname) values (?)";
//执行sql
//qr.update(conn,sql,"箱包");
/*
由于update方法中的第三个参数是可变参数
所以我们可以在外面定义一个Object数组
将要给?赋的值放到数组中,然后再将数组
放到update方法中
*/
Object[] obj = {"手机"};
qr.update(conn,sql,obj);
//关闭资源
DruidUtils.close(conn,null,null);
}
@Test
public void add()throws Exception{
//创建空参的QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
//执行sql
qr.update(conn,"insert into category(cname) values (?)","电脑");
//关闭资源
DruidUtils.close(conn,null,null);
}
}
3.2.有参QueryRunner的介绍以及使用
1.使用有参QueryRunner特点:
不需要我们自己维护连接,QueryRunner会自动维护连接对象
说白了就是不需要自己从连接池中获取连接,也不需要我们自己关闭或者归还连接池
2.构造:
QueryRunner(DataSource ds)
DataSource:传递连接池对象
3.方法:
int update(String sql, Object... params)->针对于增删改操作
sql:sql语句
params:可变参数,给sql中的?赋值
第一个值,就是给第一个?赋值,自动匹配
第二个值,就是给第二个?赋值,自动匹配
T query(String sql, ResultSetHandler<T> rsh, Object... params)
sql:sql语句
rsh:处理结果集的方式
params:可变参数,给sql中的?赋值
第一个值,就是给第一个?赋值,自动匹配
第二个值,就是给第二个?赋值,自动匹配
//改造工具类
public class DruidUtils {
//定义一个DataSource接口
private static DataSource dataSource;
static {
try {
//创建Properties集合
Properties properties = new Properties();
InputStream in = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(in);
//利用DruidDataSourceFactory,根据properties集合创建属于 Druid的连接池对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接
*/
public static Connection getConn() {
//从连接池中获取连接
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
/**
* 新加的方法
* 获取连接池对象
*/
public static DataSource getDataSource(){
return dataSource;
}
/**
* 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
*/
public static void close(Connection connection, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
public class Test02 {
@Test
public void insert()throws Exception{
//创建QueryRunner对象
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
//准备sql
String sql = "insert into category (cname) values (?)";
//执行sql
qr.update(sql,"鼠标");
}
@Test
public void delete()throws Exception{
//创建QueryRunner对象
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
//执行sql
qr.update("delete from category where cid = ?",1);
}
}
4.ResultSetHandler结果集
4.1.标准的JavaBean
什么是JavaBean:实体类
开发的过程中:JavaBean中的成员变量要和具体表中的字段要对应
一个标准的JavaBean如何编写:成员变量 构造 get/set 方法 hashcode和equals方法 toString
此类还需要实现Serializable
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Category {
private Integer cid;
private String cname;
}
1.要求:定义javabean的时候里面的字段类型都要是引用数据类型,基本类型的字段也要写成包装类
2.问题:为什么定义javabean的时候,要使用包装类呢?
a.因为包装类的默认值为NULL,如果我们的主键是自增,sql语句可以写成:
insert into category(cid,cname) values (NULL,'蔬菜');
b.使用包装类,如果有需要的话,可以直接调用包装类中的方法操作字段数据
4.2.BeanHandler
1.作用:将查询出来的结果集中的第一行数据封装成javabean对象
2.方法:
query(String sql, ResultSetHandler<T> rsh, Object... params)>有参QueryRunner时使用
query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)->空参QueryRunner时使用
3.构造:
BeanHandler(Class type)
传递的class对象其实就是我们想要封装的javabbean类的class对象
想将查询出来的数据封装成哪个javabean对象,就写哪个javabean的class对象
4.怎么理解:
将查询出来的数据为javabean中的成员变量赋值
@Test
public void beanHandler()throws Exception{
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
Category category = qr.query("select * from category", new BeanHandler<>(Category.class));
System.out.println(category);
}
4.3.BeanListHandler
1.作用:将查询出来的结果每一条数据都封装成一个一个的javabean对象,将这些javabean对象放到List集合中
2.构造:
BeanListHandler(Class type)
传递的class对象其实就是我们想要封装的javabbean类的class对象
想将查询出来的数据封装成哪个javabean对象,就写哪个javabean的class对象
3.怎么理解:
将查询出来的多条数据封装成多个javabean对象,将多个javabean对象放到List集合中
@Test
public void beanListHandler()throws Exception{
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
List<Category> list = qr.query("select * from category", new BeanListHandler<>(Category.class));
for (Category category : list) {
System.out.println(category);
}
}
4.4.ScalarHandler
1.作用:主要是处理单值的查询结果的,执行的select语句,结果集只有1个
2.构造:
ScalarHandler(int columnIndex)->不常用->指定第几列
ScalarHandler(String columnName)->不常用->指定列名
ScalarHandler()->常用
3.注意:
ScalarHandler和聚合函数使用更有意义
@Test
public void scalarHandler()throws Exception{
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
//Object o1 = qr.query("select * from category", new ScalarHandler<>(2));
//Object o2 = qr.query("select * from category", new ScalarHandler<>("cname"));
//System.out.println(o2);
Object o3 = qr.query("select count(*) from category", new ScalarHandler<>());
System.out.println(o3);
}
4.5.ColumnListHandler
1.作用:将查询出来的结果中的某一列数据,存储到List集合中
2.构造:
ColumnListHandler(int columnIndex)->指定第几列
ColumnListHandler(String columnName)->指定列名
ColumnListHandler()-> 默认显示查询结果中的第一列数据
3.注意:
ColumnListHandler可以指定泛型类型,如果不指定,返回的List泛型就是Object类型
@Test
public void ColumnListHandler()throws Exception{
QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
//List<Object> list = qr.query("select * from category", new ColumnListHandler<>());
//List<Object> list = qr.query("select * from category", new ColumnListHandler<>(2));
List<Object> list = qr.query("select * from category", new ColumnListHandler<>("cname"));
for (Object o : list) {
System.out.println(o);
}
}
第四章.事务
1.事务
1.1.事务_转账分析图
CREATE TABLE account(
`name` VARCHAR(10),
money INT
);
1.2.实现转账(不加事务)
public class Test01Transfer {
@Test
public void transfer()throws Exception{
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
//减钱的sql
String sqlOut = "update account set money= money-1000 where name = ?";
//加钱的sql
String sqlIn = "update account set money= money+1000 where name = ?";
//执行sql
qr.update(conn,sqlOut,"涛哥");
//制造错误,模仿断网
//System.out.println(1/0);
qr.update(conn,sqlIn,"茜茜");
//关闭资源
DruidUtils.close(conn,null,null);
}
}
1.3.事务的介绍
1.作用:主要是用于管理一组操作的,让这组操作(一组操作可能包含了多条sql),要么全部成功,要么全部失败
2.注意:
mysql自带事务,但是这个事务一次只能管理一条sql,如果想要事务管理多条sql,需要将mysql自带事务关闭,开启手动事务
3.如何手动管理事务? Connection对象中的方法
void setAutoCommit(false)->关闭mysql自带事务,开启手动事务
void commit() -> 提交事务->当事务一旦提交,管理的sql数据将永久保存,回不去了
void rollback() -> 回滚事务->如果事务不提交(commit)的话,调用此方法,可以将数据还原回去
4.注意:
以上三个方法,必须是同一条连接对象调用,不然无法完成手动事务管理
总结:
1.事务可以管理多条sql.使其要么全成功,要么全失败
2.mysql自带事务管理,但是一个事务只管理一条sql
3.所以我们如果想让事务管理一组操作,需要关闭mysql自带事务,开启手动事务
4.开启事务,提交事务,回滚事务的方法都需要同一个Connection对象调用
1.4.DBUtils实现转账(添加事务)
public class Test01Transfer {
@Test
public void transfer() throws SQLException {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
try{
//开启手动事务
conn.setAutoCommit(false);
//减钱的sql
String sqlOut = "update account set money= money-1000 where name = ?";
//加钱的sql
String sqlIn = "update account set money= money+1000 where name = ?";
//执行sql
qr.update(conn,sqlOut,"涛哥");
//制造错误,模仿断网
System.out.println(1/0);
qr.update(conn,sqlIn,"茜茜");
//提交事务
conn.commit();
System.out.println("转账成功");
}catch (Exception e){
//回滚事务
conn.rollback();
System.out.println("转账失败");
e.printStackTrace();
}finally {
//关闭资源
DruidUtils.close(conn,null,null);
}
}
}
1.5.mysql中操作事务_了解
1.开启事务:begin
2.提交事务:commit
3.回滚事务:rollback
-- 开启事务
BEGIN;
UPDATE account SET money = money-1000 WHERE `name` = '涛哥';
UPDATE account SET money = money+1000 WHERE `name` = '茜茜';
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
1.6.分层思想介绍以及架构搭建
三层架构思想: 表现层(web) 业务层(service) 持久层(dao)
创建package:包名都是小写单词;公司域名倒着写
com.atguigu.web-> 表现层,和页面打交道,这里面的类用于接收请求和回响应
com.atguigu.service->业务层,写业务
com.atguigu.dao->持久层,和数据库打交道
com.atguigu.pojo->javabean实体类
com.atguigu.utils-> 工具类
1.6.1.转账_web层实现
public class AccountController {
public static void main(String[] args) {
//创建Scanner
Scanner sc = new Scanner(System.in);
System.out.println("请您输入出钱的人:");
String outName = sc.next();
System.out.println("请您输入收钱的人:");
String inName = sc.next();
System.out.println("请您输入转账金额:");
int money = sc.nextInt();
//将outName,inName,money传递到service层
AccountService accountService = new AccountService();
accountService.transfer(outName,inName,money);
}
}
温馨小提示:
开发中对一个方法的注释:一般用文档注释
1.6.2.转账_service层实现
public class AccountService {
/**
* @param outName 出钱人姓名
* @param inName 收钱人姓名
* @param money 转账金额
*/
public void transfer(String outName, String inName, int money) {
//直接将数据传递给dao
AccountDao accountDao = new AccountDao();
try{
//减钱
accountDao.outMoney(outName,money);
//System.out.println(1/0);
//加钱
accountDao.inMoney(inName,money);
System.out.println("转账成功");
}catch (Exception e){
System.out.println("转账失败");
e.printStackTrace();
}
}
}
1.6.3.转账_dao层实现
public class AccountDao {
/**
* 转钱方法
* @param outName 出钱人姓名
* @param money 金额
*/
public void outMoney(String outName, int money) throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
//准备sql
String outSql = "update account set money = money-? where name = ?";
//执行sql
qr.update(conn,outSql,money,outName);
//关闭资源
DruidUtils.close(conn,null,null);
}
/**
* 加钱方法
* @param inName 收钱人姓名
* @param money 金额
*/
public void inMoney(String inName, int money)throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//获取连接
Connection conn = DruidUtils.getConn();
//准备sql
String inSql = "update account set money = money+? where name = ?";
//执行sql
qr.update(conn,inSql,money,inName);
//关闭资源
DruidUtils.close(conn,null,null);
}
}
1.6.4.在service层添加事务(传递连接)
public class AccountService {
/**
* @param outName 出钱人姓名
* @param inName 收钱人姓名
* @param money 转账金额
*/
public void transfer(String outName, String inName, int money) {
//直接将数据传递给dao
AccountDao accountDao = new AccountDao();
//获取连接
Connection conn = DruidUtils.getConn();
try{
//开启手动事务
conn.setAutoCommit(false);
//减钱
accountDao.outMoney(conn,outName,money);
System.out.println(1/0);
//加钱
accountDao.inMoney(conn,inName,money);
//提交事务
conn.commit();
System.out.println("转账成功");
}catch (Exception e){
//回滚事务
try {
conn.rollback();
System.out.println("转账失败");
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
DruidUtils.close(conn,null,null);
}
}
}
1.6.5.修改dao层代码
public class AccountDao {
/**
* 转钱方法
* @param outName 出钱人姓名
* @param money 金额
*/
public void outMoney(Connection conn,String outName, int money) throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//准备sql
String outSql = "update account set money = money-? where name = ?";
//执行sql
qr.update(conn,outSql,money,outName);
}
/**
* 加钱方法
* @param inName 收钱人姓名
* @param money 金额
*/
public void inMoney(Connection conn,String inName, int money)throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//准备sql
String inSql = "update account set money = money+? where name = ?";
//执行sql
qr.update(conn,inSql,money,inName);
}
}
1.问题描述:service层只是处理业务的,而获取连接对象这种事儿是service应该干的吗?不是
既然获取连接对象这种不在service层干,那么dao层的连接对象怎么办?
2.解决问题:给service和dao层找一个"小秘书",让这个"小秘书"去获取连接对象,然后将获取的连接对象分别传给service层和dao层
还要保证service层和dao层的连接对象是同一个,不然事务不能生效
3.问题描述:
如果在多线程的情况下,一个用户就相当于一个线程,如何保证多个用户(多个线程)不会使用同一条连接对象?
2.ThreadLocal
2.1.ThreadLocal基本使用和原理
1.概述:可以看做是一个容器,专门放变量或者对象的
2.方法:
void set(T value)-> 往ThreadLocal中存数据
T get()->从ThreadLocal中获取数据
3.特点:
a.ThreadLocal中获取的元素都是最后一次存进去的,一次只能存一个数据
b.一个线程往TheadLocal中存的数据,其他线程获取不到
在一条线程中使用的ThreadLocal中存储的对象,是同一个对象
4.作用:
保证一条线程上使用的资源是同一个
5.ThreadLocal底层实质上是一个Map集合
key:当前正在执行的线程对象->Thread.currentThread()
value:随意->往ThreadLocal中存的数据
这样,存数据的时候ThreadLocal底层直接将当前线程存的值和当前线程直接绑死,所以别的线程拿不到值
public class Test01 {
public static void main(String[] args) {
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("涛哥");
tl.set("三上");
tl.set("松下");
System.out.println(tl.get());
//使用了另外一个线程
new Thread(()-> System.out.println(tl.get())).start();//null
}
}
2.2.连接对象管理类(小秘书)
/*
使用ThreadLocal类,将连接对象存进去
保证一个用户使用一个连接对象
*/
public class ConnectionManager {
//定义一个ThreadLocal对象
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
/**
* 定义方法,从连接池中获取连接对象,存到ThreadLocal中
* 然后将ThreadLocal中的连接对象返回
*/
public static Connection getConnection()throws Exception{
//从ThreadLocal中获取连接对象,看看里面有没有Connection对象
Connection conn = tl.get();
//判断,如果获取出来的连接对象为null,证明之前没有在ThreadLocal中存
if (conn==null){
//从连接池中获取一条连接对象,存入ThreadLocal中
conn = DruidUtils.getDataSource().getConnection();
tl.set(conn);
}
return conn;
}
}
public class AccountService {
/**
* @param outName 出钱人姓名
* @param inName 收钱人姓名
* @param money 转账金额
*/
public void transfer(String outName, String inName, int money) {
//直接将数据传递给dao
AccountDao accountDao = new AccountDao();
Connection conn = null;
try{
conn = ConnectionManager.getConnection();
//开启手动事务
conn.setAutoCommit(false);
//减钱
accountDao.outMoney(outName,money);
System.out.println(1/0);
//加钱
accountDao.inMoney(inName,money);
//提交事务
conn.commit();
System.out.println("转账成功");
}catch (Exception e){
//回滚事务
try {
conn.rollback();
System.out.println("转账失败");
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
DruidUtils.close(conn,null,null);
}
}
}
public class AccountDao {
/**
* 转钱方法
* @param outName 出钱人姓名
* @param money 金额
*/
public void outMoney(String outName, int money) throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//利用连接管理类获取连接对象
Connection conn = ConnectionManager.getConnection();
//准备sql
String outSql = "update account set money = money-? where name = ?";
//执行sql
qr.update(conn,outSql,money,outName);
}
/**
* 加钱方法
* @param inName 收钱人姓名
* @param money 金额
*/
public void inMoney(String inName, int money)throws Exception {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//利用连接管理类获取连接对象
Connection conn = ConnectionManager.getConnection();
//准备sql
String inSql = "update account set money = money+? where name = ?";
//执行sql
qr.update(conn,inSql,money,inName);
}
}
3.事务的特性以及隔离级别
3.1.事务特性:ACID
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency)事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以设置隔离级别,但是效率会非常低。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
3.2 并发访问问题
如果不考虑隔离性,事务存在3中并发访问问题。
- 脏读:一个事务读到了另一个事务未提交的数据.
- 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
- 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。
3.3 隔离级别:解决问题
- 数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
-
read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
a)存在:3个问题(脏读、不可重复读、虚读)。
b)解决:0个问题
-
read committed 读已提交,一个事务读到另一个事务已经提交的数据。
a)存在:2个问题(不可重复读、虚读)。
b)解决:1个问题(脏读)
-
repeatable read:可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。
a)存在:1个问题(虚读)。
b)解决:2个问题(脏读、不可重复读)
4.serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
a)存在:0个问题。
b)解决:3个问题(脏读、不可重复读、虚读)
- 安全和性能对比
- 安全性:
serializable > repeatable read > read committed > read uncommitted
- 性能 :
serializable < repeatable read < read committed < read uncommitted
- 安全性:
- 常见数据库的默认隔离级别:
- MySql:
repeatable read
- Oracle:
read committed
- MySql:
3.4 演示
- 隔离级别演示参考:资料/隔离级别操作过程.doc【增强内容,了解】
- 查询数据库的隔离级别
show variables like '%isolation%';
或
select @@tx_isolation;
- 设置数据库的隔离级别
set session transactionisolation level
级别字符串- 级别字符串:
readuncommitted
、read committed
、repeatable read
、serializable
- 例如:
set session transaction isolation level read uncommitted;
- 读未提交:readuncommitted
- A窗口设置隔离级别
- AB同时开始事务
- A 查询
- B 更新,但不提交
- A 再查询?-- 查询到了未提交的数据
- B 回滚
- A 再查询?-- 查询到事务开始前数据
- A窗口设置隔离级别
- 读已提交:read committed
- A窗口设置隔离级别
- AB同时开启事务
- A查询
- B更新、但不提交
- A再查询?–数据不变,解决问题【脏读】
- B提交
- A再查询?–数据改变,存在问题【不可重复读】
- A窗口设置隔离级别
- 可重复读:repeatable read
- A窗口设置隔离级别
- AB 同时开启事务
- A查询
- B更新, 但不提交
- A再查询?–数据不变,解决问题【脏读】
- B提交
- A再查询?–数据不变,解决问题【不可重复读】
- A提交或回滚
- A再查询?–数据改变,另一个事务
- A窗口设置隔离级别
- 串行化:serializable
- A窗口设置隔离级别
- AB同时开启事务
- A查询
- B更新?–等待(如果A没有进一步操作,B将等待超时)
- A回滚
- B 窗口?–等待结束,可以进行操作
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency)事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以设置隔离级别,但是隔离级别越高,效率会非常低。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
如果不考虑隔离性,事务存在3中并发访问问题。(如果隔离级别低,事务跟事务之间有可能互相影响)
1. 脏读:一个事务读到了另一个事务未提交的数据.
2. 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
3. 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。
总结:
我们最理想的状态是:一个事务和其他事务互不影响
但是如果不考虑隔离级别的话,就会出现多个事务之间互相影响
而事务互相影响的表现方式为:
脏读
不可重复读
虚读/幻读