My Blog

JDBC

记录 JDBC 相关知识

学习笔记,仅供参考

参考B站Mirco_Frank - java 进阶 |


JDBC 指 Java database connectivity,它是一个 Java api 即允许用户通过 java 来访问操作多种关系型数据库的一套 规范

✋ 上手体验

Java 中对应的 api 为 java.sql 相当于布料,而各大数据库就相当于服装厂,生产各种服装。为了能让布料运到各大服装厂,中间又有了 mysql-connector-java (本文数据库以 MySQL 为例),相当于物流运输。

public class Test1 {
    public static void main(String[] args) {
        try {
            // 1. 加载驱动程序 --> 订布料,MySQL 公司的
            Class.forName("com.mysql.jdbc.Driver");

            // 2. 获取数据库连接 --> 送货,走啥路线
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/frank_school", 
            "root", "123456");  // 对应参数为 URL, USER, PASSWORD

            // 3. 获取数据库操作对象 --> 布料送到了,卸货
            Statement statement = connection.createStatement();

            // 4. 执行 sql 语句,操作数据(以查询为例)  --> 服装厂开始生产各种服装
            // 执行查询语句,并用结果集接收查询结果
            ResultSet res = statement.executeQuery("select * from user");  

            // 打印结果集
            while (res.next()) {  // 分别获取每行结果的第一、第二、第三列数据
                int id = res.getInt(1);
                String name = res.getString(2);
                int age = res.getInt(3);

                System.out.println("Id: " + id +
                    "Name: " + name + 
                    "Age: " + age);
                }
            // close stream
            statement.close();
            connection.close();

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

        statement.close();
    }
}

通过上面程序,大致就能够明白 JDBC 操作数据的过程。

  1. 先通过类加载器来加载 Driver;

  2. 再通过 DriverManager 来获取 connection;

  3. 接着由 connection 来获取数据库的操作对象 statement

  4. 最后再调用 statement 的 statementExecuteQuery("sql") 完成查询、statementExecuteUpdate("sql") 完成增删改

🎨 整理优化

为了让 JDBC 的程序更加合理,还需对上述代码进行整理修改。如:

  1. 将常量(URL、USER、PASSWORD、DRIVER)和接收变量(connection、statement、res)提前声明定义好

  2. 将整个过程都用 try-catch-finally 包裹起来

  3. 在 finally 中再次用 try-catch 将变量的关流包起来

下面为整理后的程序

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;

public class Test2 {
    // 定义常量
    private static final String DRIVER = "com.mysql.jdbc.Driver"; 
    private static final String URL = "jdbc:mysql://localhost:3306/frank_school"; 
    private static final String USER = "root"; 
    private static final String PASSWORD = "123456"; 

    // 定义变量
    private static Connection connection;
    private static Statement statement;
    private static ResultSet res;

