JDBC详解

焦旭峰 于 April 1, 2021

JDBC详解

1、JDBC是什么?

​ JDBC是Java DataBase Connectivity,是Java访问数据库的标准规范,其本质上是SUN公司制定的一套接口,其位于JDK的java.sql包中。

​ JDBC接口的具体实现类由各大数据库厂商来实现,这些实现类叫做数据库驱动。实际运用时,需要根据项目中使用的DBMS来导入对应的驱动jar包。比如项目中使用MySQL数据库,则需要导入MySQL的驱动jar包。

1.1 使用JDBC的好处

​ 面向接口编程,程序员只需要会调用JDBC 接口中的方法,无需关注类的实现。

1.2 为什么SUN公司制定一套JDBC接口呢?

​ 因为每一个数据库的底层实现原理都不一样

2、JDBC开发前的一些准备工作

2.1、使用文本编译器开发

​ 先去数据库的官网下载对应的驱动jar包,然后将其配置到环境变量classpath当中。

例如,项目中使用的是MySQL数据库,则去官网下载mysql驱动,然后配置环境变量如下:

classpath=.;D:\course\06-JDBC\resources\MySql Connector Java 5.1.23\mysql-connector-java-5.1.23-bin.jar

2.2 使用IDEA开发

​ 使用IDEA的时候,不需要配置以上的环境变量。

3、JDBC编程六步简介

第一步:注册驱动(作用:告诉Java程序,即将要连接的是哪个品牌的数据库)

第二步:获取连接(表示JVM的进程和数据库进程之间的通道打开了,这属于进程之间的通信,重量级的,使用完之后一定要关闭通道。)

第三步:获取数据库操作对象(专门执行sql语句的对象)

第四步:执行SQL语句(DQL DML….)

第五步:处理查询结果集(只有当第四步执行的是select语句的时候,才有这第五步处理查询结果集。)

第六步:释放资源(使用完资源之后一定要关闭资源。Java和数据库属于进程间的通信,开启之后一定要关闭。)

4、JDBC编程六步详解

4.1、注册驱动

4.1.1、第一种方式
Driver driver = new com.mysql.jdbc.Driver();//多态,父类型引用指向子类型对象(MySQL的驱动)
//Driver driver = new oracle.jdbc.driver.OracleDriver();//Oracle的驱动
DriverManager.registerDriver(driver);

//两行代码可以合并为:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());

​ 其中,Driver是JDK中java.sql包下的一个JDBC接口com.mysql.jdbc.Driver类(MySQL驱动)实现了Driver接口,可在MySQL官网中下载对应的jar包,并将其配置到IDEA模块中。

4.1.2、第二种方式(常用)
Class.forName("com.mysql.jdbc.Driver");//使用反射机制

​ 那么为什么第二种方式更加常用呢?

