JDBC是什么?
JDBC是 Java DataBase Connectivity(Java语言连接数据库)
JDBC是SUN公司制定的一套接口(interface)
为什么要制定一套接口?
因为每一个数据库的实现原理都不一样,每一个数据库产品都有自己独特的实现原理。SUN公司为了简化开发人员对数据库的统一操作,提供了一个Java操作数据库的规范,即JDBC。JDBC接口的实现类由具体数据库厂商提供,开发人员只需要掌握JDBC接口的操作即可。
JDBC编程六步
- 第一步:注册驱动
- 第二步:获取连接
- 第三步:获取数据库操作对象
- 第四步:执行SQL语句
- 第五步:处理查询结果集
- 第六步:释放资源
/*
JDBC编程六步
*/
import java.sql.*;
public class JDBCTest05 {
public static void main(String[] args) {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try{
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbcstudy?setSSL=true","root","root");
//3、获取数据库操作对象
st = conn.createStatement();
//4、执行sql
String sql = "SELECT * FROM users";
rs = st.executeQuery(sql);
//5、处理查询结果集
// rs.next()方法将结果集指针指向下一位,可以通过while()循环遍历整个结果集
while(rs.next()) {
/*
// getString()方法的特点是:不管数据库中的数据类型是什么,都以String形式取出
String id = rs.getString(1);
// JDBC 所有下标从1开始,不是从0开始
// 可以用“列号”,也可以用“列名”, 建议用”列名“,提高代码健壮性,如:
String id = rs.getString(1);
String id = rs.getString("id"); // 建议使用这种方式
// 注意:列名称不是表中的列名称,是查询结果集中的列名称
// 若将sql语句改为 sql = "SELECT id AS a, loginName, email FROM diary"; 则列名id也应改为a。
// String id = rs.getString("a");
String id = rs.getString("id");
String loginName = rs.getString("loginName");
String email = rs.getString("email");
System.out.println(id + "," + loginName + "," + email);
*/
// 除了可以以String类型取出之外,还可以以特定的类型取出。注意:需要一一对应
/* 用下标取
int id = rs.getInt(1);
String loginName = rs.getString(2);
System.out.println(id + "," + loginName);
*/
// 用”列名“取
int id = rs.getInt("id");
String loginName = rs.getString("loginName");
System.out.println(id + "," + loginName);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6、释放资源
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
DriverManager 驱动管理器
// 通过DriverManager的方式注册驱动
/*
Driver driver = new com.mysql.jdbc.Driver();
DriverManager.registerDriver(driver);
*/
// 通过类加载的方式注册驱动
Class.forName("com.mysql.jdbc.Driver()");
Connection建立数据库连接
// connection代表数据库
// url与对应的数据库有关,这里用MySQL举例:
// String url = "jdbc:mysql://localhost:3306/jdbcstudy?setSSL=true"
// url公式:"协议://主机地址:端口号/数据库名?参数1&参数2&参数3"
// username为数据库登录用户名,password为数据库登录密码(一般都是root)
Connection conn = DriverManager.getConnection(url, username, password);
// 事务处理机制
// 数据库设置自动提交,传入false表示关闭自动提交,默认值为true
conn.setAutoCommit(boolean);
// 事务提交
conn.commit();
// 事务回滚
conn.rollback();
Statement: 执行SQL的对象
// 创建一个statement对象
Statement statement = conn.createStatement();
String sql = ""; // 编写具体的sql语句
statement.execute(sql); // 执行任何sql
statement.executeUpdate(sql); // 执行 增、删、改,返回受影响的行数
statement.executeQuery(sql); // 执行查询操作,返回一个查询结果集 ResultSet
ResultSet查询结果集:封装了所有的查询结果
// 获得查询结果集
ResultSet resultSet = statement.executeQuery(sql);
// 可以获得指定的数据类型
resultSet.getObject();
resultSet.getString();
resultSet.getInt();
resultSet.getDouble();
resultSet.getFloat();
resultSet.getDate();
//...
// 通过查询结果集的指针遍历整个查询结果
resultSet.next(); // 移动到下一个数据
resultSet.previous(); // 移动到上一个数据
resultSet.beforeFrist(); // 移动到最前面(初始位置)
resultSet.afterLast(); // 移动到最后面
resultSet.absolute(row); // 移动到指定行
释放资源
为了保证资源一定释放,要在finally语句块(无论是否出现异常都能够执行)中关闭资源,遵循后开先关的原则。
例如:先关闭查询结果集,再关闭数据库操作对象,最后释放数据库连接。
//6、释放资源
// 关闭查询结果集
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 关闭数据库操作对象
if(st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 释放数据库连接
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
SQL 注入的问题
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
演示SQL 注入问题
创建一个测试用数据库
CREATE DATABASE jdbcStudy CHARACTER SET utf8 COLLATE utf8_general_ci;
USE jdbcStudy;
CREATE TABLE users(
id INT PRIMARY KEY,
loginName VARCHAR(40),
loginPwd VARCHAR(40),
email VARCHAR(60),
birthday DATE
);
INSERT INTO users(id,loginName,loginPwd,email,birthday)
VALUES(1,'zhangsan','123456','zs@qq.com','1980-12-04'),
(2,'lisi','123456','lisi@qq.com','1981-12-04'),
(3,'wangwu','123456','wangwu@qq.com','1979-12-04');
代码演示
实现功能:
1、需求:模拟用户登录功能的实现
2、业务描述:程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码用户输入用户名和密码之后,提交信息,java程序收集到用户信息 Java程序连接数据库验证用户名和密码是否合法。合法:显示登陆成功;不合法:显示登陆失败
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class JDBCTest06 {
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;
Statement st = null;
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBCStudy?useSSL=true","root","root");
// 3、获取数据库操作对象
st = conn.createStatement();
// 4、执行sql
String sql = "SELECT * FROM users WHERE loginName = '" + loginName + "' AND loginPwd = '" + loginPwd + "'";
rs = st.executeQuery(sql);
// 5、处理查询结果集
if(rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
if(rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(st != null) {
try {
st.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.print("用户名:");
String loginName = s.nextLine();
System.out.print("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName",loginName);
userLoginInfo.put("loginPwd",loginPwd);
return userLoginInfo;
}
}
SQL注入后的登录结果
导致SQL注入的根本原因
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被扭曲,进而达到sql注入。
// 4、执行sql
String sql = "SELECT * FROM users WHERE loginName = '" + loginName + "' AND loginPwd = '" + loginPwd + "'";
// 拼接后的sql语句
// SELECT * FROM users WHERE loginName = 'gdufs' AND loginPwd = 'gdufs' or '1' = '1';
// 以上正好完成了sql语句的拼接,以下代码发送sql语句给DBMS,DBMS进行sql编译。
// 正好能将用户提供的”非法信息“编译进去,导致了原sql语句的含义被扭曲了。
解决SQL注入问题
SQL注入的根本原因是用户输入的信息中含有SQL语句的关键字,并且这些关键字参与sql语句的编译过程,导致SQL语句的原意被扭曲,进而达到SQL注入。因此,只要用户提供的信息不参与SQL语句的编译过程,问题就解决了。PreparedStatement类就能很好的帮助我们完成这个操作。PreparedStatement接口继承了java.sql.Statement,是属于预编译的数据库操作对象,它的原理是预先对sql语句的框架进行编译,然后再给sql语句传“值”。
PreparedStatement类:执行SQL的对象
// SQL语句框架,一个?表示一个占位符,一个? 接受一个“值”。注意:占位符不能用单引号括起来
String sql = "SELECT * FROM t_user WHERE loginName = ? AND loginPwd = ?";
// 程序执行到此处,会发送sql语句框架给DBMS,然后DBMS进行sql语句的预先编译
PreparedStatement ps = conn.prepareStatement(sql);
// 给占位符? 传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
// 传值完毕,执行sql
rs = ps.executeQuery(); // 注意这里不需要再传入sql
代码演示
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class JDBCTest07 {
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/bjpowernode?useSSL=true","root","root");
// 3、获取预编译的数据库操作对象
// SQL语句框架,一个?表示一个占位符,一个? 接受一个“值”。注意:占位符不能用单引号括起来
String sql = "SELECT * FROM t_user WHERE loginName = ? AND loginPwd = ?";
// 程序执行到此处,会发送sql语句框架给DBMS,然后DBMS进行sql语句的预先编译
ps = conn.prepareStatement(sql);
// 给占位符? 传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
// 4、执行sql
rs = ps.executeQuery(); // 不需要再次传入sql
// 5、处理查询结果集
if(rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} 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.print("用户名:");
String loginName = s.nextLine();
System.out.print("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName",loginName);
userLoginInfo.put("loginPwd",loginPwd);
return userLoginInfo;
}
}
SQL注入失败
Statement和PreparedStatement比较
- Statement存在SQL注入问题,PreparedStatement解决了SQL注入问题。
- Statement是编译一次执行一次,PreparedStatement是编译一次,可执行N次。PreparedStatement效率高一些。
- PreparedStatement会在编译阶段做类型的安全检查
综上所诉,PreparedStatement使用较多,只有极少数的情况下需要使用Statement
注:若业务方面要求是需要进行sql语句拼接的,则需要使用Statement
PreparedStatement完成增删改操作
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* PreparedStatement 完成 INSERT DELETE UPDATE 即 增、删、改
*/
public class JDBCTest09 {
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/jdbcstudy?useSSL=true","root","root");
// 3、获取预编译的数据库操作对象
/*增
String sql = "INSERT INTO users(loginName,loginPwd,email,birthday) VALUES(?,?,?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1,zhaoliu);
ps.setInt(2,123456);
ps.setString(3,"zhaoliu@qq.com");
ps.setString(4,"1982-12-04");
*/
/*改
String sql = "UPDATE users SET loginName = ?, email = ? WHERE id = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,"sunqi");
ps.setString(2,"sunqi@qq.com");
ps.setInt(3,4);
*/
// 删
String sql = "DELETE FROM users WHERE id = ?";
ps = conn.prepareStatement(sql);
ps.setInt(1,4);
// 4、执行sql
int count = ps.executeUpdate();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
JDBC中的事务机制
JDBC中的事务是自动提交的:只要执行任意一条语句,则自动提交一次,这是JDBC默认的事务行为。但是在实际的业务当中,通常都是N条DML语句的共同联合才能完成的,必须保证他们这些DML语句在同一个事务中同时成功或者同时失败。
演示JDBC事务的自动提交机制
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* 以下程序验证JDBC的事务是自动提交机制
*/
public class JDBCTest10 {
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/bjpowernode?useSSL=true","root","root");
// 3、获取预编译的数据库操作对象
String sql = "UPDATE dept SET dname = ? WHERE deptno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,"x部门");
ps.setInt(2,30);
// 4、执行sql
int count = ps.executeUpdate(); //执行第一条update语句
System.out.println(count); // 在此处加断点,可以发现数据库中数据已经更改
// 若程序在此处出现异常,则可能会出现数据丢失、损坏等情况
// 重新给占位符传值
ps.setString(1,"y部门");
ps.setInt(2,20);
count += ps.executeUpdate(); //执行第二条update语句
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
解决事自动提交机制带来的问题
在jdbcstudy数据库中创建一张测试表
drop table if exists act;
create table act(
actno bigint,
balance double(7,2)
);
insert into act(actno,balance) values(111,20000);
insert into act(actno,balance) values(222,0);
commit;
select * from act;
代码演示
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* 重点三行代码:
* conn.setAutoCommit(false); 关闭自动提交机制,改为手动提交
* conn.commit(); 事务执行完毕,可以提交,手动提交数据
* conn.rollback(); 如果出现异常,回滚事务,确保数据不丢失
*/
public class JDBCTest11 {
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/jdbc?useSSL=true","root","root");
// 将自动提交机制改为手动提交
conn.setAutoCommit(false); // 开启事务
// 3、获取预编译的数据库操作对象
String sql = "UPDATE act SET balance = ? WHERE actno = ?";
ps = conn.prepareStatement(sql);
// 给?传值
ps.setDouble(1,10000);
ps.setInt(2,111);
int count = ps.executeUpdate();
// 让代码出异常,中断转账,代码进入catch块,事务回滚,数据库无数据丢失
/*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 throwables) {
throwables.printStackTrace();
}
}
e.printStackTrace();
} finally {
if(ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
JDBC工具类
为什么要有JDBC工具类?
在我们编写JDBC代码的时候,会出现大量重复编写的代码,例如注册驱动、获取连接、释放资源等。这不符合我们代码复用的原则,因此我们可以把这些重复编写的代码封装成静态方法供我们调用,这就是我们的JDBC工具类(JDBCUtil)。
代码演示
package utils;
import java.sql.*;
/**
* JDBC工具类,简化JDBC编程
*/
public class JDBCUtil {
private JDBCUtil() {}
// 静态代码块在类加载时执行,并且只执行一次
static {
try {
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接对象
* @return 连接对象
* @throws SQLException 抛出SQL异常
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbcstudy?useSSL=true","root","root");
}
/**
* 关闭资源
* @param conn 连接对象
* @param ps 数据库操作对象
* @param rs 结果集
*/
public static void close(Connection conn, Statement ps, ResultSet rs) {
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();
}
}
}
}
测试工具类
import utils.JDBCUtil;
import java.sql.*;
/**
* 程序作用:
* 1、测试DBUtil是否好用
* 2、写一个模糊查询
*/
public class JDBCTest12 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 调用工具类获取连接
conn = JDBCUtil.getConnection();
// 获取预编译的数据库操作对象
// 错误写法:占位符 ? 不能在单引号里
/*String sql = "SELECT ename FROM emp WHERE ename LIKE '_?%'";
ps = conn.prepareStatement(sql);
ps.setString(1,"A");*/
// 正确写法,用占位符将单引号内的所有字符的位置占住,传入整个字符串
String sql = "SELECT ename FROM emp WHERE ename LIKE ?";
ps = conn.prepareStatement(sql);
ps.setString(1,"_A%");
rs = ps.executeQuery();
while(rs.next()) {
System.out.println(rs.getString("ename"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 调用工具类释放资源
JDBCUtil.close(conn, ps, rs); // 如果不需要创建查询结果集,可以传入null值
}
}
}