Skip to content

JDBC

JDBC 是什么?JDBC 英文名为:Java Data Base Connectivity(Java 数据库连接),官方解释它是 Java 编程语言和广泛的数据库之间独立于数据库的连接标准的 Java API,根本上说 JDBC 是一种规范,它提供的接口,一套完整的,允许便捷式访问底层数据库。可以用 JAVA 来写不同类型的可执行文件:JAVA 应用程序、JAVA Applets、Java Servlet、JSP 等,不同的可执行文件都能通过 JDBC 访问数据库,又兼备存储的优势。简单说它就是 Java 与数据库的连接的桥梁或者插件,用 Java 代码就能操作数据库的增删改查、存储过程、事务等。

我们可以发现,JDK 自带了一个java.sql包,而这里面就定义了大量的接口,不同类型的数据库,都可以通过实现此接口,编写适用于自己数据库的实现类。而不同的数据库厂商实现的这套标准,我们称为数据库驱动

Screenshot 2023-12-13 at 01.02.08

准备工作

那么我们首先来进行一些准备工作,以便开始 JDBC 的学习:

  • 将 idea 连接到我们的数据库,以便以后调试。

  • 将 mysql 驱动 jar 依赖导入到项目中(推荐 6.0 版本以上,这里用到是 8.0)

https://dev.mysql.com/downloads/connector/j/

Screenshot 2023-12-13 at 01.12.45

Screenshot 2023-12-13 at 01.13.44

mysql-connector-j-8.2.0.jar 右键, 添加为库

  • 向 Jetbrians 申请一个学生/教师授权,用于激活 idea 终极版(进行 JavaWeb 开发需要用到,一般申请需要 3-7 天时间审核)不是大学生的话...emmm...懂的都懂。

  • 教育授权申请地址:https://www.jetbrains.com/shop/eform/students

一个 Java 程序并不是一个人的战斗,我们可以在别人开发的基础上继续向上开发,其他的开发者可以将自己编写的 Java 代码打包为jar,我们只需要导入这个jar作为依赖,即可直接使用别人的代码,就像我们直接去使用 JDK 提供的类一样。

Screenshot 2023-12-13 at 01.47.11

使用 JDBC 连接数据库

注意:6.0 版本以上,不用手动加载驱动,我们直接使用即可!

//1. 通过DriverManager来获得数据库连接
try (Connection connection = DriverManager.getConnection("连接URL","用户名","密码");
     //2. 创建一个用于执行SQL的Statement对象
     Statement statement = connection.createStatement()){   //注意前两步都放在try()中,因为在最后需要释放资源!
    //3. 执行SQL语句,并得到结果集
    ResultSet set = statement.executeQuery("select * from 表名");
    //4. 查看结果
    while (set.next()){
        ...
    }
}catch (SQLException e){
    e.printStackTrace();
}
//5. 释放资源,try-with-resource语法会自动帮助我们close

其中,连接的 URL 如果记不住格式,我们可以打开 idea 的数据库连接配置,复制一份即可。(其实 idea 本质也是使用的 JDBC,整个 idea 程序都是由 Java 编写的,实际上 idea 就是一个 Java 程序)

Screenshot 2023-12-13 at 01.56.10

Screenshot 2024-02-21 at 16.42.53

了解 DriverManager

我们首先来了解一下 DriverManager 是什么东西,它其实就是管理我们的数据库驱动的:

public static synchronized void registerDriver(java.sql.Driver driver,
        DriverAction da)
    throws SQLException {

    /* Register the driver if it has not already been added to our list */
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));    //在刚启动时,mysql实现的驱动会被加载,我们可以断点调试一下。
    } else {
        // This is for compatibility with the original DriverManager
        throw new NullPointerException();
    }

    println("registerDriver: " + driver);

}

Command + 选中 DriverManager -> DriverManager Class

Screenshot 2024-02-21 at 20.05.23

