# JDBC

[TOC]

# JDBC 是什么

Java Database Connectivity (Java 语言连接数据库)

image-20211220192831959

image-20211220194142601

驱动力的 class 文件都是对 java.sql 包里的所有接口的实现类 (classes implementing those interfaces)

模拟 JDBC

sun 公司写的:

image-20211220200442403

MySQL 厂家程序员写的: (实现类就是驱动,这里就是 MySQL 的驱动)

image-20211220220754987

我们写的:

image-20211220221305676

没有相对应的数据库的驱动,我们还是可以写代码然后编译通过,但是运行不了.

我们需要去官网下载好对应的驱动 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);

//(下面继续...)

image-20211221180331661image-20211221180647877

这样子就能连接到其他设备上的数据库,在有账号密码端口 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) 进行很容易的操作等

# 将数据库的所有信息配置到配置文件中

image-20211221195249864

	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 同理.

image-20211221214836641

# 实际使用

image-20211223000737601

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".

比如说:

image-20211223010121462

# 导致 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 是叫不出来的! 但是可以通过类名叫!!!

image-20211224023854257

在这个封装类中,可以直接把注册驱动给放到静态代码块,这样就在一开始用这里面的方式时候,他这个就会运行里面的 Class.forName(...) , 以后再用里面的方法也不需要再重新注册驱动啥的,放这就运行一次,一次就够了.

image-20211224024312954

这个方法直接 DBUril.getConnection() 就可以了,很方便,然后这里需要 throws 而不是在这里 trycatch 处理 exception.

image-20211224024507918

这个方法直接关闭所有的,更方便.

注意!这里传的是 Statement 类的,但是也可以传 PreparedStatement, 因为 PreparedStatement 皆可哦继承 Statement 接口,多态机制,这样既可以是 PreparedStatement 也可以是 Statement 或者其他…

这里没有 throws 异常而是 trycatch, 这样在 main 里面用这个也不用 trycatch 了.

# 使用工具类和模糊查询

image-20211224032915752

image-20211224032957657

需要整个传值过去,而不是使用‘…?..’在 sql 语句,因为这里的单引号会把这个问号看做是一个普通问号,而不是一个占位符。应该 sql 语句 like 那里直接整个都是个占位符,然后再给占位符传正常想查的东西,比如说 "_A%".(第二个字母是 A 的所有的)