    public static void main(String[] args) {
        try {
            // 1. 加载驱动程序
            Class.forName("DRIVER");

            // 2. 获取数据库连接
            connection = DriverManager.getConnection(URL, USER, PASSWORD);

            // 3. 获取数据库操作对象
            statement = connection.createStatement();

            // 4. 执行 sql 语句,获取结果
            res = statement.executeQuery("select * from user");

            // 打印结果集
            while (res.next()) {
                int id = res.getInt(1);
                String name = res.getString(2);
                int age = res.getInt(3);

                System.out.println("Id: " + id +
                    "Name: " + name + 
                    "Age: " + age);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                res.close();
                statement.close();
                connection.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

🚩 规范操作

为了让程序编写的更加规范,还可以将 JDBC 的前置工作,如定义 jdbc 所用到的常量、获取驱动程序等封装为一个 JDBCUtils 工具类,这样能够减少代码量。而且为了让数据读取更加方便、安全,还需要将常量放在 db.properties 的配置文件中,其中数据是以键值对的形式存储(如:user=root)

★ db.properties 文件内容

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/frank_school?characterEncoding=utf-8
user=root
password=123456
// JDBCUtil 工具类的内容

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.io.InputStream;
import java.io.IOException;

import java.util.Properties;

public class JDBCUtil {
    // 定义接收配置项的变量
    private static String driver;
    private static String url;
    private static String user;
    private static String password;

    // 通过静态代码块预先读取配置文件中的配置项
    static {
        // 导入要读取的文件流
        InputStream inputStream = ClassLoader.getSystemResourceAsStream("db.properties");
        Properties properties = new Properties();
        // 加载文件流
        properties.load(inputStream);
        // 获取配置项
        driver = properties.getProperty("driver");
        url = properties.getProperty("url");
        user = properties.getProperty("user");
        password = properties.getProperty("password");
    }

    /**
     *  单例获取数据库连接
     */
    public static Connection getConnection() throw SQLException {
        return DriverManager.getConnection(url, user, password);
    }

    /**
     *  给出 close() 的方法,关闭连接,并且重载来对应不同的数据库操作
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        try {
            resultSet.close();
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

    public static void close(Connection connection, Statement statement) {
        try {
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}
// JDBC 操作数据库

import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;

public class JDBCTest {
    private static Connection connection;
    private static Statement statement;
    private static ResultSet res;

    public static void main(String[] args) {
        try {
            // 获取数据库连接
            connection = JDBCUtil.getConnection();
            // 获取数据库操作对象
            statement = connection.createStatement();
            // 执行 sql 语句
            res = statement.ExecuteQuery("select * from user");

            // 打印结果集
            while (res.next()) {
                int id = res.getInt(1);
                String name = res.getString(2);
                int age = res.getInt(3);

                System.out.println("Id: " + id +
                    "Name: " + name + 
                    "Age: " + age);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                JDBCUtil.close(connection, statement, res);
            } catch (Exception e) {
                e.printStackTrace(); 
            }
        }
        
    }
}

下面对 .properties 文件及 Java 中的相关方法做简要说明,它主要用于 Java 相关的技术中,为了能存储应用的配置参数。其中每个参数都以字符串对的形式存储,一端是参数名(Key),另一端是参数值(Value)。参数的书写最好一行一个,且文件的编码在 Java 9 之后采用 UTF-8 的编码格式。

另外,像 XML、JSON、YAML 等配置文件更受欢迎,它们能够完成更复杂的配置

格式
key=value
key = value
key:value
key value

# 井号开头表注释
! 或叹号开头

从上面代码中可了解到,Properties 位于 java.util.Properties 包下,它能够将配置项加载到流中或者从流中读取配置项。因为它继承自 HashTable,所以能够调用 put 和 putAll 方法来添加数据,但并不鼓励这样做,而是用 setProperty 方法来代替,这能保证当配置项为非字符串时添加失败。

当然它还能像 HashMap 那样,通过 entrySet(), keySet(), values() 来返回一个迭代器,从而遍历配置项。

💉 SQL 注入

一般用户在前端页面提交表单时,程序总要获取表单的信息,然后通过 JDBC 执行对应的 sql 语句来操作数据库。比如说,通过用户名来查看用户信息

// 前端表单
Username: Tom

// 获取表单信息
username = getRequestString("username");

// 编写 sql 语句
sql = "select * from user where username =\"" + 
        username + "\"";

// 对应的 sql 语句
// SELECT * FROM user WHERE username="Tom"; 

这样处理看似很正常,但却有着潜在的危险。在这种情况下,黑客会钻 sql 语句的空子,从而绕过检查获取用户信息。

// 1. 利用 "1=1" 保证恒为真
// 前端表单
UserId: 1 OR 1=1

// 获取表单信息
id = getRequestString("UserId");

// 编写 sql 语句
sql = "select * from user where id =" + 
        id + "";

// 对应的 sql 语句
// SELECT * FROM user WHERE id=1 OR 1=1;
// 该 sql 相当于直接执行了 SELECT * FROM user;
// 直接获取到所有用户的信息

/**********************分割线************************/

// 2. 利用 "="
// 前端表单
Username: "or""="
Password: "or""="

// 获取表单信息
username = getRequestString("Username");
password = getRequestString("Password");

// 编写 sql 语句
sql = "select * from user where username =\"" + 
        username + "\" and password=\"" + 
        password + "\"";

// 对应的 sql 语句
// SELECT * FROM user WHERE username=""or""
// and password=""="";
// 也相当执行 SELECT * FROM user;

对于这种直接将用户的输入作为 sql 语句的参数,将会引起 SQL 注入从而数据库数据泄露或毁坏。于是 JDBC 中就有了 PreparedStatement 类,以此来防止 SQL 注入。

🌂 PreparedStatement

PreparedStatement 属于 java.sql.PreparedStatement包,它的对象表示一个预先编译的 SQL 语句,SQL 语句会预先编译好然后再存放在该对象中,该对象能够被用来多次高效地执行 SQL 语句。PreparedStatement 的用法与 Statement 会有些许不同,具体看下面的例子

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import java.util.Scanner;

public class JDBCTest {
    private static Connection con;
    private static PreparedStatement preparedStatement;
    private static ResultSet res;
    private static Scanner scanner = new Scanner();

    public static void main(String[] args) {
        try {
            // 与用户交互,获取参数
            System.out.println("请输入用户 ID 来查询用户信息:");
            int userId = scanner.nextInt();

            // 编写 SQL 语句, 使用问号占位符暂时代替具体参数
            String sql = "select * from user where id=?";

            // 获取数据库连接,并获取preparedStatement对象
            con = JDBCUtil.getConnection();
            preparedStatement = con.prepareStatement(sql);

            // 设置参数, 其中 1 指第一个占位符,不同的参数类型要使用
            // 不同的方法。如整数用 setInt,字符串用 setString 等
            preparedStatement.setInt(1, userId);

            // 执行 sql 语句, 并获取结果集
            res = preparedStatement.executeQuery();

            // 打印结果集
            while (res.next()) {
                int id = res.getInt(1);
                String name = res.getString(2);
                int age = res.getInt(3);

                System.out.println("Id: " + id +
                    "Name: " + name + 
                    "Age: " + age);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            scanner.close();
            JDBCUtil.close(con, preparedStatement, res);
        }
    }
}

从上面代码可看出,在获取 preparedStatement 时就要传入 sql 语句,且使用的是 prepareStatement(sql) 方法,然后再通过 setter 来设置对应占位符的参数,最后再执行 sql 语句。与 Statement 相同的是,执行 sql 语句所用的方法名是一样的。