Screenshot 2024-02-21 at 20.17.18

在刚开始的时候已经自动注册了

com.mysql.cj.jdbc.Driver :

Screenshot 2024-02-21 at 20.21.29

在加载的时候, static 静态代码块就会被执行了

我们可以通过调用 getConnection()来进行数据库的链接:

@CallerSensitive
public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));   //内部有实现
}

Screenshot 2024-02-21 at 20.24.39

我们可以手动为驱动管理器添加一个日志打印:

static {
    DriverManager.setLogWriter(new PrintWriter(System.out));   //这里直接设定为控制台输出
}

现在我们执行的数据库操作日志会在控制台实时打印。

了解 Connection

Connection 是数据库的连接对象,可以通过连接对象来创建一个 Statement 用于执行 SQL 语句:

Statement createStatement() throws SQLException;

我们发现除了普通的 Statement,还存在 PreparedStatement:

PreparedStatement prepareStatement(String sql)
    throws SQLException;

在后面我们会详细介绍 PreparedStatement 的使用,它能够有效地预防 SQL 注入式攻击。

它还支持事务的处理,也放到后面来详细进行讲解。

Screenshot 2024-02-21 at 20.28.00

了解 Statement

我们发现,我们之前使用了executeQuery()方法来执行select语句,此方法返回给我们一个 ResultSet 对象,查询得到的数据,就存放在 ResultSet 中!

Statement 除了执行这样的 DQL 语句外,我们还可以使用executeUpdate()方法来执行一个 DML 或是 DDL 语句,它会返回一个 int 类型,表示执行后受影响的行数,可以通过它来判断 DML 语句是否执行成功。

也可以通过excute()来执行任意的 SQL 语句,它会返回一个boolean来表示执行结果是一个 ResultSet 还是一个 int,我们可以通过使用getResultSet()或是getUpdateCount()来获取。

执行 DML 操作

我们通过几个例子来向数据库中插入数据。

执行 DQL 操作

执行 DQL 操作会返回一个 ResultSet 对象,我们来看看如何从 ResultSet 中去获取数据:

//首先要明确,select返回的数据类似于一个excel表格
while (set.next()){
    //每调用一次next()就会向下移动一行,首次调用会移动到第一行
}

我们在移动行数后,就可以通过 set 中提供的方法,来获取每一列的数据。

img

Screenshot 2024-02-21 at 20.47.30

Screenshot 2024-02-21 at 20.48.37

Screenshot 2024-02-21 at 20.52.46

Screenshot 2024-02-21 at 20.55.43

Screenshot 2024-02-21 at 20.57.34

Screenshot 2024-02-21 at 21.21.44

执行批处理操作

当我们要执行很多条语句时,可以不用一次一次地提交,而是一口气全部交给数据库处理,这样会节省很多的时间。

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection();
         Statement statement = connection.createStatement()){

        statement.addBatch("insert into user values ('f', 1234)");
        statement.addBatch("insert into user values ('e', 1234)");   //添加每一条批处理语句
        statement.executeBatch();   //一起执行

    }catch (SQLException e){
        e.printStackTrace();
    }
}
Bug

会报错 Not fix yet Screenshot 2024-02-22 at 09.14.31

将查询结果映射为对象

既然我们现在可以从数据库中获取数据了,那么现在就可以将这些数据转换为一个类来进行操作,首先定义我们的实体类:

public class Student {
    Integer sid;
    String name;
    String sex;

    public Student(Integer sid, String name, String sex) {
        this.sid = sid;
        this.name = name;
        this.sex = sex;
    }

    public void say(){
        System.out.println("我叫:"+name+",学号为:"+sid+",我的性别是:"+sex);
    }
}

Screenshot 2024-02-23 at 03.24.23

现在我们来进行一个转换:

while (set.next()){
    Student student = new Student(set.getInt(1), set.getString(2), set.getString(3));
    student.say();
}

