JDBC 简介

JDBC (Java DataBase Connectivity, Java 数据库连接) 是使用Java语言操作关系型数据库的一套 API.

JDBC其实是SUN公司制订的一套操作数据库的标准接口. JDBC中定义了所有操作关系型数据库的规则. 由各自的数据库厂商给出实现类 (驱动jar包).

Java, JDBC和各种数据库的关系如下图:

使用JDBC的好处:

  • 不需要针对不同数据库分别开发.
  • 可随时替换底层数据库, 访问数据库的Java代码基本不变.

JDBC 使用的基本步骤

  1. 导入JDBC驱动jar包:

    • 下载MySQL jar驱动包, 菜鸟教程 Java MySQL 连接

    • 在项目中, 将下载好的jar包放入项目的 lib目录中.

    • 然后点击鼠标右键–>Add as Library (添加为库).

    • 在添加为库文件的时候,有如下三个选项:

      • Global Library: 全局有效

      • Project Library: 项目有效

      • Module Library: 模块有效

        选择Global Library.

  2. 注册驱动:

    1Class.forName("com.mysql.jdbc.Driver");
    

    MySQL提供的 Driver的静态代码块会自动执行 DriverManager.registerDriver() 方法来注册驱动. 所以我们只需加载 Driver即可. MySQL5之后的驱动包, 可以省略注册驱动的步骤.

  3. 获取数据库连接:

    1Connection conn = DriverManager.getConnection(url, username, password);
    
    • 其中, url, usernamepassword都是 String类型.

    • url格式:

      1jdbc:数据库软件名称://ip地址或域名:端口/数据库名称?参数键值对1&参数键值对2...
      

      例如, 连接本地mysql中名为test的数据库:

      1jdbc:mysql://127.0.0.1:3306/test
      

      本地mysql, 且端口为3306, url可简写为:

      1jdbc:mysql:///数据库名称?参数键值对
      

      常用的参数键值对有:

      1useSSL=false            // 禁用安全连接方式, 解决警告提示
      2useServerPrepStmts=true // 开启预编译(默认为false)
      3serverTimezone=GMT%2B8  // 设置时区, 东八区(即GMT+8)
      4serverTimezone=Asia/Shanghai    // 设置时区东八区
      5useUnicode=true&characterEncoding=UTF-8 // 设置字符集为UTF-8
      
  4. 获取执行SQL对象:

    执行SQL语句需要SQL执行对象 (Statement对象):

    1Statement stmt = conn.createStatement();
    

    Statement对象存在安全问题 (SQL注入等问题), 而使用 PreparedStatement不仅可以提升查询速度, 而且还能防止SQL注入问题.

    1String sql = "...SQL语句...";
    2PreparedStatement pstmt = conn.prepareStatement(sql);
    
  5. 执行SQL语句:

    1int count = pstmt.executeUpdate(sql);
    

    用于执行DML, DDL语句.

    或者:

    1ResultSet rs = pstmt.executeQuery(sql);
    

    用于执行DQL语句.

  6. 处理返回结果

  7. 释放资源:

    ResultSetStatementConnection对象都要 <i>按照顺序 </i>释放资源.

    1rs.close();
    2stmt.close();
    3conn.close();
    

大致代码如下:

 1import java.sql.*;
 2
 3public class JDBCDemo {
 4  
 5    public static void main(String[] args) throws Exception {
 6
 7        // - 接收用户输入的用户名和密码
 8        String name = "...";
 9        String pwd = "...";
10    
11        // 1. 注册驱动(装载类,并实例化)
12        Class.forName("com.mysql.jdbc.Driver");
13    
14        // 2. 获取连接
15        String url = "jdbc:mysql://127.0.0.1:3306/test" + 
16                "?useServerPrepStmts=true";
17        String username = "root";
18        String password = "1234";
19        Connection conn = DriverManager.getConnection(url, username, password);
20    
21        // 3. 定义SQL语句 (用?作占位符)
22        String sql = "SELECT id,username,password" +
23                " FROM tb_user" +
24                " WHERE username = ?" +
25                " AND password = ?";
26    
27        // 4. 获取执行SQL的PreparedStatement对象
28        PreparedStatement pstmt = conn.prepareStatement(sql);
29        // 设置参数(?)的值 pstmt.setXxx(index, value)
30        pstmt.setString(1, name);
31        pstmt.setString(2, pwd);
32    
33        // 5. 执行SQL
34        ResultSet rs = pstmt.executeQuery();
35    
36        // 6. 处理结果
37        while (rs.next) {
38            /*
39                ...
40            */
41        }
42
43        // 7. 释放资源
44        rs.close();
45        pstmt.close();
46        conn.close();
47    }
48}