​ 我们可以从com.mysql.jdbc.Driver的源码分析,其源码如下:

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {   //静态代码块,类加载时完成初始化,而Class.forName("完整类名")刚好可以完成类加载动作
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

​ 可以看出,注册驱动的动作在静态代码块中,因此只需要使用其类加载时机。另外Class.forName()方法的参数是一个字符串,该字符串可以写到properties属性配置文件中,同时我们不需要该方法的返回值。因此使用这种方式可以利用类加载动作完成静态代码块的初始化,从而实现注册驱动。

4.2、获取连接

​ 获取连接需要URL,数据库用户名和密码。

conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/db","root","123456");
4.2.1、URL

URL是统一资源定位符(网络中某个资源的绝对路径)。例如: https://www.baidu.com/ 这就是一个URL。

URL包括四个部分:协议,IP,端口号,资源名

4.2.2、数据库用户名和密码

​ 数据库的用户名和密码可以在相应的DBMS中进行设置。

4.3、获取数据库操作对象

数据库操作对象Statement是专门执行sql语句的对象。

可以通过数据库连接对象connection来获取数据库操作对象statement。

		stmt = conn.createStatement();

4.4、执行SQL语句

4.4.1、执行DQL语句
			String sql = "select empno as a,ename,sal from emp";
            rs = stmt.executeQuery(sql);

​ 注意:Statement的executeQuery(select语句)方法源码为:

ResultSet executeQuery(String sql) throws SQLException;

​ 其中ResultSet是JDK中java.sql包下的一个JDBC接口。

4.4.2、执行DML语句

使用Statement的executeUpdate方法执行DML语句。

			String sql = "insert into dept(deptno,dname,loc) values(50,'开发部','广州')";
            int count = stmt.executeUpdate(sql);

​ 注意:Statement的executeUpdate(insert/delete/update)方法为:

int executeUpdate(String sql) throws SQLException;

​ 该方法的返回值是”影响数据库中的记录条数”

4.5、处理查询结果集

只有当第四步中执行的是DQL语句时,才需要处理查询结果集。

​ 主要有四种方式:下标获取,列名获取,指定类型的下标获取,指定类型的列名获取

 while(rs.next()){
     //第一种,下标获取
/*     String empno = rs.getString(1);//JDBC中所有下标从1开始,不是从0开始的。
     String ename = rs.getString(2);
     String sal = rs.getString(3);
     System.out.println(empno + "," + ename + "," + sal);*/
         
     //第二种,这个不是以列的下标获取,以列的名字获取
  /*   String empno = rs.getString("a");
     String ename = rs.getString("ename");//重点注意:列名称不是表中的列名称,是查询结果集的列名称
     String sal = rs.getString("sal");
     System.out.println(empno + "," + ename + "," + sal);*/

  	//第三种,除了可以以String类型取出之外,还可以以特定的类型取出
/*     int empno = rs.getInt(1);
     String ename = rs.getString(2);
     double sal = rs.getDouble(3);
     System.out.println(empno + "," + ename + "," + (sal + 100));*/
         
	//第四种
     int empno = rs.getInt("a");
     String ename = rs.getString("ename");
     double sal = rs.getDouble("sal");
     System.out.println(empno + "," + ename + "," + (sal + 200));
 }

4.6、释放资源

使用完资源之后一定要关闭资源。Java和数据库属于进程间的通信,开启之后一定要关闭。

为了保证资源一定释放,在finally语句块中关闭资源,并且要遵循从小到大一次关闭,分别对其try..catch。

4.6.1、执行DML语句时如何释放资源?

​ 当执行的是DML语句时,先关闭Statement数据库操作对象,最后关闭Connection数据库连接对象。

finally {
            if (stmt != null){
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null){
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
4.6.2、执行DQL语句时如何释放资源?

​ 当执行的是DQL语句时,先关闭ResultSet查询结果集对象,再关闭Statement数据库操作对象,最后关闭Connection数据库连接对象。

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();
                }
            }
        }

5、什么是SQL注入?

​ 比如在一个网站的用户登录界面,输入如下用户名和密码:

用户名:feng
密码:feng' or '1'='1

​ 如果此时会显示登录成功,则发生了SQL注入

5.1、SQL注入的根本原因

用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被扭曲,进而达到sql注入。(可通过debug程序看出)

5.2、如何解决SQL注入

使用PreparedStatement可以解决SQL注入。

       	    //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);

5.3、PreparedStatement的原理

​ 只要用户提供的信息不参与SQL语句的编译过程,SQL注入问题就可以解决。或者用户提供的信息中含有SQL语句的关键字,但是没有参与编译,则关键字不起作用,SQL注入也可以得到解决。 要想用户信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement,PreparedStatement接口继承了java.sql.Statement,PreparedStatement是属于预编译的数据库操作对象。PreparedStatement的原理是预先对SQL语句的框架进行编译,然后再给SQL语句传“值”

5.4、Statement和PreparedStatement的区别

​ 1、Statement存在sql注入问题,PreparedStatement解决了sql注入问题

​ 2、Statement是编译一次执行一次。PreparedStatement是编译一次,可执行N次,PreparedStatement效率较高一些

​ 3、PreparedStatement会在编译阶段做类型的安全检查

​ 综上所述,PreparedStatement使用较多,只有极少数的情况下需要使用Statement。如果业务方面要求支持SQL注入,需要进行SQL语句拼接,则使用Statement。

6、JDBC的事务

默认是自动提交。自动提交:只要执行任意一条DML语句,则自动提交一次。

但是在实际的业务当中,通常都是N条DML语句共同联合才能完成的,必须保证他们这些DML语句在同一个事务中同时成功或者同时失败。此时需要将JDBC的事务修改为手动提交。

6.1、将JDBC的事务修改为手动提交

​ 使用conn.setAutoCommit(false)可以修改为手动提交。

例如:

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/db","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();//执行第一条UPDATE语句
            System.out.println(count);

            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) {
                    ex.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();
                }
            }
        }
    }
}

7、模糊查询

			//正确的写法(模糊查询)
            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"));
            }

8、悲观锁和乐观锁

8.1、悲观锁(行级锁)

​ 事务必须排队执行。数据锁住了,不允许并发。

​ 使用方式:select后面添加 for update

8.2、乐观锁

​ 支持并发,事务也不需要排队,只不过需要一个版本号。