JDBC的使用(详细)

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开发配置驱动
项目或者模块右键
image
点击Libraries
image
之后选择自己的驱动包

实现功能:
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<String, String> initUI() {
    Scanner s = new Scanner(System.in);
    System.out.println("用户名:");
    String loginName = s.nextLine();
    System.out.println("密码:");
    String loginPwd = s.nextLine();
    s.close();
    Map<String, String> userLoginInfo = new HashMap<>();
    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<String, String> userLoginInfo =initUI();

// 验证用户名和密码
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<String, String> userLoginInfo =initUI();

		// 验证用户名和密码
		boolean loginSuccess = login(userLoginInfo);

		// 最后输出结果
		System.out.println(loginSuccess ? "登录成功!" : "登录失败!");

	}

	/**
	 * 用户登录
	 * @param userLoginInfo 用户登录信息
	 * @return false表示失败,true表示成功
	 */
	private static boolean login(Map<String, String> userLoginInfo) {
		// 打标记的意识
		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<String, String> initUI() {
		Scanner s = new Scanner(System.in);
		System.out.println("用户名:");
		String loginName = s.nextLine();
		System.out.println("密码:");
		String loginPwd = s.nextLine();
		s.close();
		Map<String, String> userLoginInfo = new HashMap<>();
		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<String, String> userLoginInfo =initUI();
		// 验证用户名和密码
		boolean loginSuccess = login(userLoginInfo);
		// 最后输出结果
		System.out.println(loginSuccess ? "登录成功!" : "登录失败!");

	}

	/**
	 * 用户登录
	 * @param userLoginInfo 用户登录信息
	 * @return false表示失败,true表示成功
	 */
	private static boolean login(Map<String, String> userLoginInfo) {
		// 打标记的意识
		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<String, String> initUI() {
		Scanner s = new Scanner(System.in);
		System.out.println("用户名:");
		String loginName = s.nextLine();
		System.out.println("密码:");
		String loginPwd = s.nextLine();
		Map<String, String> userLoginInfo = new HashMap<>();
		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时,会直接出现报错
image

这次错误信息提示“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);

这样就能够成功解决这个问题
image

image

存在就有道理,因此我们不能将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, "张三丰");

image

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, "张三丰");

image

3、删除数据库
// 删除数据库
String sql = "delete from t_user where realName = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "张三丰");

image

12、账户转账演示事务

回滚是什么?
在数据库中,"回滚"指的是将数据库状态回退到某个确定点的操作。在SQL中,用于回滚的语句是 ROLLBACK
当在事务中执行了如 INSERT, UPDATE, 或 DELETE 这样的操作后,如果你需要取消这些操作,并恢复到事务开始前的状态,你可以使用 ROLLBACK 命令。这个命令将撤销事务内所有未提交的更改

START TRANSACTION;
INSERT INTO t_act(actno, balance) VALUES (333, 15000);
-- 假设这里发生了错误或者我们出于某种原因需要撤销之前的操作
ROLLBACK;

例如:没有进行操作的数据库表
image
进行修改后回滚
image

在IDEA里面,同样需要回滚的操作,保证安全性
重点三行代码 :

conn.setAutoCommit(false); // 开启事务
conn.commit(); // 提交事务
conn.rollback(); // 回滚事务

这里演示了一个账户转账的例子,在第一个账户转账后,会出现异常;如果不进行回滚,那么第一个账户完成了转账,第二个账户却不会收到转账
image

我们需要定义一个事务的起始点

// 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运行,在运行第二个程序,此时的被锁住的数据未被修改;当第一个事务运行结束后,第二个程序就能够对数据库进行修改了

posted @ 2024-04-13 13:32  佛砸氢氟酸  阅读(710)  评论(0)    收藏  举报