JDBC的使用(详细)
- 世界杯没有中国
- 2025-06-23 12:50:47
- 6543
1. JDBC是什么
Java DataBase Connectivity(java语言连接数据库)
2. JDBC本质是什么
JDBC是SUN公司制定的一套接口
接口都有调用者和实现者
面向接口调用、面向接口写实现类,这都属于面向接口编程
为什么要面向接口编程?
解耦合:降低程序的耦合度,提高程序的扩展力
多态机制就是非常典型的:面向抽象编程(不需要面向具体编程)
建议:
Animal a = new Cat();
Animal b = new Dog();
// 喂养的方法
public void feed(Animal a){
}
// 不建议:
Dog d = new Dog()
Cat c = new Cat()
3. JDBC开发前的准备工作,先从官网下载对应的驱动jar包,然后将其配置到环境变量classpath当中
classpath=.;D:\MYSQL_CONNECTION\mysql-connector-java-5.1.47\mysql-connector-java-5.1.47\mysql-connector-java-5.1.47-bin.jar;
以上的配置是针对于文本编辑器的方式开发,使用IDEA工具的时候,不需要配置以上的环境变量
IDEA有自己的配置方式
4. JDBC编程六步(需要背会)
第一步:注册驱动(告诉java程序,即将连接的是哪一个品牌的数据库)
第二步:获取连接(表示JVM的进程和数据库进程之间的通道打开了,这属于进程之间的通信,重量级的,使用完一定要关闭)
第三步:获取数据库操作对象(专门执行sql语句的对象)
第四步:执行SQL语句
第五步:处理查询结果(只有当第四步执行的时select语句时,才有这第五步处理查询的结果集)
第六步:释放资源(使用完资源之后就一定要关闭资源。Java和数据库属于进程间的通信,开启后一定要关闭)
// JDBC编程六步
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Connection;
public class JDBCTest01 {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
// 1. 注册驱动
Driver driver = new com.mysql.jdbc.Driver(); // 多态,父类型指向子类型对象
DriverManager.registerDriver(driver);
// 2. 获取连接
/**
* url: 统一资源定位符(网络中某个资源的绝对路径)
* url 包括哪几个部分?
* 协议 IP PORT 资源名
* jdbc:mysql://127.0.0.1:3306/bjpowernode
* jdbc:mysql:// 协议
*127.0.0.1 IP地址
*3306 mysql数据库端口号
* bjpowernode 具体的数据库实例名
* 说明:localhost和127.0.0.1都是本机IP地址
* 什么是通信协议,有什么用?
* 通信协议是通信之前就提前定好的数据传送格式
* 数据包具体怎么传数据,格式是提前定好的
*/
String url = "jdbc:mysql://127.0.0.1:3306/bjpowernode";
String user = "root";
String password = "123456";
conn = DriverManager.getConnection(url, user, password);
System.out.println("数据库连接对象 = " + conn);
// 3. 获取数据库操作对象(Statement专门执行sql语句的)
stmt = conn.createStatement();
// 4. 执行sql
String sql = insert into dept(deptno,dname,loc) values('50','人事部','北京');
// 执行DML语句(insert delete update)
// 返回值是“影响数据库中的记录条数”
int count = stmt.executeUpdate(sql);
System.out.println(count == 1 ? "保存成功" : "保存失败");
// 5. 处理查询结果集
} catch(SQLException e) {
e.printStackTrace();
} finally {
// 6. 释放资源
// 为了保证资源一定释放,在finally语句块中关闭资源
// 并且要遵循从小到大依次关闭
// 分别对其try...catch...
try {
if(stmt != null) {
stmt.close();
}
} catch(SQLException e) {
e.printStackTrace();
}
try {
if(conn != null) {
conn.close();
}
} catch(SQLException e) {
e.printStackTrace();
}
}
}
}
5. 注册驱动的另一种方式(常用)
import java.sql.*;
public class JDBCTest01 {
public static void main(String[] args) {
try {
// 1. 注册驱动
// 这是注册驱动的第一种写法
// DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// 注册驱动的第二种方式:常用的
// 为什么这种方式常用,因为参数是一个字符串,字符串可以写到xxx.properties文件中
// 以下方法不需要接收返回值,因为我们只想用它的类加载动作
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接
Connection conn = DriverManager.getConncetion("jdbc:mysql://127.0.0.1:3306/bjpowernode", "root", "123456");
System.out.println(conn);
} catch(SQLException e) {
e.printStackTrace();
}
}
6.将连接数据库的所有信息配置到配置文件中
首先需要编辑一个properties文件
driver=com.sql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/bjpowernode
user=root
password=123456
在java文件中,使用资源绑定器绑定属性配置文件
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String driver = bundle.getString("driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
这样在java程序中就不需要将连接数据库的信息写死了
例如:
// 1. 注册驱动
Class.forName(driver);
// 2. 获取连接
Connection conn = DriverManager.getConncetion("url", "user", "password");
7. 处理查询结果
这里省略一二三步,直接到第四步
// 执行sql
String sql = "select empno,ename,sal from emp";
//这里的rs是ResultSet用于存储从数据库获取的结果集合的对象
rs = stmt.executeQuery(sql) // 专门执行DQL的方法
// 处理查询结果集
// 使用while循环遍历
while(re.next()) {
// 注意:getString()方法的特点是:无论数据库中的数据类型是什么,都以String的形式取出
// JDBC中所有下标从1开始,不是从零开始
// String empno = rs.getString(1);
// 以列的名字获取
String empno = rs.getString("empno");
String ename = rs.getString(2);
String sal = rs.getString(3);
System.out.println(empno + "," + ename + "," + sal);
}
8. 用户登录业务介绍
IDEA开发配置驱动
项目或者模块右键
点击Libraries
之后选择自己的驱动包
实现功能:
1、需求:模拟用户登录功能的实现
2、业务描述:
程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码
用户输入用户名和密码之后,提交信息,java程序收集到用户信息
java程序连接到数据库验证用户名和密码是否合法
合法:显示登录成功
不合法:显示登录失败
3、数据的准备:
在实际的开发中,表的设计会使用专业的建模工具,我们这里安装一个建模工具:PowerDesigner
使用PD工具来进行数据库表的创建(参见userLogin.sql脚本)
1、首先我们创建一个数据库表的脚本
drop table if exists t_user;
/*==============================================================*/
/* Table: t_user */
/*==============================================================*/
create table t_user
(
id bigint auto_increment,
loginName varchar(255),
loginPwd varchar(255),
realName varchar(255),
primary key (id)
);
insert into t_user(loginName,loginPwd,realName) values('zhangsan','123','张三');
insert into t_user(loginName,loginPwd,realName) values('lisi','234','李四');
insert into t_user(loginName,loginPwd,realName) values('wangwu','345','王五');
commit;
select * from t_user;
2、开始进行初始化工作:在控制台上输入账号密码进行登录
我们使用Map的数据结构,采用键值对的方式
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等登录信息
*/
private static Map
Scanner s = new Scanner(System.in);
System.out.println("用户名:");
String loginName = s.nextLine();
System.out.println("密码:");
String loginPwd = s.nextLine();
s.close();
Map
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
3、开始进行连接数据库的操作
1、初始化工作
boolean loginSuccess = false;
// JDBC代码
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
2、注册驱动
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
3、获取连接
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
4、获取数据库操作对象
// 3、获取数据库操作对象
stmt = conn.createStatement();
5、执行sql
// 4、执行sql
String sql = "select * from t_user where loginName = '" + userLoginInfo.get("loginName")
+ "' and loginPwd = '" + userLoginInfo.get("loginPwd") + "'";
// 以上正好完成了sql语句的拼接,以下代码的含义是,发送sql语句给DBMS,DBMS进行sql编译
// 正好将用户提供的“非法信息”编译进去,导致原sql语句的含义被扭曲了
rs = stmt.executeQuery(sql);
6、处理结果集
// 5、处理结果集
if (rs.next()) {
loginSuccess = true;
}
7、资源释放
finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
8、在主函数中通过布尔值验证登录
// 初始化一个界面
Map
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功!" : "登录失败!");
4、完整代码
package test;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* 实现功能:
* 1、需求:模拟用户登录功能的实现
* 2、业务描述:
* 程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码
* 用户输入用户名和密码之后,提交信息,java程序收集到用户信息
* java程序连接到数据库验证用户名和密码是否合法
* 合法:显示登录成功
* 不合法:显示登录失败
* 3、数据的准备:
* 在实际的开发中,表的设计会使用专业的建模工具,我们这里安装一个建模工具:PowerDesigner
* 使用PD工具来进行数据库表的创建(参见userLogin.sql脚本)
* 4、当前程序存在的问题:
* 用户名:fdsa
* 密码:fdsa' or '1'='1
* 登录成功
* 这种现象称为sql注入(安全隐患)。(黑客经常使用)
* 5、导致sql语句注入的原因是什么?
* 用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,
* 导致sql语句的愿意被扭曲,进而达到sql注入
*/
public class JDBCTest01 {
public static void main(String[] args) {
// 初始化一个界面
Map
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功!" : "登录失败!");
}
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return false表示失败,true表示成功
*/
private static boolean login(Map
// 打标记的意识
boolean loginSuccess = false;
// JDBC代码
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 3、获取数据库操作对象
stmt = conn.createStatement();
// 4、执行sql
String sql = "select * from t_user where loginName = '" + userLoginInfo.get("loginName")
+ "' and loginPwd = '" + userLoginInfo.get("loginPwd") + "'";
// 以上正好完成了sql语句的拼接,以下代码的含义是,发送sql语句给DBMS,DBMS进行sql编译
// 正好将用户提供的“非法信息”编译进去,导致原sql语句的含义被扭曲了
rs = stmt.executeQuery(sql);
// 5、处理结果集
if (rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return loginSuccess;
}
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等登录信息
*/
private static Map
Scanner s = new Scanner(System.in);
System.out.println("用户名:");
String loginName = s.nextLine();
System.out.println("密码:");
String loginPwd = s.nextLine();
s.close();
Map
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
}
5、存在的问题
当前程序存在的问题:
用户名:fdsa
密码:fdsa' or '1'='1
登录成功
这种现象称为sql注入(安全隐患)。(黑客经常使用)
导致sql语句注入的原因是什么?
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的愿意被扭曲,进而达到sql注入
9、解决sql注入问题?
解决sql注入问题?
只要用户提供的信息不参与sql语句的编译过程,问题就解决了
即使用户提供的信息中含有sql语句的关键字,但是没有参与编译,不起作用
要想用户信息不参与sql语句的编译,那么必须使用java.sql.PreparedStatement PreparedStatement接口继承了java.sql.Statement
PreparedStatement是属于预编译的数据库操作对象
PreparedStatement原理是预先对sql语句框架进行编译,然后再给sql语句传“值”
在用户登录介绍的基础上进行修改,只需要修改三、四步就能够解决这个问题
1、获取预编译的数据库操作对象
// 3、获取预编译的数据库操作对象
// sql语句框架。其中一个?表示一个占位符,一个?将来接收一个“值”。注意:占位符不能使用单引号括起来
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
// 程序执行到此处,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译
ps = conn.prepareStatement(sql);
// 给占位符?传值(第一个?下标是1,第二个?下标是2,JDBC中所有下标从1开始)
ps.setString(1, loginName);
ps.setString(2, loginPwd);
2、执行sql语句
// 4、执行sql
rs = ps.executeQuery();
3、测试结果
测试结果
用户名:fdas
密码:fdas' or '1'='1
登录失败!
4、完整代码
package test;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* 1、解决sql注入问题?
* 只要用户提供的信息不参与sql语句的编译过程,问题就解决了
* 即使用户提供的信息中含有sql语句的关键字,但是没有参与编译,不起作用
* 要想用户信息不参与sql语句的编译,那么必须使用java.sql.PreparedStatement
* PreparedStatement接口继承了java.sql.Statement
* PreparedStatement是属于预编译的数据库操作对象
* PreparedStatement原理是预先对sql语句框架进行编译,然后再给sql语句传“值”
* 2、测试结果
* 用户名:fdas
* 密码:fdas' or '1'='1
* 登录失败!
* 3、解决sql注入的关键是什么?
* 用户提供的信息中即使含有sql语句的关键字,但是这些关键字并没有参与编译,不起作用
* 4、对比一下Statement和PreparedStatement
* - Statement存在sql注入问题,PreparedStatement解决了sql注入问题
* - Statement是编译一次执行一次。PreparedStatement是编译一次,可执行N次。PreparedStatement效率较高
* - PreparedStatement会在编译阶段做类型的安全检查
* - 综上所述:PreparedStatement使用较多,只有极少数的情况下需要使用Statement
* 5、什么情况下必须使用Statement?
* 业务方面要求必须支持sql注入的时候
* Statement支持sql注入,凡是业务方面要求是需要进行sql语句拼接的,必须使用Statement
*/
public class JDBCTest02 {
public static void main(String[] args) {
// 初始化一个界面
Map
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功!" : "登录失败!");
}
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return false表示失败,true表示成功
*/
private static boolean login(Map
// 打标记的意识
boolean loginSuccess = false;
// 单独定义变量
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
// JDBC代码
Connection conn = null;
PreparedStatement ps = null; // 这里使用PreparedStatement(预编译的数据库操作对象)
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 3、获取预编译的数据库操作对象
// sql语句框架。其中一个?表示一个占位符,一个?将来接收一个“值”。注意:占位符不能使用单引号括起来
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
// 程序执行到此处,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译
ps = conn.prepareStatement(sql);
// 给占位符?传值(第一个?下标是1,第二个?下标是2,JDBC中所有下标从1开始)
ps.setString(1, loginName);
ps.setString(2, loginPwd);
// 4、执行sql
rs = ps.executeQuery();
// 5、处理结果集
if (rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return loginSuccess;
}
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等登录信息
*/
private static Map
Scanner s = new Scanner(System.in);
System.out.println("用户名:");
String loginName = s.nextLine();
System.out.println("密码:");
String loginPwd = s.nextLine();
Map
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
}
5、解决sql注入的关键
用户提供的信息中即使含有sql语句的关键字,但是这些关键字并没有参与编译,不起作用
6、对比一下Statement和PreparedStatement
Statement存在sql注入问题,PreparedStatement解决了sql注入问题
Statement是编译一次执行一次。PreparedStatement是编译一次,可执行N次。PreparedStatement效率较高
PreparedStatement会在编译阶段做类型的安全检查
综上所述:PreparedStatement使用较多,只有极少数的情况下需要使用Statement
7、什么情况下必须使用Statement?
业务方面要求必须支持sql注入的时候
Statement支持sql注入,凡是业务方面要求是需要进行sql语句拼接的,必须使用Statement
10、演示Statement的用法
PreparedStatement和Statement都有各自的用途,大多数情况下使用的是PreparedStatement,但是在一些情况下,就必须使用Statement
例如:在进行mysql语句的升序降序过程,使用PreparedStatement就不行,这时就需要使用到Statement
1、首先使用PreparedStatement来进行升降序的过程
// 获取预编译的数据库操作对象
String sql = "select loginPwd from t_user order by loginPwd ?";
ps = conn.prepareStatement(sql);
ps.setString(1, keyWords);
// 执行sql
rs = ps.executeQuery();
当我们在控制台输入desc或者asc时,会直接出现报错
这次错误信息提示“near ''desc'' at line 1”,这意味着 SQL 查询在尝试理解 'desc' 时遇到了问题,可能是因为 'desc' 被错误地用了单引号包围,这样它就被识别为字符串文字,而不是 SQL 关键字。在 SQL 中 DESC 是用来指定降序排序的关键字,并且在使用时不应该被单引号包围。
因此我们不采用这种方式,使用Statement能够解决这个问题
2、使用Statement来进行升降序的过程
// 获取数据库操作对象
stmt = conn.createStatement();
// 执行sql
String sql = "select loginPwd from t_user order by loginPwd " + keyWords;
rs = stmt.executeQuery(sql);
这样就能够成功解决这个问题
存在就有道理,因此我们不能将Statement忽略掉,不能因为使用的情况少,就抛弃了这种用法
3、完整代码
package test;
import java.sql.*;
import java.util.Scanner;
public class JDBCTest03 {
public static void main(String[] args) {
/*
// 用户在控制台输入desc就是降序,输入asc就是升序
Scanner scanner = new Scanner(System.in);
System.out.println("请输入desc或asc,desc表示降序,asc表示升序");
System.out.println("请输入:");
String keyWords = scanner.nextLine();
// 执行sql
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 获取预编译的数据库操作对象
String sql = "select loginPwd from t_user order by loginPwd ?";
ps = conn.prepareStatement(sql);
ps.setString(1, keyWords);
// 执行sql
rs = ps.executeQuery();
// 遍历结果集
while (rs.next()) {
System.out.println(rs.getString("loginPwd"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} */
// 用户在控制台输入desc就是降序,输入asc就是升序
Scanner scanner = new Scanner(System.in);
System.out.println("请输入desc或asc,desc表示降序,asc表示升序");
System.out.println("请输入:");
String keyWords = scanner.nextLine();
// 执行sql
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 获取数据库操作对象
stmt = conn.createStatement();
// 执行sql
String sql = "select loginPwd from t_user order by loginPwd " + keyWords;
rs = stmt.executeQuery(sql);
// 遍历结果集
while (rs.next()) {
System.out.println(rs.getString("loginPwd"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
11、PreparedStatement实现增删改
1、添加数据库
// 添加数据库
String sql = "insert into t_user(loginName,loginPwd,realName) values(?,?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1, "去啊");
ps.setInt(2, 432);
ps.setString(3, "张三丰");
2、更改数据库
// 更改数据库
String sql = "update t_user set loginName = ?, loginPwd = ? where realName = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "好啊");
ps.setInt(2, 442);
ps.setString(3, "张三丰");
3、删除数据库
// 删除数据库
String sql = "delete from t_user where realName = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "张三丰");
12、账户转账演示事务
回滚是什么?
在数据库中,"回滚"指的是将数据库状态回退到某个确定点的操作。在SQL中,用于回滚的语句是 ROLLBACK
当在事务中执行了如 INSERT, UPDATE, 或 DELETE 这样的操作后,如果你需要取消这些操作,并恢复到事务开始前的状态,你可以使用 ROLLBACK 命令。这个命令将撤销事务内所有未提交的更改
START TRANSACTION;
INSERT INTO t_act(actno, balance) VALUES (333, 15000);
-- 假设这里发生了错误或者我们出于某种原因需要撤销之前的操作
ROLLBACK;
例如:没有进行操作的数据库表
进行修改后回滚
在IDEA里面,同样需要回滚的操作,保证安全性
重点三行代码 :
conn.setAutoCommit(false); // 开启事务
conn.commit(); // 提交事务
conn.rollback(); // 回滚事务
这里演示了一个账户转账的例子,在第一个账户转账后,会出现异常;如果不进行回滚,那么第一个账户完成了转账,第二个账户却不会收到转账
我们需要定义一个事务的起始点
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 将自动提交机制修改为手动提交
conn.setAutoCommit(false); // 开启事务
当发生异常,就捕获异常并回滚事务
catch (Exception e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
e.printStackTrace();
}
}
e.printStackTrace();
}
如果没有发生异常,那么就可以进行事务提交了
conn.commit(); // 提交事务
完整代码
首先是一个数据库表
DROP TABLE IF EXISTS t_act;
CREATE TABLE t_act (
actno INT,
balance DOUBLE(7,2) -- 注意,7,表示有效字数的个数,2表示小数位的个数
);
INSERT INTO t_act(actno, balance) VALUES (111, 20000);
INSERT INTO t_act(actno, balance) VALUES (222, 0);
COMMIT;
SELECT * FROM t_act;
IDEA演示代码
package test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCTest05 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
// 将自动提交机制修改为手动提交
conn.setAutoCommit(false); // 开启事务
//3、获取预编译的数据库操作对象
String sql = "update t_act set balance = ? where actno = ?";
ps = conn.prepareStatement(sql);
// 给问号传值
ps.setDouble(1, 10000);
ps.setInt(2, 111);
int count = ps.executeUpdate();
String s = null;
s.toString();
// 再给问号传值
ps.setDouble(1, 10000);
ps.setInt(2, 222);
count += ps.executeUpdate();
System.out.println(count == 2 ? "转账成功" : "转账失败");
// 程序能够走到这里说明以上程序没有异常,事务结束,手动提交数据
conn.commit(); // 提交事务
} catch (Exception e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
e.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 6、释放资源
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
13、JDBC工具类的封装
我们一般将工具类放入到一个包里,这个包有一般名为Utils
因为有可能会存在多次获取数据库连接的情况,有不同的数据库;因此我们将注册驱动这一过程放入在静态代码块中,当执行类加载时,就执行,且只执行一次
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
获取数据库连接对象放入到一个静态函数中
/**
* 获取数据库连接对象
* @return 连接对象
* @throws SQLException 抛出异常
*/
public static Connection getConnection () throws SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
}
释放资源
/**
* 关闭资源
* @param conn 连接对象
* @param ps 数据库操作对象
* @param rs 结果集
*/
public static void close(Connection conn, PreparedStatement ps, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
完整代码
package utils;
import java.sql.*;
// JDBC工具类,简化JDBC编程
public class DBUtil {
/**
* 工具类中的构造方法都是私有的.
* 因为工具类中的方法都是静态的,不需要new对象,直接采用类名调用
*/
private DBUtil() {}
// 静态代码块在类加载时执行,且只执行一次
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 获取数据库连接对象
* @return 连接对象
* @throws SQLException 抛出异常
*/
public static Connection getConnection () throws SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/bili?useSSL=false", "root", "123456");
}
/**
* 关闭资源
* @param conn 连接对象
* @param ps 数据库操作对象
* @param rs 结果集
*/
public static void close(Connection conn, PreparedStatement ps, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
这样代码就显得代码特别简洁
14、模糊查询
我们先调用刚才写好的工具类
模糊查询执行数据库的过程,这里查询了loginName第二个字符为a的realName,注意,在sql语句中的?前是‘like’而不是‘=’,这样我们就能够查找到想要的结果
// 获取预编译的数据库操作对象
String sql = "select realName from t_user where loginName like ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "_a%");
rs = ps.executeQuery();
while(rs.next()) {
System.out.println(rs.getString("realName"));
}
15、悲观锁和乐观锁的概念
悲观锁:事务必须排队,数据锁住了,不允许并发(行级锁:select后面添加for update)
乐观锁:支持并发,事务也不需要排队,只不过需要一个版本号
演示悲观锁(行级锁)
首先一个事务使用悲观锁锁住相关的记录
package test;
import utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 这个程序开启一个事务,这个事务专门进行查询,并且使用行级锁/悲观锁,锁住相关的记录
public class JDBCTest07 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
// 开启事务
conn.setAutoCommit(false);
String sql = "select * from t_user where loginName = ? for update";
ps = conn.prepareStatement(sql);
ps.setString(1, "lisi");
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("loginName") + "," + rs.getInt("loginPwd") + "," + rs.getString("realName"));
}
conn.commit(); // 提交事务(事务结束)
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务(事务结束)
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
e.printStackTrace();
} finally {
// 释放资源
DBUtil.close(conn, ps, rs);
}
}
}
另一个事务负责修改被锁定的记录
package test;
import utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 这个程序负责修改被锁定的记录
public class JDBCTest08 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
String sql = "update t_user set loginPwd = loginPwd * 2 where realName = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "李四");
int count = ps.executeUpdate();
System.out.println(count);
conn.commit();
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务(事务结束)
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, null);
}
}
}
在以上两个程序中,可以实现行级锁机制,第一个程序对相关记录采用了行级锁,如果该事务还未完成,那么在第二个程序就不能对相关数据进行修改,知道第一个事务结束后,第二个数据才能进行修改;
要通过以上两个程序实现这个机制,我们只需要现在第一个程序事务结束处打一个断点,之后debug运行,在运行第二个程序,此时的被锁住的数据未被修改;当第一个事务运行结束后,第二个程序就能够对数据库进行修改了