PreparedStatement 对象

PreparedStatement 对象可以:

  • 预编译SQL语句并执行
  • 预防SQL注入问题

获取 PreparedStatement需要先传入SQL语句:

1// SQL语句中的参数值,使用 ? 占位符替代
2String sql = "SELECT id,username,password" +
3        " FROM tb_user" +
4        " WHERE username = ?" +
5        " AND password = ?";
6
7// 通过Connection对象获取PreparedStatement, 并传入对应的SQL语句
8PreparedStatement pstmt = conn.prepareStatement(sql);

接着我们需要设置SQL对象中的参数值:

使用 pstmt.setXxx(index, value), 给 ? 赋值. 其中, index的值从 1开始, value的类型为 Xxxxxx.

例如, 给 int类型的 value赋值使用 setInt(), String类型使用 setString(). 除此之外还有 setFloat(), setDouble(), setArray(), setByte()等.

如果 prepareStatement()方法传入的是DML, DDL语句, 则使用 executeUpdate() 方法:

1int executeUpdate() 
2            throws SQLException

如果该方法执行的是DML语句 (INSERT, UPDATEDELETE), 则返回DML语句操作的行数; 如果是DDL语句则返回 0.

需要注意, 在开发中很少使用java代码操作DDL语句.

如果 prepareStatement()方法传入的是DQL语句 (SELECT), 使用的是 executeQuery() 方法:

1ResultSet executeQuery()
2                throws SQLException

该方法返回的是DQL语句查询后的结果集.

在使用 PreparedStatement对象后, 需要使用 close()方法释放资源.

Statement 和 PreparedStatement

Statement 对象的一般用法如下:

1String sql = "UPDATE tb_user SET password = \"abc\" WHERE id = 1";
2Statement stmt = conn.createStatement();
3int count = stmt.executeUpdate(sql);

Statement的SQL语句是作为 executeUpdate()executeQuery()的参数传入, 而 PreparedStatement则是在创建对象就已经作为 prepareStatement()方法的参数传入.

这是因为 PreparedStatement需要预先传入SQL语句, 来起到预编译SQL语句和预防SQL注入问题.

预编译

一般情况下, java执行SQL语句的过程如下:

java程序请求数据库执行SQL语句后:

  • 检查: 数据库接收指令, 检查SQL语法
  • 编译: 如果SQL语句无语法错误, 则将该语句编译成可执行的函数
  • 执行: 编译完成后执行SQL语句

而检查SQL和编译SQL花费的时间比执行SQL的时间还要长, 如果需要一次性执行多条SQL语句, 那会浪费大量时间和资源. 所以, PreparedStatement的出现解决了这个问题.

通过使用 PreparedStatement对象, 并且在连接数据库的 url中添加 useServerPrepStmts=true参数来开启SQL语句预编译功能. 预编译功能会将我们设置的SQL语句 (如 "SELECT id,username,password FROM tb_user WHERE username = ? AND password = ?") 预先传给数据库, 让其先完成检查和编译的工作 (先完成耗时的工作), 然后再一次性执行所有SQL语句 (这些SQL语句都是相同的, 只是占位符处设置的值不同).

SQL注入

SQL注入是指通过把SQL命令插入到Web表单提交, 或输入域名或页面请求的查询字符串, 最终达到欺骗服务器执行恶意的SQL命令.