注意:列的下标是从1开始的。

Screenshot 2024-03-02 at 19.24.51

我们也可以利用反射机制来将查询结果映射为对象,使用反射的好处是,无论什么类型都可以通过我们的方法来进行实体类型映射:

private static <T> T convert(ResultSet set, Class<T> clazz){
    try {
        Constructor<T> constructor = clazz.getConstructor(clazz.getConstructors()[0].getParameterTypes());   //默认获取第一个构造方法
        Class<?>[] param = constructor.getParameterTypes();  //获取参数列表
        Object[] object = new Object[param.length];  //存放参数
        for (int i = 0; i < param.length; i++) {   //是从1开始的
            object[i] = set.getObject(i+1);
            if(object[i].getClass() != param[i])
                throw new SQLException("错误的类型转换:"+object[i].getClass()+" -> "+param[i]);
        }
        return constructor.newInstance(object);
    } catch (ReflectiveOperationException | SQLException e) {
        e.printStackTrace();
        return null;
    }
}

现在我们就可以通过我们的方法来将查询结果转换为一个对象了:

while (set.next()){
    Student student = convert(set, Student.class);
    if(student != null) student.say();
}

实际上,在后面我们会学习Mybatis框架,它对JDBC进行了深层次的封装,而它就进行类似上面反射的操作来便于我们对数据库数据与实体类的转换。

Screenshot 2024-03-02 at 19.34.17

实现登陆与SQL注入攻击

在使用之前,我们先来看看如果我们想模拟登陆一个用户,我们该怎么去写:

try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
     Statement statement = connection.createStatement();
     Scanner scanner = new Scanner(System.in)){
    ResultSet res = statement.executeQuery("select * from user where username='"+scanner.nextLine()+"'and pwd='"+scanner.nextLine()+"';");
    while (res.next()){
        String username = res.getString(1);
        System.out.println(username+" 登陆成功!");
    }
}catch (SQLException e){
    e.printStackTrace();
}

Screenshot 2024-03-02 at 19.48.39

Screenshot 2024-03-02 at 19.50.00

Screenshot 2024-03-02 at 19.52.21

用户可以通过自己输入用户名和密码来登陆,乍一看好像没啥问题,那如果我输入的是以下内容呢:

Test
1111' or 1=1; -- 
# Test 登陆成功!

1=1一定是true,那么我们原本的SQL语句会变为:

select * from user where username='Test' and pwd='1111' or 1=1; -- '

我们发现,如果允许这样的数据插入,那么我们原有的SQL语句结构就遭到了破坏,使得用户能够随意登陆别人的账号。因此我们可能需要限制用户的输入来防止用户输入一些SQL语句关键字,但是关键字非常多,这并不是解决问题的最好办法。

Screenshot 2024-03-02 at 19.54.30

Screenshot 2024-03-02 at 20.01.18

使用PreparedStatement

我们发现,如果单纯地使用Statement来执行SQL命令,会存在严重的SQL注入攻击漏洞!而这种问题,我们可以使用PreparedStatement来解决:

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
         PreparedStatement statement = connection.prepareStatement("select * from user where username= ? and pwd=?;");
         Scanner scanner = new Scanner(System.in)){

        statement.setString(1, scanner.nextLine());
        statement.setString(2, scanner.nextLine());
        System.out.println(statement);    //打印查看一下最终执行的
        ResultSet res = statement.executeQuery();
        while (res.next()){
            String username = res.getString(1);
            System.out.println(username+" 登陆成功!");
        }
    }catch (SQLException e){
        e.printStackTrace();
    }
}

我们发现,我们需要提前给到PreparedStatement一个SQL语句,并且使用?作为占位符,它会预编译一个SQL语句,通过直接将我们的内容进行替换的方式来填写数据。使用这种方式,我们之前的例子就失效了!我们来看看实际执行的SQL语句是什么:

