JDBC
1. JDBC 概述
在JAVA 中要如何去操作数据库呢?
答案就是 通过 JDBC
JDBC 是什么?
- "Java Database Connectivity " 即 JAVA 的数据库连接技术。
- JDBC 是 JAVA 规定的,连接数据库的接口。具体的实现由,各个数据库厂商去实现
- 身为 java 开发人员,我们只需要知道规范和使用方法即可,不必了解内部的实现。
JDBC 接口位于 JAVA 哪个包下?
答:在 java.sql 、javax.sql 下
JDBC体系结构:
- 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。保存在
java.spl、javax.spl下 - 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。我们不必了解
JDBC 是很明显的 面向接口编程
JDBC是sun公司提供一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可。
不同的数据库厂商,需要针对这套接口,提供不同实现。不同的实现的集合,即为不同数据库的驱动。 ————面向接口编程
涉及具体核心类和接口
DriverManager:负责注册驱动- 将第三方数据库厂商的实现驱动 jar 注册到程序中
- 可以根据数据库连接信息获取
connection
Connection:负责连接数据库- 和数据库建立的连接,在连接对象上可以多次执行数据库
curd动作 - 可以获取
statement和preparedstatement,callablestatement对象
- 和数据库建立的连接,在连接对象上可以多次执行数据库
Statement(静态sql,没有动态值语句) |PreparedStatement(动态sql,有动态值语句)|CallableStatement(执行标准存储过程SQL路线):负责将SQL 语句发送到 数据库上。- 具体发送 SQL 语句到数据库管理软件的对象。
- 不同发送方式稍有不同! preparedstatement 使用为重点!
Result:存储 SQL 返回的结果集- 面向对象思维的产物(抽象成数据库的查询结果表)
- 存储 DQL 查询数据库结果的对象
- 需要我们进行解析,获取具体的数据库数据
2. JDBC 实现的步骤
要实现 JDBC 那只需要六步。
- 注册驱动的 jar 包进行安装 我们需要引入,sql 厂商的 实现 jar 包,这样才能实现 数据库的连接
- 创建链接
我们引入 jar 包之后就可以创建连接了,其实就是创建一个
connection对象。 - 创建发送SQL语句的对象
创建连接之后我们就需要一个对象,能够帮助我们将
SQL发送到 数据库中。这个对象就是statement - 发送SQL语句,获取结果
利用上面生成的
statement对象,发送SQL语句到数据库 - 解析返回结果
此时我们将得到一个
resultset对象,如果是select语句,那么就会返回对应的数据,如果是其他语句,那么就只会返回INT类型的数据 - 销毁资源
此时,我们需要将之前使用的对象
result、statement、connection进行销毁
看个例子:
package com.jdbctest.api.statament;
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
/**
* ClassName: StatementQueryPart
* Package: com.jdbctest.api.statament
* Description:
*
* @Author arzhu
* @Create 2023/7/27 20:37
* @Version 1.0
*/
public class StatementQueryPart {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
/*
* 如果使用的是 mysql8.0 以下,那么此处的 new Driver 是 com.mysql.jdbc.Driver 包下的
* 如果是 mysql8.0 那么就是 com.mysql.cj.jdbc.Driver
* */
DriverManager.registerDriver(new Driver());
//2. 获取链接
/*
* 参数1:URL : 格式为:jdbc:数据库厂家://ip地址:端口/数据库名
* 参数2:USER : 账户
* 参数1:PASSWORD : 密码
* */
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/jdbctest","root","abc123");
//3. 创建 statment
Statement statement = connection.createStatement();
//4. 发送sql语句,并且返回结果
String sql = "SELECT * FROM t_user";
// 执行 查询语句
ResultSet resultSet = statement.executeQuery(sql);
//5. 进行结果解析
// result 是一行一行接收的数据,所以我们要使用 result.next 使其读取下一行数据
while (resultSet.next()){
int id = resultSet.getInt("id");
String account = resultSet.getString("account");
String password = resultSet.getString("password");
String nickName = resultSet.getString("nickname");
System.out.println(id+"--"+account+"--"+password+"--"+nickName);
}
//6. 关闭资源
resultSet.close();
connection.close();
statement.close();
}
}
执行之后就能看到 数据库的全部数据了
3. Statment的问题以及JDBC的细节
需求:从键盘输入账号密码,读取数据库的账号跟密码判断是否能够成功
代码为:
package com.jdbctest.api.statament;
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
/**
* ClassName: StatementUserLoginPart
* Package: com.jdbctest.api.statament
* Description:
* 需求:从键盘输入账号密码,读取数据库的账号跟密码判断是否能够成功
* @Author arzhu
* @Create 2023/7/28 14:06
* @Version 1.0
*/
public class StatementUserLoginPart {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
//0. 准备工作
Scanner scanner = new Scanner(System.in);
System.out.println("请输入账号:");
String account = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
//1. 注册驱动
/**
* DriverManager.registerDriver 底层调用了一个 registerDriver 注册驱动的方法,
* 但是,在 new Driver 上的静态代码块也调用了一个 registerDriver
* 这就意味着 驱动调用了两次!!!形成了很大的资源浪费
* 如果想要改进的话,我们只需要去调用一次 registerDriver
* 考虑是使用 DriverManager.registerDriver 还是 new Driver() 呢?
* 很明显,我们只能使用 new Driver()。因为 DriverManager.registerDriver 必须要用到 new Driver。
*
*/
/**
* 静态代码块是随着类的加载而执行的,并且只会执行一次
* 类的加载机制:(类一旦加载那么必定会执行静态代码块)
* 加载 [class 文件 ---> jvm 的虚拟机对象]
* 链接 [验证(检查文件类型,语法)-> 准备(静态变量 默认值)->(触发静态代码块)]
* 初始化(静态属性赋真实值)
* // 这里可以去 F:\javacode\JavaSeCode\chatpter18_reflect\src\com\arzhu\doc 找到对应的文档
* 触发静态代码块的方法(触发类加载机制的做法):
* 1. new 关键字
* 2. 调用静态方法
* 3. 调用静态属性
* 4. 接口 1.8后往接口添加 default 实现
* 5. 反射
* 6. 子类触发父类
* 7. 程序的入口 main
*/
//方案一:注册了两次 registerDriver
// DriverManager.registerDriver(new Driver());
// 方案二:
/*
* 非常不雅 并且 没有适配性
* 此时我们导入的是 com.mysql.cj.jdbc.Driver 的 Driver
* 如果我们导入的是 oracle 数据库呢?那么 这个包的路径又要变一次,不适合我们后续做 数据库的迁移
* */
// new Driver();
// 方案三:使用 反射 可以将 配置有关的,都提取成 配置文件
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 获取链接
/**
* getConnection 是一个重载的方法
* 方法一:三个参数(推荐,更加灵活)
* 参数1:url String ---> jdbc:jdbc:数据库厂家://ip地址:端口/数据库名?key1=value1&key2=value2
* 如果默认 ip 是主机 端口是3306 那么可以简写成 jdbc:mysql///数据库名。
* 举个例子:此项目为:jdbc:mysql://127.0.0.1:3306/jdbctest
* 那么可以简写为:jdbc:mysql:///jdbctest
* 后面的 key1 和 value1 等都是附加信息。可选可不选
* 具体有:
* serverTimezone=AsiaShanghai(8.0.25以上可自动识别时区)
* useUnicode=true(是否使用unicode编码格式,8.0版本以后默认)
* characterEncoding=utf8(是否使用 utf8编码,8.0版本以后默认)
* useSSL=true(是否忽略SSL,8.0版本以后默认)
* 参数2:user String ---> 账号
* 参数3:password String ---> 密码
* 方法二:两个参数
* 参数1:url String 等同于上面
* 参数2:info Properties 类型,也就是一个"user=xxx" "password=yyy" 的 Properties类型数据
*
* 方法三:一个参数
* 参数:url String 等同于上,但是需要添加 "user=xxxx$password=yyy"
*/
// 方法一(推荐)
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest","root","abc123");
// 方法二(不推荐)
// Properties info = new Properties();
// info.put("user","root");
// info.put("password","abc123");
// Connection connection1 = DriverManager.getConnection("jdbc:mysql:///jdbctest",info);
// 方法三
// Connection connection2 = DriverManager.getConnection("jdbc:mysql:///jdbctest?user=root&password=abc123");
//3. 创建 statment
Statement statement = connection.createStatement();
//4. 发送sql语句,并且返回结果
String sql = "SELECT * FROM t_user WHERE account = '"+account+"' AND `password` = '"+password+"';";
// 执行 查询语句
System.out.println(sql);
/**
* DML:INSERT、UPDATE、DELETE、SELECT 等操作数据的语言
* DDL:REATE、ALTER、DROP 等定义更改数据库的语言
* DCL:用于控制不同用户的权限的语言
* TCL:事务控制语言,用于处理数据库中的事务。
* DQL:主要负责查询数据的语言。
* statement 有两个执行 sql 的方法
* executeUpdate:只执行没有返回数据的sql,例如:DDL相关,INSERT、UPDATE、DELETE 等
* 返回类型为 INT。
* 情况一:DML(除了 SELECT) 返回 影响的行数
* 情况二:非DML 返回0
* executeQuery:执行 SELECT 的语言,返回是一个 ResultSet 类型。只接收 DQL 参数
*/
ResultSet resultSet = statement.executeQuery(sql);
//5. 进行结果解析
/**
* resultSet 得到的是一个结果集
* resultSet 有一个指针,默认指针指向第一条数据之前
* resultSet.next() 返回一个 boolean 类型的数据,如果 指针下方还有数据
* 有数据,返回 true,并且将指针指向下一条数据
* 否则返回 false
* 通常做法就是 while(resultSet.next()){执行代码....}
* resultSet 得到数据:
* 使用 resultSet.get类型(String 列的别名或列名 | int 列的索引 从1开始)
*/
// while (resultSet.next()){
// int id = resultSet.getInt("id");
// String account1 = resultSet.getString(2);
// String password1 = resultSet.getString(3);
// String nickName = resultSet.getString("nickname");
// System.out.println(id+"--"+account1+"--"+password1+"--"+nickName);
// }
if(resultSet.next()){
System.out.println("登录成功!!!");
}else{
System.out.println("登录失败!!!");
}
//6. 关闭资源
resultSet.close();
statement.close();
connection.close();
}
}
- 我们使用反射进行驱动的注册,目的是为 更灵活,可配置
- 建立
connection连接,我们使用 三个参数的getConnection,目的是: 更灵活,可配置 - 由于输入的是
SELECT是DQL,所以我们使用executeQuery发送sql
上述代码中还存在着以下问题
-
sql 只能使用 字符串拼接的方式,非常不雅观和不方便
-
只能拼接字符串类型,其他类型无能为力
-
可能发生SQL注入!!! 尝试将密码输入变为:
'OR '1'='1执行结果之后发现登录成功,实际上的sql变成了:
SELECT * FROM t_user WHERE account = 'root' AND `password` = ''OR '1'='1';
那么我们要如何解决这些问题呢?
答案就是:使用 PreparedStatement
4. 使用 PreparedStatment解决SQL注入
直接看代码:
public class PreparedStatementUserLoginPart {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入账号:");
String account = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest","root","abc123");
String sql = "SELECT * FROM t_user WHERE account = ? AND `password` = ?;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,account);
preparedStatement.setObject(2,password);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.next()){
System.out.println("登录成功!!!");
}else{
System.out.println("登录失败!!!");
}
//6. 关闭资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
密码尝试输入:'OR '1'='1。
测试一下,登录失败, PreparedStatment预防了SQL注入!!!
对比一下statement和 PreparedStatement
PreparedStatement在创建时就传入了sql,使用?替代我们传入的变量account和passwordPreparedStatement调用setObject(index,column)方法去设置?处的值,按照索引的值去设置对应的值,索引从 1 开始。preparedStatement.executeQuery支持DQL 如果是除了SELECT以外的DML那么就是使用preparedStatement.executeUpdate`
原理是啥呢?
5. 使用PreparedStatment执行DML
我们接下来尝试使用PreparedStatment 去实现 DML
package com.jdbctest.api.statament;
import org.junit.Test;
import java.sql.*;
import java.util.*;
/**
* ClassName: PrepareStatementUserDML
* Package: com.jdbctest.api.statament
* Description:
*
* @Author arzhu
* @Create 2023/7/28 18:03
* @Version 1.0
*/
public class PrepareStatementUserDML {
/**
* 需求: 插入一条数据,test 123 testname
* @throws ClassNotFoundException
*/
@Test
public void testInsert() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,"test");
preparedStatement.setObject(2,123);
preparedStatement.setObject(3,"testname");
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("插入成功");
}else{
System.out.println("插入失败");
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
/**
* 需求:删除 id = 3 的数据
*/
@Test
public void testDelete() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "DELETE FROM t_user WHERE id = 3";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// preparedStatement.setObject(1,"大狗子");
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
/**
* 修改 id = 3 nickname 为 大狗子
*/
@Test
public void testUpdate() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "UPDATE t_user SET nickname = ? WHERE id = 3";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,"大狗子");
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("修改成功");
}else{
System.out.println("修改失败");
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
/**
* 需求:将数据库中的数据变成 List<Map>的形式
* 每一个List 存储 一个 Map Map 中是这样的 key = 列, value = 值
*/
@Test
public void testSelectt() throws Exception {
// 准备
List resultList = new ArrayList();
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "SELECT * FROM t_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
ResultSet resultSet = preparedStatement.executeQuery();// 返回的是 影像的行数
// 5. 解析结果
while (resultSet.next()){
Map rowMap = new HashMap();
// 存储单个的 Map
int id = resultSet.getInt("id");
String account = resultSet.getString("account");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
rowMap.put("id",id);
rowMap.put("account",account);
rowMap.put("password",password);
rowMap.put("nickname",nickname);
resultList.add(rowMap);
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
// 输出
Iterator iterator = resultList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
从上面我们可以看到,对于SELECT,我们使用executeQuery 去执行SQL
对于其他的DML来说,我们使用executeUpdate执行 SQL
但是上面还是优点小问题的。
testSelect这段语句中,我们存入 Map 是手动去存的,这么做很蠢,也不利于维护
**testSelect**改良版
/**
* 需求:将数据库中的数据变成 List<Map>的形式
* 每一个List 存储 一个 Map Map 中是这样的 key = 列, value = 值
*/
@Test
public void testSelectt() throws Exception {
// 准备
List resultList = new ArrayList();
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "SELECT * FROM t_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
ResultSet resultSet = preparedStatement.executeQuery();// 返回的是 影像的行数
// 获取列信息
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount(); // 获取列的行数
// 5. 解析结果
while (resultSet.next()){
Map rowMap = new HashMap();
// 方案1 不完美
// 存储单个的 Map
// int id = resultSet.getInt("id");
// String account = resultSet.getString("account");
// String password = resultSet.getString("password");
// String nickname = resultSet.getString("nickname");
// rowMap.put("id",id);
// rowMap.put("account",account);
// rowMap.put("password",password);
// rowMap.put("nickname",nickname);
// resultList.add(rowMap);
// 方案2 我们对列进行循环取到对应的 key 和 value 再存进去
// 注意这里!!!必须要从 1 开始读取,因为 数据库的索引都是从 1 开始的
for (int i = 1; i <= columnCount; i++) {
rowMap.put(metaData.getColumnLabel(i),resultSet.getObject(i));
}
resultList.add(rowMap);
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
// 输出
Iterator iterator = resultList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
我们使用 resultSet.getMetaData获取列的信息,再从这个对象中获取获取列的个数getColumnCount,再通过metaData.getColumnLabel(i)获取第i个列的信息。从而实现了,智能赋值
6. JDBC 高级应用
6.1 主键回显
这个意思就是 插入数据时,得到该条数据的自增长主键。
应用举例:表A为订单表(主表),表B为订单项表(子表),此时新增了一个订单,于是我们往 订单表中插入数据,但是也要往订单项表中插入数据,此时,我们必须要
- 先插入 订单表 拿到自增主键
- 拿着主键再去 订单项表 插入数据
看看例子:
package com.jdbctest.api.statament.com.jdbctest.api.preparedStatement;
import org.junit.Test;
import java.sql.*;
/**
* ClassName: PSOtherPart
* Package: com.jdbctest.api.statament.com.jdbctest.api.preparedStatement
* Description:
* 主键回显
* @Author arzhu
* @Create 2023/7/28 20:36
* @Version 1.0
*/
public class PSOtherPart {
/**
* 需求:
* 插入一条数据,并且拿到该条数据的 主键
*/
@Test
public void returnPrimaryKey() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
// ①:此时,我们需要在 connection.prepareStatement 传入第二个参数,此参数告知 preparedStatement 在返回时需要带上 主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
preparedStatement.setObject(1,"马丫丫");
preparedStatement.setObject(2,"abc123");
preparedStatement.setObject(3,"myy");
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("修改成功");
// 此时需要去获取主键 preparedStatement.getGeneratedKeys 得到一个 ResultSet 结果集
ResultSet pk = preparedStatement.getGeneratedKeys();
// 指针指向的是 第一项以前的,所以我们必须要 next 一下
pk.next();
// 此时通过 pk.getInt 就能拿到 主键了
System.out.println("主键是:"+pk.getInt(1));
}else{
System.out.println("修改失败");
}
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
}
6.2 批量插入数据的优化
在我们处理批量插入数据时,往往会比较耗费资源,那么我们怎么做才能进行优化呢?
需求:插入一万条数据,测试耗费的时间
先看普通的插入:
@Test
public void plainInsert() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
//3. 创建statement
String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
preparedStatement.setObject(1,"马丫丫"+i);
preparedStatement.setObject(2,"abc123"+i);
preparedStatement.setObject(3,"myy"+i);
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
preparedStatement.executeUpdate(); // 返回的是 影像的行数
}
long end = System.currentTimeMillis();
System.out.println("插入耗费的时间是:"+(end - start));
//4. 传输SQL 获取结果
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
执行一下:
插入耗费的时间是:21258
看看优化的插入:
/**
* 改进的插入
* ①:创建 Connection 对象时,传入的 URL 添加 rewriteBatchedStatements=true
* ②:sql 语句 不能带有 ';'号
* ③:setObject之后 使用 preparedStatement.addBatch()
* ④:使用 executeBatch 发送 SQL
*/
@Test
public void improveInsert() throws Exception{
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest?rewriteBatchedStatements=true", "root", "abc123");
//3. 创建statement
String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
preparedStatement.setObject(1,"马丫丫"+i);
preparedStatement.setObject(2,"abc123"+i);
preparedStatement.setObject(3,"myy"+i);
// 使用 addBatch 将 SQL 的values 进行一个拼接 类似于:VALUES(x,x,x),(y,y,y),(z,z,z)....
preparedStatement.addBatch();
}
// 使用 executeBatch 将拼接好的 SQL 一次性发送到 MYSQL 去
preparedStatement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("插入耗费的时间是:"+(end - start));
// 6. 关闭连接
connection.close();
preparedStatement.close();
}
看看执行结果:
插入耗费的时间是:229
步骤:
- 创建 Connection 对象时,传入的 URL 添加 rewriteBatchedStatements=true
- sql 语句 不能带有 ';'号
- setObject之后 使用 preparedStatement.addBatch()
- 使用 executeBatch 发送 SQL
原理:
数据库的优化策略之一,就是要减少 JAVA 与 mysql 的交互,即IO。普通插入,在 for 中,setObject 之后 就将该SQL 发送到 MYSQL 中,for了10000次,那么就会产生10000次交互。而优化的插入则是采用SQL拼接的方式,将10000条数据变成一条SQL(利用了 INSERT t_(col1,col2,clo3) VALUES(y,y,y),(x,x,x),(z,z,z)....) 发送到 mysql 中,交互只发生了一次。
sql 语句 不能带有 ';'号,正式为了 进行SQL的拼接做的准备addBatch()方法正是拼接SQLexecuteBatch作用是 发送拼接好的SQL
6.3 JDBC 实现事务
6.3.1 事务回顾
什么是事务?
事务(Transaction)是数据库操作的基本单位,通常包含了一组操作:
要么这组操作全部执行并且数据持久化到数据库中,要么如果某个操作失败则全部操作不执行,数据库状态回滚到执行事务之前的状态。
举个例子就像转账一样;A余额 50,B 余额 20,A 需要转账10块到 B,那么完整的转账流程是:A - 10、B + 10,这个完整的流程就是一个事务。
事务的特点:ACID
- 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么全部完成,要么全部不完成。 就像上面提到的转账例子,如果 A - 10 成功,B + 10 失败,那么这个事务就是失败的,此时需要回滚到转账之前的状态
- 一致性(Consistency):事务必须使数据库从一个一致性状态变到另一个一致性状态。从上面那个转账例子来说:A 和 B 的总和是:50 + 20 = 70,就算转账成功了 它们的总和也是70,就算转账失败,它们的总和也理应是 70。
- 隔离性(Isolation):事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability):一旦事务完成,则它对数据库中的改变就是永久性的。接下来的其他操作或者系统故障不应对其有任何影响。
**事务场景:**一个业务涉及到多条修改的数据库语句,比如转账
事务要求:事务的一组操作,必须要在一个连接中!!!
事务类型:
自动提交:每条语句自动存储在一个事务中,执行成功就提交,执行失败就自动回滚手动提交:手动开启事务,添加语句,手动提交或者手动回滚。
sql开启事务
针对自动提交(推荐): 关闭自动提交,多条语句添加之后,最终手动提交或者手动回滚
- 执行
SET autocommit = off;关闭自动提交(只在当前连接中有效) - 执行多条语句
- 根据执行结果选择,提交或者回滚。
**针对手动提交(不推荐):**开启事务代码,添加SQL,手动提交或者回滚。
**一般而言,我们在 java 中 执行事务是 使用try catch **
try存放 多条 SQL 语句,最后一段为提交catch存放 回滚语句
try{
connection.setAutoCommit(false); // 关闭自动提交
多条SQL
connection.commit // 提交
} catch{
connection.rollback(); // 回滚
}
6.3.2 JDBC 实现事务
就从上面的例子 进行 一个 业务中的事务的操作。
前期准备:
CREATE TABLE t_bank(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
money INT UNSIGNED COMMENT '金额不能为负数'
);
INSERT t_bank(account,money) VALUES('张三',1000),('李四',2000);
需求:实现 张三 向 李四转账 500,直至 张三没钱。
Service层:存放业务代码DAO层:存放 跟数据库打交道的代码,即 SQL 的执行。一般来说是一个表对应一个DAO
DAO层 BankDao.class
package com.jdbctest.api.statament.transaction.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
/**
* ClassName: BankDAO
* Package: com.jdbctest.api.statament.transaction.dao
* Description:
* 操作 数据库的类 DAO 包(存储DAO类)
* @Author arzhu
* @Create 2023/7/29 13:53
* @Version 1.0
*/
public class BankDAO {
/**
* 针对 account 加 money 操作
* @param account 账号
* @param money 金钱
*/
public void add(String account,int money,Connection connection) throws Exception {
//3. 创建statement
String sql = "UPDATE t_bank SET money = money + ? WHERE account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//4. 传输SQL 获取结果
int i = preparedStatement.executeUpdate();
// 5. 解析结果
if(i==1){
System.out.println("加钱成功");
}else{
System.out.println("加钱失败");
}
preparedStatement.close();
}
/**
* 针对 account 减 money 操作
* @param account 账号
* @param money 金钱
*/
public void sub(String account,int money,Connection connection) throws Exception {
//3. 创建statement
String sql = "UPDATE t_bank SET money = money - ? WHERE account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//4. 传输SQL 获取结果
int i = preparedStatement.executeUpdate();
// 5. 解析结果
if(i==1){
System.out.println("减钱成功");
}else{
System.out.println("减钱失败");
}
preparedStatement.close();
}
}
Service层 BankService
package com.jdbctest.api.statament.transaction.service;
import com.jdbctest.api.statament.transaction.dao.BankDAO;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* ClassName: BankService
* Package: com.jdbctest.api.statament.transaction.service
* Description:
* 针对业务的类 存储在 Service 下
* @Author arzhu
* @Create 2023/7/29 13:56
* @Version 1.0
*/
public class BankService {
@Test
public void test() throws Exception {
transfer("李四","张三",500);
}
/**
*
* 转装的业务 addAccount 加钱 subAccount 减钱
* @param addAccount
* @param subAccount
* @param money
*/
public void transfer(String addAccount,String subAccount,int money) throws Exception {
BankDAO dao = new BankDAO();
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 建立连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
try {
// 关闭自动提交
connection.setAutoCommit(false);
// 执行多条 sql 语句
dao.add(addAccount,money,connection);
dao.sub(subAccount,money,connection);
// 执行到这里证明没问题 所以要提交了
connection.commit();
}catch(Exception e){
// 走到这里证明SQL 出错 所以要回滚
connection.rollback();
throw e;
}
}
}
总结:
- 每个业务就是一个事务,因为每个业务都会涉及到 多个SQL 的执行
- 利用 try catch 进行事务的回滚
- try 执行 SQL 完毕之后进行提交
- catch 进行 抛错 和 回滚
- 关闭连接要在 业务层去关闭(这点其实不优雅,可以优化一下)
7. Druid连接池
7.1 连接池概念
试想一个问题,一个 数据库连接的创建,一般来说有三个步骤
- 创建,时间为 t1。
- 使用,时间为 t2。
- 销毁,时间为 t3。
像上面的例子,如果我们去实现一个业务就需要创建一个连接,那么总的时间就是:业务数量 *(t1+t2+t3)
一般来说,t2 < t1 + t3。使用的耗费的资源和时间,远远小于 连接的创建和销毁。
那么能不能有一个方式,能让我们只使用连接,无需去创建和销毁连接呢?达到业务数量 * t2
有!那就是连接池设计模式。
顾名思义,连接池就是无需创建和销毁连接,要用连接就从连接池里取,取完再放回去就行
数据库连接池在Java中是一种常见的设计模式,其目的是为数据库的连接请求创建一个缓存。当应用程序需要与数据库进行交互时,它可以从连接池中获取一个已存在的连接,而不是新建一个。当应用程序完成与数据库的交互后,这个连接可以被返回到连接池,而不是被关闭,以便下次再次使用。这可以大大提高对数据库的访问速度并节省系统资源。
在 JAVA 中的连接池,都是实现了 JAVA 的接口javax.sql.DataSource
7.2 市面上常见的连接池技术
- HikariCP:这是一个非常快的数据库连接池,被广泛认为是目前性能最好的连接池。它的优点在于简洁的代码和零配置的默认设置,以及其在高并发环境下的优异表现。
- 优点在于它的性能极高,而且提供了零配置的默认设置,但是对于特殊的配置需求,其选项可能较少。
- C3P0:C3P0是一个开源的JDBC连接池,它提供了丰富的配置选项,并能够自动恢复关闭的连接。然而,与HikariCP相比,其性能略逊一筹。
- 优点是提供了很多配置选项,而且稳定性较高,但是性能较低。
- Apache DBCP:DBCP(Database Connection Pool)是Apache Commons项目的一部分,它提供了基础的连接池功能,但性能和可配置性相对较低。
- 优点在于它作为Apache Commons项目的一部分,与许多Apache项目有很好的集成,但其性能和可配置性较低。
- Tomcat JDBC Connection Pool:这是Tomcat提供的一个连接池实现,它提供了一些高级功能,如验证查询和连接验证,同时保持了较高的性能。
- 优点是提供了一些高级功能,同时保持了较高的性能,但是如果你的应用程序不是运行在Tomcat服务器上,使用这个连接池可能会有一些额外的配置工作。
- Druid:Druid 是阿里巴巴开源的数据库连接池,它是一个高效的并发连接管理工具,可大幅提升应用程序的数据库访问效率。它支持对于许多不同类型的数据库。
- 优点:可配置化,性能高,扩展强,开源免费
7.3 Druid 的使用
1、我们需要导入 Druid 的 jar 包
2、创建数据库配置文件druid.properties
# 使用 key=value 的形式
# 能比 properties 读取
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=abc123
url=jdbc:mysql:///jdbctest
3、使用
@Test
public void testSoft() throws Exception {
// 1. 读取配置
Properties properties = new Properties();
// src 下的文件可以使用类的加载器去加载
InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(resourceAsStream);
// 2. 使用连接池的工具类,工厂设计模式 创建连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 拿出连接
Connection connection = dataSource.getConnection();
// 回收连接
connection.close();
}
8. JDBC 工具类封装
我们针对上面的 事务 例子 来进行 工具类的封装
8.1 JDBC 连接池的工具类封装
package com.jdbctest.api.statament.JDBCUtils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.jdbctest.api.statament.druid.DruidTest;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* ClassName: JDBCUtils
* Package: com.jdbctest.api.statament.JDBCUtils
* Description:
* TODO
* JDBC 的 utils 类
* 负责创建 连接池
* 提供连接对象
* 因为 Service 层 和 DAO 层都是在一个线程
* 所以要使用 ThreadLocal 保存连接对象
* @Author arzhu
* @Create 2023/7/29 19:15
* @Version 1.0
*/
public class JDBCUtils {
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
private static DataSource dataSource = null;
/**
* 静态代码块,一开始将初始化 连接池
* 初始化 连接对象
*/
static {
// 1. 初始化连接池
// 1. 读取配置
Properties properties = new Properties();
// src 下的文件可以使用类的加载器去加载
InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(resourceAsStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 2. 使用连接池的工具类,工厂设计模式 创建连接池
}
/**
* 得到 connection 对象
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
// 第一次 进入 没有 connection 对象的话 那么就从连接池里取
if(connection == null){
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
}
/**
*
*/
public static void freeConnection() throws SQLException {
Connection connection = tl.get();
if(connection!=null){
// 释放资源,节省内存
tl.remove();
// 更改为自动提交
connection.setAutoCommit(true);
}
connection.close(); // 放回资源池
}
}
Service、DAO层都是在一个线程内的,所以,我们使用ThreadLocal固定住这个线程唯一的connection实例,避免了资源浪费,也优化了代码逻辑
使用:
DAO层
package com.jdbctest.api.statament.transactionnew.dao;
import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
/**
* ClassName: BankDAO
* Package: com.jdbctest.api.statament.transaction.dao
* Description:
* 操作 数据库的类 DAO 包(存储DAO类)
* @Author arzhu
* @Create 2023/7/29 13:53
* @Version 1.0
*/
public class BankDAO {
/**
* 针对 account 加 money 操作
* @param account 账号
* @param money 金钱
*/
public void add(String account,int money) throws Exception {
Connection connection = JDBCUtils.getConnection();
//3. 创建statement
String sql = "UPDATE t_bank SET money = money + ? WHERE account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("加钱成功");
}else{
System.out.println("加钱失败");
}
preparedStatement.close();
}
/**
* 针对 account 减 money 操作
* @param account 账号
* @param money 金钱
*/
public void sub(String account,int money) throws Exception {
Connection connection = JDBCUtils.getConnection();
//3. 创建statement
String sql = "UPDATE t_bank SET money = money - ? WHERE account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//4. 传输SQL 获取结果
// 由于是 DML 且不是 SELECT 所以我们使用 executeUpdate
int i = preparedStatement.executeUpdate(); // 返回的是 影像的行数
// 5. 解析结果
if(i==1){
System.out.println("减钱成功");
}else{
System.out.println("减钱失败");
}
preparedStatement.close();
}
}
跟上面的事务例子相比,我们没有传入connection对象。而是利用线程全局存在的 connection
Service层
package com.jdbctest.api.statament.transactionnew.service;
import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;
import com.jdbctest.api.statament.transactionnew.dao.BankDAO;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* ClassName: BankService
* Package: com.jdbctest.api.statament.transaction.service
* Description:
* 针对业务的类 存储在 Service 下
* @Author arzhu
* @Create 2023/7/29 13:56
* @Version 1.0
*/
public class BankService {
@Test
public void test() throws Exception {
transfer("李四","张三",500);
}
/**
* 注意:
* 每个业务就是一个事务,因为每个业务都会涉及到 多个SQL 的执行
* 利用 try catch 进行事务的回滚
* try 执行 SQL 完毕之后进行提交
* catch 进行 抛错 和回滚
* 关闭连接要在 业务层去关闭(这点其实不优雅,可以优化一下)
*
* 转装的业务 addAccount 加钱 subAccount 减钱
* @param addAccount
* @param subAccount
* @param money
*/
public void transfer(String addAccount,String subAccount,int money) throws Exception {
BankDAO dao = new BankDAO();
Connection connection = JDBCUtils.getConnection();
try {
// 关闭自动提交
connection.setAutoCommit(false);
// 执行多条 sql 语句
dao.add(addAccount,money);
dao.sub(subAccount,money);
// 执行到这里证明没问题 所以要提交了
connection.commit();
}catch(Exception e){
// 走到这里证明SQL 出错 所以要回滚
connection.rollback();
throw e;
}
}
}
8.3 JDBC 的 DAO层封装
一个表对应着一个DAO层,但是很多表的 CRUD 都是一致的,所以我们要将这些逻辑提取出来,变成一个 抽象类,我们称之为BaseDAO。
CRUD 可以分成两类
非DQL:即INSERT、DELETE、UPDATE,这三个操作我们都是用executeUpdate,返回值都是影响的行数DQL:即SELECT,这个操作我们是使用executeQuery,返回是一个ResultSet
BaseDAO层
package com.jdbctest.api.statament.JDBCUtils;
import com.jdbctest.api.statament.transaction.dao.User;
import org.junit.Test;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* ClassName: BaseDAO
* Package: com.jdbctest.api.statament.JDBCUtils
* Description:
* TODO
* DAO 的基类
* 目的就是简化 下面的 DAO 操作
* 作用:1、从连接池中取出 连接实例
* 2、执行SQL
* 3、执行完毕之后,返还 连接实例
* 4、返回结果
* @Author arzhu
* @Create 2023/7/29 20:22
* @Version 1.0
*/
public abstract class BaseDAO {
/**
* 执行 非 DQL 操作
* @param sql 传入的SQL 用占位符 进行赋值
* @param params
* @return
* @throws SQLException
*/
public int executeUpdate(String sql,Object...params) throws SQLException {
// 1. 拿到连接实例
Connection connection = JDBCUtils.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 进行SQL 的赋值
// 从1 开始
for (int i = 1; i <= params.length; i++) {
preparedStatement.setObject(i,params[i-1]);
}
int i = preparedStatement.executeUpdate();
preparedStatement.close();
return i;
}
/**
* 每一个类对应一个表
* 类中的属性 对应着 表中的字段
* 返回一个 List<T>
* 比如 我们传入一个 USER 类,那么我们执行 SQL 之后 就会创建一个 List<User>
*
* @param clazz 类的路径 为了获得 反射
* @param sql sql 语句
* @param params 参数
* @param <T> 泛型
* @return
*/
public <T> List<T> executeQuery(Class<T> clazz, String sql, Object...params) throws Exception {
// 建立一个 List<T> 接收结果集
List<T> list = new ArrayList<>();
// 1. 拿到连接实例
Connection connection = JDBCUtils.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 进行SQL 的赋值
// 从1 开始 查询
for (int i = 1; i <= params.length; i++) {
preparedStatement.setObject(i,params[i-1]);
}
ResultSet resultSet = preparedStatement.executeQuery();
ResultSetMetaData metaData = preparedStatement.getMetaData();
int columnCount = metaData.getColumnCount();
// 解析结果集
while (resultSet.next()){
// 创建 一个类的实例 用来接收 结果
T t = clazz.getDeclaredConstructor().newInstance();
// 为 类中的属性赋值 采用了 反射的方法
for (int i = 1; i <= columnCount; i++) {
Field declaredField = clazz.getDeclaredField(metaData.getColumnName(i).toLowerCase());// 这里是因为我数据库字段名设计成了大写,所以我需要转成小写
declaredField.setAccessible(true); // 破坏 private 权限
declaredField.set(t,resultSet.getObject(i));
}
list.add(t);
}
preparedStatement.close();
return list;
}
}
DAO层:UserDAO
package com.jdbctest.api.statament.transaction.dao;
import com.jdbctest.api.statament.JDBCUtils.BaseDAO;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
/**
* ClassName: UserDAO
* Package: com.jdbctest.api.statament.transaction.dao
* Description:
*
* @Author arzhu
* @Create 2023/7/29 20:30
* @Version 1.0
*/
public class UserDAO extends BaseDAO {
public void insertTest(String account,String password,String nickname) throws SQLException {
int i = this.executeUpdate("INSERT t_user(account,password,nickname) VALUES(?,?,?)", account, password, nickname);
if(i==1){
System.out.println("插入成功");
}else{
System.out.println("插入失败");
}
}
public void delTest(String account) throws SQLException {
int i = this.executeUpdate("DELETE FROM t_user WHERE account = ?", account);
if(i==1){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
}
public void selectTest() throws Exception {
List<User> selList = executeQuery(User.class, "SELECT * FROM t_user;");
Iterator<User> iterator = selList.iterator();
while (iterator.hasNext()){
User user = iterator.next();
System.out.println(user);
}
}
}
User类
package com.jdbctest.api.statament.transaction.dao;
/**
* ClassName: User
* Package: com.jdbctest.api.statament.transaction.dao
* Description:
*
* @Author arzhu
* @Create 2023/7/29 21:40
* @Version 1.0
*/
public class User {
private int id;
private String account;
private String password;
private String nickname;
public User(int id, String account, String password, String nickname) {
this.id = id;
this.account = account;
this.password = password;
this.nickname = nickname;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", account='" + account + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
}
UserService类
package com.jdbctest.api.statament.transaction.service;
import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;
import com.jdbctest.api.statament.transaction.dao.UserDAO;
import org.junit.Test;
import java.sql.SQLException;
/**
* ClassName: UserService
* Package: com.jdbctest.api.statament.transaction.service
* Description:
* 插入一条数据
* @Author arzhu
* @Create 2023/7/29 20:37
* @Version 1.0
*/
public class UserService {
@Test
public void test1() throws Exception {
// insertService();
// delService();
selService();
}
public void insertService() throws SQLException {
UserDAO userDAO = new UserDAO();
userDAO.insertTest("sdhjhf","124545","马丫丫");
JDBCUtils.freeConnection();
}
public void delService() throws SQLException {
UserDAO userDAO = new UserDAO();
userDAO.delTest("sdhjhf");
JDBCUtils.freeConnection();
}
public void selService() throws Exception {
UserDAO userDAO = new UserDAO();
userDAO.selectTest();
JDBCUtils.freeConnection();
}
}