- 回顾
- 使用工具类查询表
需求: 查询student表的所有数据,把数据封装到一个集合中
- 数据准备
#创建表
CREATE TABLE student(
sid INT,
name VARCHAR(100),
age INT,
sex VARCHAR(100)
)
#插入数据
INSERT INTO student VALUES(1,'张三',18,'女'),(2,'李四',19,'男'),(3,'王五',20,'女'),(4,'赵六',21,'男')
- 创建工程,导入jar包和工具类
连接池配置文件内容:
driverClassName = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/day05pre
username = root
password = root
initialSize = 5
maxActive = 10
minIdle = 3
maxWait = 60000
德鲁伊连接池工具类
import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; public class DruidUtil { //1.创建一个连接池对象 private static DataSource dataSource; //静态代码创建,这样第一次使用这个类的时候就可以直接创建DataSource对象了 static{ try { //读取Druid.properties文件中的数据 创建连接池对象 InputStream is = DruidUtil.class.getClassLoader().getResourceAsStream("Druid.properties"); //创建properties集合载入流中数据 Properties pro = new Properties(); pro.load(is); //Druid工具载入pro集合中的数据 创建数据源对象 dataSource = DruidDataSourceFactory.createDataSource(pro); } catch (Exception e) { e.printStackTrace(); } } //2.创建方法 返回一个连接 public static Connection getConn() throws SQLException{ return dataSource.getConnection(); } //3.关闭所有资源 public static void closeAll(ResultSet rs, PreparedStatement pst, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (pst != null) { try { pst.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } |
- 创建student类
这个类的字段与数据库字段对应
public class Student { private int sid; private String name; private int age; private String sex; 构造 set/get toString } |
- 创建测试类书写查询代码
/* * 查询student类中的所有的信息 展示到控制台上 * * */ @Test public void selectAll() throws Exception{ //1.获取链接 Connection conn = DruidUtil.getConn(); System.out.println("Demo04 ======>selectAll() ======> 获取链接完毕 conn= "+conn); //2.通过链接获取SQL的发射器 String sql = "select * from student"; PreparedStatement pst = conn.prepareStatement(sql); //3.发射SQL语句 得到结果集 ResultSet rs = pst.executeQuery(); System.out.println("Demo04 ======>selectAll() ======> 发射完毕 rs= "+rs); //创建一个集合 List<Student> list = new ArrayList<Student>(); //4.处理结果集 while(rs.next()){ Student s = new Student(rs.getInt("sid"), rs.getString("name"), rs.getInt("age"), rs.getString("sex")); list.add(s); } //5.关闭资源 DruidUtil.closeAll(rs, pst, conn); //遍历 for(Student stu: list){ System.out.println(stu); } } |
- JDBC转账案例
-
- 需求:
完成转账功能
-
- 实现思路:
-
- 实现步骤
- 数据准备
创建数据表和数据
# 创建账号表
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
money DOUBLE
);
# 初始化数据
INSERT INTO account VALUES (NULL,'张三',10);
INSERT INTO account VALUES (NULL,'李四',10);
- Dao层
创建两个方法分别实现取钱和存钱
import java.sql.Connection; import java.sql.PreparedStatement; import com.czxy.util.DruidUtil; public class AccountDao { /* * 从指定的账户中取钱(减钱) * */ public void outMoney(String name,int money) throws Exception{ //1.获取链接 Connection conn = DruidUtil.getConn(); //2.发射器 String sql = "UPDATE account SET money=money-? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 DruidUtil.closeAll(null, pst, conn); } /* * 从指定的账户中存钱(加钱) * */ public void inMoney(String name,int money) throws Exception{ //1.获取链接 Connection conn = DruidUtil.getConn(); //2.发射器 String sql = "UPDATE account SET money=money+? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 DruidUtil.closeAll(null, pst, conn); } } |
- Service层
创建一个方法完成转账业务
import com.czxy.dao.AccountDao; public class AccountService { /** * 实现转账 * @param srcName : 钱的来源 * @param descName : 钱的去向 * @param money : 钱数 * */ public void transfer(String srcName,String descName,int money){ AccountDao ad = new AccountDao(); try { // -钱 ad.outMoney(srcName, money); // +钱 ad.inMoney(descName, money); System.out.println("转账完毕 "); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } |
- 测试类
创建一个测试类 ,书写main方法 实现转账
public class Demo02 { public static void main(String[] args) { //创建Service对象 AccountService as = new AccountService(); //张三 给 李四 转2块钱 as.transfer("张三", "李四", 2); } } |
- 测试
执行前:
执行后:
-
- 遇到问题
如果转账的中间出现了bug,很容易导致A账户的钱减少了,但是B账户的钱没有增加。这会造成事故,不是我们期望看到的。
下面代码在Service层模拟转账过程出现问题。
效果如下:
转账前:
执行代码:
转账出错结果:
-
- 处理思路
- 核心思路:
让转账的两个动作:减钱,加钱 必须同时成功或者是同时失败。
- 可选技术:
数据库的事务。
- 事务的概述
- 事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败.
- 事务作用:保证一组操作要么全都成功,对数据库进行完整更新。要么在某一个动作失败的时候让数据恢复原状,不会引起不完整的修改。
- MySQL事务的操作
sql语句 | 描述 |
start transaction; | 开启事务 |
commit; | 提交事务(完整更新) |
rollback; | 回滚事务(恢复原状) |
-
- MYSQL中可以有两种方式进行事务的管理:
- 自动提交:MySql默认自动提交。即执行一条sql语句提交一次事务。
- 手动提交:先开启,再提交
- 方式1:手动提交(当执行手动提交的时候自动提交会暂停)
- MYSQL中可以有两种方式进行事务的管理:
start transaction;
update account set money=money-1000 where name='守义';
update account set money=money+1000 where name='凤儿';
commit;
#或者
rollback;
-
- 方式2:自动提交,通过修改mysql全局变量“autocommit”进行控制
show variables like '%commit%';
* 设置自动提交的参数为OFF:
set autocommit = 0; -- 0:OFF 1:ON
- &bsp;&bsp;测试利用事物实现转账1块钱 顺利完成情况
START TRANSACTION; -- 开启事物
-- 执行一组操作
UPDATE account SET money=money-1 WHERE NAME='张三';
UPDATE account SET money=money+1 WHERE NAME='李四';
COMMIT; -- 提交事物
执行前:
执行后:
- &bsp;&bsp;测试利用事物实现转账1块钱 出现问题并回滚(恢复原状)
# 测试利用事物实现转账1块钱
START TRANSACTION; -- 开启事物
-- 执行一组操作
UPDATE account SET money=money-1 WHERE NAME='张三';
-- 下一句发生错误
UPDATE account SET money=money+1 WHERE NAME &……&%&……¥(*&* ='李四';
ROLLBACK; -- 回滚(恢复原状)
执行前:
执行如下两句:
执行效果:
执行下面一句
这一组操作的第二个给李四加钱执行失败了 ,李四的钱并不会改变
此时一组操作没有全部成功,需要回滚来让数据恢复原状
- &bsp;JDBC事务操作
Connection对象的方法名 | 描述 |
conn.setAutoCommit(false) | 设置关闭自动提交,(开启事务) |
conn.commit() | 提交事务 |
conn.rollback() | 回滚事务 |
利用如下模板解决问题
//事务模板代码
public void demo01() throws SQLException{
// 获得连接
Connection conn = ...;
try {
//#1关闭自动提交事物(开始事务)
conn.setAutoCommit(false);
//.... 加钱 ,减钱
//#2 手动提交事务
conn.commit();
} catch (Exception e) {
//#3 手动回滚事务
conn.rollback();
} finally{
// 释放资源
conn.close();
}
}
-
- 解决问题
- Dao层
把原来的两个方法进行修改,使用Service层传递过来的conn对象,并且执行完毕不要关闭链接
/* * 从指定的账户中取钱(减钱) * */ public void outMoney(String name,int money,Connection conn) throws Exception{ //2.发射器 String sql = "UPDATE account SET money=money-? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 只关闭结果集不关闭链接 DruidUtil.closeAll(null, pst, null); } /* * 从指定的账户中存钱(加钱) * */ public void inMoney(String name,int money,Connection conn) throws Exception{ //2.发射器 String sql = "UPDATE account SET money=money+? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 只关闭结果集不关闭链接 DruidUtil.closeAll(null, pst, null); } |
- Service层
获取连接,关闭自动提交变成手动提交,一组动作成功则手动提交事务,一旦有异常则回滚,最后无论异常与否都要关闭连接
public void transfer(String srcName,String descName,int money){ AccountDao ad = new AccountDao(); Connection conn =null; try { //获取链接 conn = DruidUtil.getConn(); //把自动提交关闭,变成手动提交 conn.setAutoCommit(false); // -钱 传递连接对象 ad.outMoney(srcName, money,conn); //制造一个bug , 模拟转账出现问题 int a=1/0; // +钱 传递连接对象 ad.inMoney(descName, money,conn); //转账成功则手动提交事物 conn.commit(); System.out.println("转账完毕 "); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); if(conn!=null){ try { //操作失败 回滚 conn.rollback(); System.out.println("执行了回滚 ,把数据恢复原状 "); } catch (SQLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } }finally { if(conn!=null){ try { //关闭链接 conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } |
- 测试
- 正常情况:
把 如下代码注释上
执行前:
转账2块钱执行完毕
执行结果
- 异常情况:
保留如下代码
执行前:
执行效果
执行后:数据恢复原状,问题解决
- 理论补充
事务特性:ACID
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency)事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
并发访问问题
如果不考虑隔离性,事务存在3种并发访问问题。
- 脏读:一个事务读到了另一个事务未提交的数据.
- 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
- 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。(数据量不同)
严重性: 脏读 > 不可重复读 >虚读(幻读)
设置隔离级别:解决问题
- 数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
- read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
- 存在:3个问题(脏读、不可重复读、虚读)。
- 解决:0个问题
效率最高,引发所有读问题
基本不设置
- read committed 读已提交,一个事务读到另一个事务已经提交的数据。
- 存放:2个问题(不可重复读、虚读)。
- 解决:1个问题(脏读)
如果要 效率,那么选择这个read committed
- repeatable read :可重复读,在一个事务中读到的数据信息始终保持一致,无论另一个事务是否提交。
- 存放:1个问题(虚读)。
- 解决:2个问题(脏读、不可重复读)
如果 要求安全,选择这个repeatable read
虚读的问题可以通过程序来规避:
- 事务刚开启时,可以count(*)
- 事务要关闭时,可以count(*)
- 比对,如果两次数据一致,说明没有虚读
- serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
- 存放:0个问题。
- 解决:1个问题(脏读、不可重复读、虚读)
没有效率,安全性最高,基本不设置
- 安全和性能对比
- 安全性:serializable > repeatable read > read committed > read uncommitted
- 性能 : serializable < repeatable read < read committed < read uncommitted
- 常见数据库的默认隔离级别:
- MySql:repeatable read 安全,本身做的优化比较好
- Oracle:read committed 效率