My JDBC notes
# JDBC
[TOC]
# JDBC 是什么
Java Database Connectivity (Java 语言连接数据库)
驱动力的 class 文件都是对 java.sql 包里的所有接口的实现类 (classes implementing those interfaces)
模拟 JDBC
sun 公司写的:
MySQL 厂家程序员写的: (实现类就是驱动,这里就是 MySQL 的驱动)
我们写的:
没有相对应的数据库的驱动,我们还是可以写代码然后编译通过,但是运行不了.
我们需要去官网下载好对应的驱动 jar 包,然后将其配置到环境变量 classpath 当中。放到 classpath 之后类加载器才会加载到.
(如果使用 IDEA, 不需要这么配,只需要在那个 module 的 module setting 里面选上那个 jar 包作为 library 就行)
# JDBC 编程六步
-
第一步:注册驱动 (告诉 Java 程序,即将要连接的是哪个品牌的数据库)
-
第二步:获取连接 (表示 JVM 的进程和数据库进程之间的通道打开了, 这属于进程之间的通信,使用完之后一定要关闭通道)
-
第三步:获取数据库操作对象 (专门执行 sql 语句的对象)
-
第四步:执行 SQL 语句 (DQL DML …)
-
第五步:处理查询结果集 (只有当第四步执行的是
select
语句的时候,才有这第五步处理查询结果集) -
第六步:释放资源 (关闭 java 和数据库两个进程之间的通信)
# 例子:执行 DML 语句
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import ...(其他需要的)
public class learn {
public static void main(String[] args) {
//需要之后finally关闭时候要让知道这些variables不然找不到,先declare
Statement statement = null;
Connection connection = null;
try {
//1. 注册驱动
//这里需要是带包名,因为没有import进来这个class
Driver driver = new com.mysql.cj.jdbc.Driver();
DriverManager.registerDriver(driver);
//2. 获取连接(对象)
String url = "jdbc:mysql://ip:port/databaseName"; //这里都是用这种格式
String user = "root";
String password = "XXX";
connection = DriverManager.getConnection(url, user, password);
//(下面继续...)
这样子就能连接到其他设备上的数据库,在有账号密码端口 IP 地址数据库名等的情况下,且对方没开防火墙之类的.
//3. 从连接对象调用方法获取一个Statement对象来将SQL语句发送到数据库
statement = connection.createStatement(); //这个Statement对象就是专门用来执行SQL语句的
String sql = "insert into dept(deptno, dname, loc) values(50,'人事部','北京')";
//4. 执行SQL语句
//这个方式是专门用来执行DML语句的---update,insert,select
//返回值是影响数据库的记录条数(number of rows affected by your sql dml command)
int count = statement.executeUpdate(sql);
//这里只有影响到一行数据,insert一行,所以是1
System.out.println(count == 1 ? "保存成功" : "保存失败");
} catch (SQLException e) {
e.printStackTrace();
} finally {
//6. 释放资源
//为了确保资源一定释放,在finally语块中关闭资源
//并且要遵循从小到大
//分别对其try...catch,不能放在用一个try..catch因为如果第一个有问题会直接被catch那第二个就无法执行了他的close方法
try {
if (statement != null) {
//先关小的,也就是Statement type的
statement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (connection != null) {
//再关大的,也就是Connection type的
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
注意:
- 注册驱动 new Driver 的时候带上包名,如果是其他数据库包名名字等会不一样
- url 的写法不同数据库也会都不一样
registerDriver(Driver driver)
,getConnection(url, user, password)
,createStatement()
,executeupdate(String sql)
- 那个 String 的 sql 语句不需要写;来结尾,写了会报错 (String sql = “…(这里不要;结尾)”😉
executeUpdate(String sql(DML语句))
方法是给 DML 语句 (delete,insert,update) 用的,返回值是 number of rows affected by your DML statement. 不是给 DQL 使用的!- 这里是 DML 语句,不是 DQL 语句,所以没有第五步
- Java 那些基础,try…catch 外面声明之后要关闭的这样 finally 语句块才可以 access
- 关闭时 == 从小到大关 ==
- 关闭时要在 finally 语句块里
- 关闭时要__各自有个 try…catch, 而不是一起 try…catch__
# 注册驱动的另外一种方式
这种方式是__最常用的,之前方式几乎不用__
//注册驱动,第一种方式方式(上面用的):
//Driver driver = new com.mysql.cj.jdbc.Driver();
//DriverManager.registerDriver(driver);
//注册驱动,以反射机制的方式:
Class.forName("com.mysql.cj.jdbc.Driver"); //也不需要他的这个返回值
上面这一行代码就能注册驱动了!!!
这是因为执行静态方法
Class.forName
的时候,类加载器就会加载你给的 String 参数的这个类,加载一个类的时候会执行那个类的静态代码块 (static{...}
) 如果有的话.而这里一般我们给
Class.forName
提供的参数就是比如说 MySQL 厂家提供的驱动 (class 文件) 的名字,具体点说就是他们写的 Driver 类 (实现类,实现了 JDBC 的接口 (java.sql.Driver)).而这个 "com.mysql.cj.jdbc.Driver" 这个 Driver.class 文件中就有静态代码块,如以下:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
这里的静态代码块已经包括了自己创建了自己的对象 (new Driver () 也就是 a instance of itself), 然后让 DriverManager 注册自己新建的 Driver 对象 (驱动).
所以我们执行
Class.forName
时,我们提供的那个类就会执行自己类里面写的静态代码块,所以就会执行这里创建一个自己的对象然后让注册这个对象 (驱动).
Class.forName
也会给我们返回一个 Class type 的 return value, 但我们也不需要这个,所以就没必要 assign to any variables.
我们喜欢用这种方式因为我们只需要一个 String 参数给 Class.forName 方法,所以我们可以直接在配置文件 (xxx.properties) 进行很容易的操作等
# 将数据库的所有信息配置到配置文件中
FileReader reader = new FileReader("jdbc"); //读取jdbc.properties文件
//创建并存好对应数据
Properties properties = new Properties();
properties.load(reader);
//关闭流
reader.close();
//注册驱动
String driver = properties.getProperty("driver");
Class.forName(driver);
//获取连接
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
connection = DriverManager.getConnection(url, user, password);
这里用了 getProperty 方法而不是 get 方法是因为 getProperty 是 Property 类的方法,返回也是 String, 不然你还要转型等等才可以接受到返回值.
DQL 语句和处理查询结果集 (第五步)
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null; //这个就是结果集
try{
....(Properties一些东西)
//1. 注册驱动
Class.forName(driver);
//2. 获取连接
Connection connection = DriverManager.getConnection(url,user,password);
//3. 获取数据库操作对象
Statement statement = connection.createStatement();
String sql = "select empno,ename,sal from emp";
//4. 执行sql语句(DQL)
//这里不是executeUpdate(...(DML语句))方法,因为那是给DML用的->delte,update,insert
//这里要执行select语句也就是DQL,需要用到executeQuery(...(DQL语句)),返回值是ResultSet typed的!
resultSet = statement.executeQuery(sql);
//5. 处理查询结果集
while(resultSet.next()){
//从1开始!!! 1->当前row的第一行的数据
String empno = resultSet.getString(1);
String ename = resultSet.getString(2);
String sal = resultSet.getString(3);
//或者:
String empno = resultSet.getString("empno");
String ename = resultSet.getString("ename");
String sal = resultSet.getString("sal");
//这么做也行
//int sal = resultSet.getInt("sal");
//double sal = resultSet.getDouble("sal");
}
} catch(...){
...
} finally{
//6. 释放资源
//关闭从小到大,先关闭resultSet!
if(resultSet != null){
try{
resultSet.close();
}catch(...){
...
}
}
if(statement != null){
try{
statement.close();
}catch(...){
...
}
}
if(connection != null){
try{
connection.close();
}catch(...){
...
}
}
}
注意:
executeQuery(String sql(DQL语句))
是给 DQL 语句用的,返回值是 ResultSet 类型的最后关闭也要关闭 ResultSet type 的 variable, 而且还要先关闭,因为是从 Statement 类那得来的,所以比 Statement 还小
返回的 ResultSet 就是查询结果 (不包括 column headings)
ResultSet 的
next()
方法可以移光标到下一个,然后返回这下一个是不是数据,true 为是,false 为不是.ResultSet 的
getString(int index)
方法不管数据库存储的数据类型是什么, 都以 String 类型取出这里 getString (int index) 方法的 index 从 1 开始!
1 代表的是 next 所指向的那一行数据的第 1column 的数据
2 代表的是 next 所指向的那一行数据的第 2column 的数据
3 代表的是 next 所指向的那一行数据的第 3column 的数据
或者就是用
getString(String 字段名)
来获取当前 row 的那个字段的数据注意!!! 如果使用以上方式,那么注意如果你在你上面写的那个 select 语句 (DQL 语句) 中重命名了一个字段 (用了
as ...
), 那么你这里就必须要用那个重命名了的名字,而不是表里原来的字段名。这是因为你这里得到的结果集就是你执行 select 语句之后的结果,那张表 (就像你在 DOS 窗口输入你那个 select 语句之后一样), 而并不是直接看原表,所以这个表结果上的字段名要是起了别名就要用你起的那个名
- 可以使用
getInt(String 字段名)
, 或者getDouble(String 字段名)
等来取回 int 或者 double 的值。不过需要看情况,需要数据存的那个数据就是 int 数据类型,才可以取出作为 int,double 同理.
# 实际使用
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
/**
* This class checks user input against the database records
*
* @author: Harry Qu
* @create: 9/12/2021
*/
public class practice {
public static void main(String[] args) {
Map<String, String> userLogin = initialise();
Boolean result = logIn(userLogin);
System.out.println(result?"Login success":"Login failed");
}
private static Boolean logIn(Map<String, String> userLogin) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
String username = userLogin.get("username");
String password = userLogin.get("password");
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://ip:port/database","root","XXX");
statement = connection.createStatement();
//注意加上'',sql语句需要
String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";
resultSet = statement.executeQuery(sql);
//如果select语句返回了数据(证明有这个用户名和密码的)这个方法就会返回true
return resultSet.next();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement!=null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return false;
}
private static Map<String, String> initialise() {
HashMap<String, String> hm = new HashMap<>();
System.out.print("Please enter username: ");
Scanner sc = new Scanner(System.in);
String username = sc.nextLine();
System.out.print("Please enter password: ");
String password = sc.nextLine();
hm.put("username",username);
hm.put("password",password);
return hm;
}
}
这里直接把输入的内容放到 select 语句的 where 部分,如果输入的都对那么就会返回那个值,所以直接调用 next 方法来确定到底对不对 (有没有 select 展示出来)
# sql 注入
以上例子,如果我们输入的信息不对,但是搞了 sql 注入,会照样显示 "Login success".
比如说:
# 导致 sql 注入的根本原因
导致 sql 注入的根本原因就是用户输入进的数据含有__关键字 (比如说:or)__并且这些__关键字__参与 sql 语句的编译过程,导致 sql 语句的愿意被扭曲,进而达到 sql 注入.
上面我们执行的 sql 语句是:
String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";
如果我们输入的是这个:
username: XXX
password: XXX’ or ‘1’=’1
所以我们其实执行的__sql__语句其实是的是:
select * from t_user where username = 'XXX' and password = 'XXX' or '1'='1';
这行 sql 语句代表 username=‘XXX’并且密码是‘XXX’_或者___所有’1’='1’的,注意__and 的执行顺序优先于 or
‘1’='1’是会一直会成立的,所以也就相当于这个 where 并没有过滤掉任何的信息,还是原来的那张表
这一行 sql 语句如果在 DOS 窗口执行,会直接把 t_user 整张表 select 出来!!!
而这里我们的判断逻辑就是只要有数据就成功,所以就攻进去了
# 解决 sql 注入
只要用户提供的信息不参与 sql 语句的编译过程,问题就解决了
即使用户提供的信息中含有 sql 语句的关键字,但是没有参与编译,不起作用
如果不想用户提供的信息参与 sql 语句的编译,那么就需要用 java.sql.PreparedStatement 而不是 Statement
PreparedStatement__接口__继承了 java.sql.Statement\
PreparedStatement 是属于__预编译的数据库操作对象__
PreparedStatement 的__原理是预先对 sql 语句框架进行编译,然后再给 sql 语句传值__
private static Boolean logIn(Map<String, String> userLogin) {
Connection connection = null;
ResultSet resultSet = null;
// Statement statement = null; <-为了解决sql注入,不使用这个了
//用这个
PreparedStatement ps = null;
try {
//statement = connection.createStatement(); <-为了解决sql注入,不使用这个了
//sql语句的框子
String sql = "select * from t_user where username = ? and password = ?";
//创建预编译数据库操作对象
ps = connection.prepareStatement(sql);
//给第一个占位符(第一个?)传值
ps.setString(1,username);
//给第二个占位符(第一个?)传值
ps.setString(2,password);
//执行,并获得查询结果集
resultSet = ps.executeQuery();
//如果select语句返回了数据(证明有这个用户名和密码的)这个方法就会返回true
return resultSet.next();
} catch (Exception e) {
e.printStackTrace();
}finally {
...
if (ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
...
return false;
}
注意:
- 不适用 Statement 而是 PreparedStatement
- 建立预编译的数据库操作对象是
connection.prepareStatement(String sql);
注意不是 prepared, 是 prepare!- 因为是预编译对象,所以要先创建 sql 语句然后传进
connection.prepareStatement(String sql);
来创建这个用汇编与对象- 这个 sql 语句我们用
?
来代表一个值 (相当于是 placeholder), 每一个?
代表一个 placeholder, 然后会在将来接收一个值- 这个问号__不能!!!__向我们之前一样用
''
单引号包起来,必须就是String xxx = "... where ... = ? ..."
在 java String 语句的双引号里面。如果要是加了''
单引号把问号包起来,那么就会被认为成普通的问号.- 这个 sql 语句相当于是个 sql 语句框子通过
connection.prepareStatement(String sql);
方法传给了 DBMS, 然后 DBMS 进行 sql 语句的预先编译.(就已经编译好了)- 之后就是给这个问号 (占位符) 传值用
ps.setString(下标(第几个占位符),值)
, 这里 ps 以及知道 sql 语句的框子了知道哪有占位符,所以直接拿 ps 叫方法,这里的下标给的是第几个占位符,1 代表第一个占位符,2 代表第二个占位符. JDBC 中所有下标从 1 开始!不是 0!- 这里
ps.setString(下标(第几个占位符),值)
会自动把你传的值拿''
单引号给包起来因为这个是setString
, 如果用的比如说是ps.setInt(下标(第几个占位符),值)
, 就不会拿单引号给包起来了而且只能传 int 的值.- 这个样子就算你传过去的值带有 sql 语句关键字,但是这些并没有参加编译,因为 sql 语句已经编译过了,所以就不会发生之前的 sql 注入
- 这里用的
ps.executeQuery();
方法就不需要一个String sql
的参数了. 不要再传了!!!都已经传过了,要在传又再次编译
# Statement 和 PreparedStatement 区别
-
Statment 存在 sql 注入问题,而 PreparedStatement 解决了 sql 注入
-
__PreparedStatement 效率更高!Statement 是编译一次执行一次,PreparedStatement 是编译一次,可执行 N 次
这是因为数据库方面,如果反复同一个语句:
select ename from emp;
select ename from emp;
select ename from emp;
select ename from emp;
因为是完全一样的,所以不会反复编译,如果不一样了比如说多了个空格:
select ename from emp ;
这样就不时一样,会重新编译了.
所以,PreparedStatement 是提前把 sqsl 语句框子预编译好了,那个编译的永远不会变,那个占位符 (问号) 也永远不会变,在传值之前就__编译好了__, 所以就算是占位符但也确实是不会变的,所以编译一次就足够.
之后要是传值,不管怎么传,传多少,之前的语句已经编译好了且占位符本身 (那个问号) 等等都不会变,所以就不会再次编译,效率更快.
-
PreparedStatement 会在编译阶段做类型的安全检查,更安全
如果用 Statement 和相关方法,即使你传的数据其实是个 int 但你那个字段应该是个字符串类型的,他也不会报错,因为你拿‘’单引号给他们包起来了.
而 PreparedStatement 不一样,他使用 setString,setInt 等方法,如果使用 setString 而你传的是个 int 类型的,那就会报错
综上所述:我们大部分都是用 PreparedStatement, 只有极小数的情况下用 Statement (比如说需要客户 sql 注入的时候).
需要 sql 注入的例子 -> 比如说在网购,可以价格升序或者降序排序,需要用到 order by xxx asc/desc
这时候就不能用 PreparedStatement 和占位符,如果用了,比如会是 order by xxx ?
, 之后再用 setString 给占位符传值,会自动给你整个外面阔上个单引号 (因为是 setString), 比如说 setString(1,”desc”);
, 这样整个就成 order by xxx 'desc'
.
这个样子不行,所以必须要用 Statement 以此来允许 sql 注入.
# PreparedStatement 增删改
String sql = "insert into t_user(username,password) values(?,?,?)";
...//(预编译,setString,executeUpdate方法(没有String sql参数这次),等)
String sql = "delete from t_user where username = ? and password = ?";
...//(预编译,setString,executeUpdate方法(没有String sql参数这次),等)
String sql = "update t_user set username = ?, password = ? where id = ?"
...//(预编译,setString,setInt,executeUpdate方法(没有String sql参数这次),等)
# JDBC 事务
JDBC 中的事务是自动提交的 -> 只要执行任意一条语句,则自动提交一次,这是 JDBC 默认的事务行为.
这不符合实际开发,实际开发需要捆绑多个 DML 语句共同联合才能完全,所以需要保证他们这些 DML 语句全都在一个事务中同时成功或者同时失败.
需要将自动提交机制改为手动提交:
try{
...//注册驱动,获取连接对象等
//取消自动提交机制,改为手动提交
connection.setAutoCommit(false);
... // 获取预编译数据库操作对象或者数据库操作对象,sql语句,执行语句啥的等等...
... //各种(DML)sql语句的执行
... //各种(DML)sql语句的执行
... //各种(DML)sql语句的执行
//程序能够走到这里说明上面都没出异常,上面的所有操作包括DML语句的执行都是顺利的,这个时候事务结束,可以提交事务.
connection.commit();
} catch(...) {
//这里必须回滚事务,不然要是上面try只成功执行了
if (connection != null){
connection.rollback();
}
} finally {
...
}
connection.setAutoCommit(false);
取消自动提交,相当于是开启事务connection.commit();
在多个 DML 语句执行之后,提交事务connection.rollback();
__需要这个!!__如果在执行多个 DML 语句 (事务) 时,出现异常,那么就需要让之前成功执行的 DML 语句__存在历史的操作给删掉!!! 因为就算你把自动提交给关了,他不会自动提交 (每执行完一个 DML 语句就自动让硬盘的数据随之改变), 但还是会在历史留下记录,这后要是提交事务会按照历史中的__所有__操作给提交让硬盘文件随之改变,所以需啊哟__回滚一下,结束当前失败事务 (因为抛异常了), 清除下历史,之后要再开启新的事务就不会被这次的不成功的事务所影响.- 确认 connection 不是 null, 才可以叫他的方法,不然会空指针异常
- 确认回滚所在 catch 是可以接收到所有执行事务时可能抛出的 exception
# JDBC 工具类的封装
可以自己创个类,然后创一堆静态方法,然后通过类名去叫这些方法,然后把一些 main 方法里面重复的直接改为用自己创建的方法.
注意!可以将这些静态方法改为 private, 这个样通过 instance 是叫不出来的! 但是可以通过类名叫!!!
在这个封装类中,可以直接把注册驱动给放到静态代码块,这样就在一开始用这里面的方式时候,他这个就会运行里面的
Class.forName(...)
, 以后再用里面的方法也不需要再重新注册驱动啥的,放这就运行一次,一次就够了.
这个方法直接
DBUril.getConnection()
就可以了,很方便,然后这里需要 throws 而不是在这里 trycatch 处理 exception.
这个方法直接关闭所有的,更方便.
注意!这里传的是 Statement 类的,但是也可以传 PreparedStatement, 因为 PreparedStatement 皆可哦继承 Statement 接口,多态机制,这样既可以是 PreparedStatement 也可以是 Statement 或者其他…
这里没有 throws 异常而是 trycatch, 这样在 main 里面用这个也不用 trycatch 了.
# 使用工具类和模糊查询
需要整个传值过去,而不是使用‘…?..’在 sql 语句,因为这里的单引号会把这个问号看做是一个普通问号,而不是一个占位符。应该 sql 语句 like 那里直接整个都是个占位符,然后再给占位符传正常想查的东西,比如说 "_A%".(第二个字母是 A 的所有的)