一、JDBC 事务操作
事务在 JDBC 中非常重要,没有事务是一件非常恐怖的事情,如下案例
1.1 银行转账案例
需求: 银行转账, 从凯哥账户上给赵云转 1000 块钱.
1.1.1 准备 account(账户)
id | name(账号,唯一) | balance(余额) |
1 | 凯哥 | 20000 |
2 | 赵云 | 0 |
CREATE TABLE account(id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(20),balance DECIMAL(10,2) );
INSERT INTO account(name,balance) VALUES('张无忌',20000),VALUES('赵敏',0);
1.1.2 转账操作步骤
- 查询凯哥的账户余额是否大于等于1000.
SQL : SELECT * FROM account WHERE balance >= 1000 AND name='凯哥
余额小于1000 : 温馨提示: 亲,你的余额不足.
余额大于等于1000 : GOTO 2.
- 从凯哥的账户余额中减少1000
SQL : UPDATE account SET balance = balance - 1000 WHERE name='凯哥'
- 在赵云的账户余额中增加1000
SQL : UPDATE account SET balance = balance +1000 WHERE name='赵云'
案例实现:
@Test
public void testTx() throws Exception {
// 1 查询凯哥的账户余额是否大于等于1000
Connection conn = JDBCUtil.getConnection();
String sql = "SELECT * FROM account WHERE balance>=? AND name=?";
PreparedStatement pst = conn.prepareStatement(sql);
//给?设置数据
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"凯哥");
ResultSet rs = pst.executeQuery();
if(!rs.next()){
System.out.println("余额不足");
return;
}
// 2 从凯哥的账户余额中减少1000.
sql = "UPDATE account SET balance = balance-? WHERE name=?";
pst = conn.prepareStatement(sql);
//设置? 的数据
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"凯哥");
pst.executeUpdate();
// 模拟出异常或停电
int a = 10/0;
// 3 赵云的账户余额中增加1000.
sql = "UPDATE account SET balance = balance+? WHERE name=?";
pst = conn.prepareStatement(sql);
//设置? 的数据
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"赵云");
pst.executeUpdate();
// 释放资源
JDBCUtil.close(conn,pst,rs);
}
问题: 当程序执行到第 ②步和第 ③步中间时,突然出现一个异常或停电,此时会造成凯哥减了1000,而赵云未加1000的问题.
造成这个问题的根本原因是,加减是两个单独的操作,加失败,减已经被执行,而转账业务中,需要保证加减两个操作要么都成功,要么都失败.
所以这里我们需要使用 InnoDB 存储引擎,用事务管理来解决这个问题。
1.2 JDBC 的事务操作
事务(Transaction): 简写为tx
事务是指将一组操作括为一个单元,为确保数据库中数据的一致性,数据操作是成组的单元,当单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。
1.2.1 事务的 ACID 属性
- 原子性(Atomicity): 原子在化学中,是最小单位,不可以再分割了. 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency): 保证数据的完整性. 事务必须使数据库从一个一致性状态变换到另外一个一致性状态。(数据不被破坏)
- 隔离性(Isolation): 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability): 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响
1.2.2 事务的操作步骤
- 先定义开始一个事务,然后对数据作修改操作,
- 执行过程中,如果没有问题就提交(commit)事务,此时的修改将永久地保存下来
- 如果执行过程中有问题(异常),回滚事务(rollback),数据库管理系统将放弃您所作的所有修改而回到开始事务时的状态。
1.2.3 事务的操作模板
try{
//取消事务自动提交机制,设置为手动提交
connection对象.setAutoCommit(false);
//操作1
//操作2
//异常
//操作3
//....
//所有操作成功则 手动提交事务
connection对象.commit();
}catch(Exception e){
//处理异常
//出现异常 回滚事务
connection对象.rollback();
}
@Test
public void testTx() throws Exception {
Connection conn = null;
PreparedStatement pst = null;
ResultSet rs = null;
try{
//1 查询凯哥的账户余额是否大于等于1000 conn = JDBCUtil.getConnection();
//事务设置为手动提交
conn.setAutoCommit(false);
String sql = "SELECT * FROM account WHERE balance>=? AND name=?";
pst = conn.prepareStatement(sql);
//给 ? 设置数据
pst.setBigDecimal(1,new BigDecimal("1000")); pst.setString(2,"凯哥"); rs = pst.executeQuery();
if(!rs.next()){
System.out.println("余额不足");
return;
}
//2 从凯哥的账户余额中减少1000.
sql = "UPDATE account SET balance = balance-? WHERE name=?"; pst = conn.prepareStatement(sql);
//设置 ? 的数据
pst.setBigDecimal(1,new BigDecimal("1000")); pst.setString(2,"凯哥"); pst.executeUpdate();
//模拟出异常或停电 int a = 10/0;
//3 在赵云的账户余额中增加1000.
sql = "UPDATE account SET balance = balance+? WHERE name=?"; pst = conn.prepareStatement(sql);
//设置 ? 的数据
pst.setBigDecimal(1,new BigDecimal("1000")); pst.setString(2,"赵云"); pst.executeUpdate();
//没问题,提交事务 conn.commit();
}catch(Exception e){
//出现异常 回滚事务
connection对象.rollback();
}finally{
释放资源
JDBCUtil.close(conn,pst,rs);
}
}
默认情况下,事务在执行完 DML 操作就自动提交.
查询操作,其实是不需要事务的.但是,一般的,我们在开发中都把查询放入事务中.
开发中,代码完全正确,没有异常,但是就是数据库中数据不变
意识 : 没有提交事务
在 MySQL 中,只有 InnoDB 存储引擎支持事务,支持外键,MyISAM 不支持事务.
以后事务我们不应该在 DAO 层处理,应该在 service 层控制(了解).
事务在讲解 MyBatis,Spring,项目的时候都会再讲.
二、连接池思想
2.1 连接池引入和介绍
普通的 JDBC 数据库连接(Connectiond对象)使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码得花费 0.05s~1s 的时间, 时间成本比较大 。
需要数据库连接时,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。
数据库连接对象并没有得到很好的重复利用.若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
对于每一次数据库连接,使用完后都得断开,不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,可能导致内存泄漏,服务器崩溃.
解决: 先创建好 Connection 对象,存起来重复使用,如下图.存了 Connection 的容器则为连接池.
连接池属性分析:联想春运去火车站购票
基本属性:连接池存了连接对象,而连接对象依赖四要素,所以四要素是基本要求
driverClassName,url,username,password
其他属性:对连接对象做限制的配置
最多连接数:10 在连接池中最多有10个Connection对象,其他客户端进入等待状
最少连接数 : 3 在连接池中最少存在3个Connection对象
最长等待时间:5 min 使用5分钟来申请获取 Connection 对象,如果时间到还没有申请到,则提示,自动放弃
最长超时时间:10min 如果你在10分钟之内没有任何动作,则认为是自动放弃 Connection 对象.
在 Java 中,连接池使用 javax.sql.DataSource 接口来表示连接池. DataSource(数据源)和连接池 (Connection Pool)是同一个.
注意 :DataSource 仅仅只是一个接口,由各大服务器厂商来实现(Tomcat,JBoss).
2.2 常见的 DataSource 实现
DBCP : Spring 框架推荐的
C3P0 : Hibernate 框架推荐的
druid : 阿里巴巴的连接池(号称 Java 语言中性能最好的连接池).
2.3 使没使用连接池的区别
如何获取 Connection 对象:
没有使用连接池: Connection conn =DriverManager.getConnection(url,username,password);
使用 连接池:Connection conn = DataSource对象.getConnection();
只要获取了Connection对象,接下来的操作和以前是一模一样的.
2.如何释放 Connection对象(Connection对象.close()):
没有使用连接池: 是和数据库服务器断开.
使用连接池: 是把Connection对象返还给连接池中,并没有和数据库服务器断开.
关键在于:如何创建 DataSource 对象,所以需要来学习 DataSource 实现的使用.
2.4 druid 连接池使用
druid:是阿里巴巴研发出来的号称 Java语言领域性能最高的连接池. wiki地址:https://github.com/alibaba/druid/wiki
使用起来,类似于 DBCP 连接池. 方便检测性能/状态. 支持: MySQL,Oracle,DB2,MS Server等. 支持: 对配置文件的密码加密. 拷贝 jar: druid-1.0.15.jar.
最基本的写法:
@Test
public void testDruidDataSource() throws Exception {
// 创建一个连接池对象
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/jdbcdemo");
ds.setUsername("root");
ds.setPassword("admin");
ds.setInitialSize(5);//初始化创建连接的个数
Connection conn = ds.getConnection();
System.out.println(conn.getClass());
}
处理 Druid 连接池使用过程中的硬编码
在创建连接池对象时,使用到的连接数据库的信息应该编写到 Properties 配置文件中,然后再读取到内存中来使用
db.properties
#这里的 key 一定要和 DruidDataSource 中对应的属性名一致
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/javaweb 4 username=root
password=admin
三、SQL 注入详解
Statement 和 PreparedStatement 的区别
Statement 和 PreparedStatement 的区别: PreparedStatement 存在的优势:
- 更好的可读性,可维护性.
- 可以提供更好的性能(预编译). MySQL 不支持 PreparedStatement 性能优化.
- 更安全,可以防止 SQL 注入的问题
如果使用 Statement 语句对象,我们是把参数直接拼接到 SQL 中然后执行,此时,如果参数会改变SQL
的语法结构,执行的结果就会存在问题了,如,在登录查询的 SQL 中,如果用户名参数值为: ' or 1=1 or ', 此时,填写的密码无论是多少,登录都会成功, 这就是 SQL 注入的问题.
这个问题可以使用预编译语句对象完美解决
- 预先发送带有占位符的 SQL 到数据库中进行编译, 语句结构固定下来
- 设置参数给对应的占位符,然后再执行 SQL
这里无论是什么参数,都不会再改变 SQL 的语法结构,达到防止 SQL 注入问题的目的
四、查询操作模板代码抽取
/**
- 处理查询操作
- <T>:是声明泛型类型
- List<T>, Class<T>使用声明好的T
- @param sql 要执行的SQL语句
- @param type 将每行数据封装的对象类型
- @param params 执行的SQL需要的参数
- @return 返回查询到的结果,统一放到List集合中
*/
public static <T> List<T> executeQuery(String sql,Class<T> type, Object...params){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<T> list = new ArrayList<>();
try {
conn = DruidUtil.getConnection();
ps = conn.prepareStatement(sql);
//为占位符设值
for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]);
}
rs = ps.executeQuery();
//直到next方法返回false时结束 while (rs.next()) {
//使用反射创建对象
T t = type.newInstance();
//将数据从结果集中获取到,并设置给对象t
//通常情况下,属性名和列名一样,所以我们可以根据属性名去获取对应列的值
BeanInfo beanInfo = Introspector.getBeanInfo(t.getClass(),
Object.class);
PropertDescriptor[] pds = beanInfo.getPropertyDescriptors();
//操作每个属性
for (PropertyDescriptor pd : pds) {
//获取到属性名
String name = pd.getName();
//根据这个属性名从结果集中获取到数据
Object value = rs.getObject(name);
//获取到属性对应的set方法
Method writeMethod = pd.getWriteMethod(); writeMethod.invoke(t, value);
}
list.add(t);
}
} catch (Exception e) { e.printStackTrace();
} finally {
JdbcUtil.close(conn, ps, rs);
}
return list;
}