com.mysql.cj.jdbc.ClientPreparedStatement: select * from user where username= 'Test' and pwd='123456'' or 1=1; -- ';

我们发现,我们输入的参数一旦出现'时,会被变为转义形式\',而最外层有一个真正的'来将我们输入的内容进行包裹,因此它能够有效地防止SQL注入攻击!

Screenshot 2024-03-02 at 20.06.45

管理事务

JDBC默认的事务处理行为是自动提交,所以前面我们执行一个SQL语句就会被直接提交(相当于没有启动事务),所以JDBC需要进行事务管理时,首先要通过Connection对象调用setAutoCommit(false) 方法, 将SQL语句的提交(commit)由驱动程序转交给应用程序负责。

con.setAutoCommit();   //关闭自动提交后相当于开启事务。
// SQL语句
// SQL语句
// SQL语句
con.commit(); con.rollback();

一旦关闭自动提交,那么现在执行所有的操作如果在最后不进行commit()来提交事务的话,那么所有的操作都会丢失,只有提交之后,所有的操作才会被保存!也可以使用rollback()来手动回滚之前的全部操作!

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
         Statement statement = connection.createStatement()){

        connection.setAutoCommit(false);  //关闭自动提交,现在将变为我们手动提交
        statement.executeUpdate("insert into user values ('a', 1234)");
        statement.executeUpdate("insert into user values ('b', 1234)");
        statement.executeUpdate("insert into user values ('c', 1234)");

        connection.commit();   //如果前面任何操作出现异常,将不会执行commit(),之前的操作也就不会生效
    }catch (SQLException e){
        e.printStackTrace();
    }
}

Screenshot 2024-03-02 at 20.21.27

Screenshot 2024-03-02 at 20.21.52

我们来接着尝试一下使用回滚操作:

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
         Statement statement = connection.createStatement()){

        connection.setAutoCommit(false);  //关闭自动提交,现在将变为我们手动提交
        statement.executeUpdate("insert into user values ('a', 1234)");
        statement.executeUpdate("insert into user values ('b', 1234)");

        connection.rollback();   //回滚,撤销前面全部操作

        statement.executeUpdate("insert into user values ('c', 1234)");

        connection.commit();   //提交事务(注意,回滚之前的内容都没了)

    }catch (SQLException e){
        e.printStackTrace();
    }
}

Screenshot 2024-03-02 at 22.37.59

Screenshot 2024-03-02 at 22.38.18

同样的,我们也可以去创建一个回滚点来实现定点回滚:

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
         Statement statement = connection.createStatement()){

        connection.setAutoCommit(false);  //关闭自动提交,现在将变为我们手动提交
        statement.executeUpdate("insert into user values ('a', 1234)");

        Savepoint savepoint = connection.setSavepoint();   //创建回滚点
        statement.executeUpdate("insert into user values ('b', 1234)");

        connection.rollback(savepoint);   //回滚到回滚点,撤销前面全部操作

        statement.executeUpdate("insert into user values ('c', 1234)");

        connection.commit();   //提交事务(注意,回滚之前的内容都没了)

    }catch (SQLException e){
        e.printStackTrace();
    }
}

Screenshot 2024-03-02 at 22.41.16

通过开启事务,我们就可以更加谨慎地进行一些操作了,如果我们想从事务模式切换为原有的自动提交模式,我们可以直接将其设置回去:

public static void main(String[] args) throws ClassNotFoundException {
    try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
         Statement statement = connection.createStatement()){

        connection.setAutoCommit(false);  //关闭自动提交,现在将变为我们手动提交
        statement.executeUpdate("insert into user values ('a', 1234)");
        connection.setAutoCommit(true);   //重新开启自动提交,开启时把之前的事务模式下的内容给提交了
        statement.executeUpdate("insert into user values ('d', 1234)");
        //没有commit也成功了!
    }catch (SQLException e){
        e.printStackTrace();
    }