PreparedStatement通过在SQL语句中使用 ?占位符, 并且使用相应的 setXxx()方法来设置值 (设置的值如果含有特殊字符, 如 "' 等, 则会进行转义), 防止了SQL注入的发生.

下面代码说明了 PreparedStatement如何防止SQL注入:

 1class Demo {
 2    public static void main(String[] args) {
 3        // useServerPrepStmts=true开启预编译
 4        String url = "jdbc:mysql:///test?useSSL=false&useServerPrepStmts=true";
 5        String username = "root";
 6        String password = "n546,Lin0";
 7        Connection conn = DriverManager.getConnection(url, username, password);
 8
 9        // - 接收用户输入的用户名和密码
10        String name = "zhangsan";
11        String pwd = "' OR '1' = '1";
12
13        // - 定义SQL(用?作占位符)
14        String sql = "SELECT id,username,password" +
15                " FROM tb_user" +
16                " WHERE username = ?" +
17                " AND password = ?";
18
19        // - 获取PreparedStatement对象
20        //   - 预编译SQL,性能更高
21        //     默认关闭,在url加上参数useServerPrepStmts=true开启
22        //   - 防止SQL注入
23        PreparedStatement pstmt = conn.prepareStatement(sql);
24
25        // - 设置参数(?)的值
26        //   - 防注入原理:
27        //     字符串参数在setString中会被转义,
28        //     即整个参数被当成sql里面的字符串,而不是java的字符串
29        pstmt.setString(1, name);
30        // 从mysql日志文件可以发现:
31        // ' OR '1' = '1 转义成了 \' OR \'1\' = \'1
32        pstmt.setString(2, pwd);
33
34        // - 执行SQL
35        ResultSet rs = pstmt.executeQuery();
36
37        // - 判读登录是否成功
38        if (rs.next()) {
39            System.out.println("登录成功!");
40        }
41        else {
42            System.out.println("登陆失败!");
43        }
44
45        rs.close();
46        pstmt.close();
47        conn.close();
48    }
49}

下面代码演示了把SQL代码片段插入到SQL命令, 来进行免密登录:

 1class LoginInject {
 2    public static void main(String[] args) throws Exception {
 3        String url = "jdbc:mysql:///test";
 4        String username = "root";
 5        String password = "1234";
 6        Connection conn = DriverManager.getConnection(url, username, password);
 7
 8        // 接收用户输入的用户名和密码
 9        String name = "abcdefg"; // 用户名随意
10        String pwd = "' OR '1' = '1"; // 密码传入SQL代码片段
11
12        String sql = "SELECT id,username,password" +
13                " FROM tb_user" +
14                " WHERE username = '" + name +
15                "' AND password = '"+ pwd + "'";
16        // 将sql语句where部分展开:
17        // WHERE username = 'abcdefg' AND password = '' OR '1' = '1'
18        // 发现where语句条件始终为真
19        System.out.println(sql);
20
21        Statement stmt = conn.createStatement();
22        ResultSet rs = stmt.executeQuery(sql);
23
24        // 判读登录是否成功
25        if (rs.next()) {
26            System.out.println("登录成功!");
27        }
28        else {
29            System.out.println("登陆失败!");
30        }
31        // 返回的是登录成功
32
33        rs.close();
34        stmt.close();
35        conn.close();
36    }
37}

ResultSet 对象

ResultSet (结果集对象) 作用: 封装了SQL查询语句的结果, 是 executeQuery()方法的返回值类型.

ResultSet对象有三个方法:

  • next():

    1boolean next()
    2        throws SQLException
    

    每次执行时, 将光标从当前位置向前移动一行 (光标从第0行开始), 并且判断当前行是否为有效行 (返回 true则代表为有效行)。

  • getXxx():

    1xxx getXxx(arg)
    2        throws SQLException
    

    arg类型:

    • int: 代表列的编号 (按照 SELECT语句中的查询顺序), 从1开始
    • String: 列的名称
  • close():

    1void close()
    2        throws SQLException
    

    释放 ResultSet对象.

下面演示了 ResultSet的使用:

 1class Demo {
 2    public static void main(String[] args) {
 3        // ...
 4
 5        String sql = "SELECT id,username,password FROM tb_user";
 6        Statement stmt = conn.createStatement();
 7        PreparedStatement pstmt = conn.prepareStatement(sql);
 8        // - 处理结果,遍历rs中的所有数据
 9        //   - rs.next():光标向下移动一行,并判断当前行是否有效
10        while (rs.next()) {
11            // - 获取数据 getXxx()
12            int id = rs.getInt(1);
13            // getXxx()方法可以使用列索引(从1开始)也可以使用列名
14            String usrname = rs.getString("username");
15            String passwd = rs.getString(3);
16
17            System.out.println("id: " + id);
18            System.out.println("username: " + usrname);
19            System.out.println("passwd: " + passwd);
20            System.out.println("-----------------------");
21        }
22        // - 释放资源
23        // ResultSet、Statement和Connection都要按照顺序释放资源
24        // 先释放ResultSet, 再释放Statement, 最后是Connection
25        rs.close();
26        stmt.close();
27        conn.close();
28    }
29}

操作实例

用户账号密码增删改操作.

在编写JDBC代码之前需要先完成创建数据库, 创建 pojo包并编写 User实体类的工作:

  1. 创建数据库并运行下方SQL代码:

     1-- 删除tb_user表
     2DROP TABLE IF EXISTS tb_user;
     3-- 创建tb_user表
     4CREATE TABLE tb_user(
     5    id INT PRIMARY KEY AUTO_INCREMENT,
     6    username VARCHAR(20),
     7    password VARCHAR(32)
     8);
     9
    10-- 添加数据
    11INSERT INTO tb_user VALUES(NULL, 'zhangsan', '123'), (NULL, 'lisi', '234');
    
  2. 创建 pojo包, 并在包中添加 User实体类:

     1package pojo; // pojo包存放实体类
     2
     3public class User {
     4
     5    private Integer id;
     6    private String username;
     7    private String password;
     8
     9    public Integer getId() {
    10        return id;
    11    }
    12
    13    public void setId(Integer id) {
    14        this.id = id;
    15    }
    16
    17    public String getUsername() {
    18        return username;
    19    }
    20
    21    public void setUsername(String username) {
    22        this.username = username;
    23    }
    24
    25    public String getPassword() {
    26        return password;
    27    }
    28
    29    public void setPassword(String password) {
    30        this.password = password;
    31    }
    32
    33    @Override
    34    public String toString() {
    35        return "Account{" +
    36                "id=" + id +
    37                ", username='" + username + '\'' +
    38                ", password='" + password + '\'' +
    39                '}';
    40    }
    41}
    

增删改操作

JDBC数据访问层的代码放在 DAO包下:

  1package dao;
  2
  3import pojo.User;
  4
  5import java.sql.*;
  6
  7public class UserDAO {
  8
  9    private static String URL = "jdbc:mysql:///test" +
 10                "?useSSL=false&useServerPrepStmts=true";
 11    private static String USERNAME = "root";
 12    private static String PASSWORD = "1234";
 13
 14    /**
 15     * 根据用户名和密码查询
 16     * @param username
 17     * @param password
 18     * @return User
 19     * @throws SQLException
 20     */
 21    public User select(String username, String password) throws SQLException {
 22
 23        // 参数有null值时
 24        if (username == null || password == null) {
 25            return null;
 26        }
 27
 28        // 连接数据库
 29        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
 30
 31        // 获取PreparedStatement对象, 并设置SQL语句
 32        String sql = "SELECT id, username, password" +
 33                " FROM tb_user" +
 34                " WHERE username = ?" +
 35                " AND password = ?";
 36        PreparedStatement pstmt = conn.prepareStatement(sql);
 37        pstmt.setString(1, username);
 38        pstmt.setString(2, password);
 39
 40        // 获取ResultSet
 41        ResultSet rs = pstmt.executeQuery();
 42
 43        User user = null;
 44        if (rs.next()) {
 45            user = new User();
 46
 47            Integer id = rs.getInt("id");
 48            String name = rs.getString("username");
 49            String pw = rs.getString("password");
 50
 51            user.setId(id);
 52            user.setUsername(name);
 53            user.setPassword(pw);
 54        }
 55
 56        rs.close();
 57        pstmt.close();
 58        conn.close();
 59
 60        return user;
 61    }
 62
 63    /**
 64     * 根据用户名和密码添加数据
 65     * @param username
 66     * @param password
 67     * @return boolean
 68     * @throws SQLException
 69     */
 70    public boolean add(String username, String password) throws SQLException {
 71
 72        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
 73
 74        String sql = "INSERT INTO tb_user" +
 75                " VALUE(null, ?, ?)";
 76        PreparedStatement pstmt = conn.prepareStatement(sql);
 77        pstmt.setString(1, username);
 78        pstmt.setString(2, password);
 79
 80        int count = pstmt.executeUpdate();
 81
 82        pstmt.close();
 83        conn.close();
 84
 85        return count > 0;
 86    }
 87
 88    /**
 89     * 根据用户名和密码删除数据
 90     * @param username
 91     * @param password
 92     * @return boolean
 93     * @throws SQLException
 94     */
 95    public boolean delete(String username, String password) throws SQLException {
 96
 97        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
 98
 99        String sql = "DELETE FROM tb_user" +
100                " WHERE username = ?" +
101                " AND password = ?";
102        PreparedStatement pstmt = conn.prepareStatement(sql);
103        pstmt.setString(1, username);
104        pstmt.setString(2, password);
105
106        int count = pstmt.executeUpdate();
107
108        pstmt.close();
109        conn.close();
110
111        return count > 0;
112    }
113}