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. 注册驱动:

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

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

  3. 获取数据库连接:

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

    • url格式:

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

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

      jdbc:mysql://127.0.0.1:3306/test
      

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

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

      常用的参数键值对有:

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

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

    Statement stmt = conn.createStatement();
    

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

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

    int count = pstmt.executeUpdate(sql);
    

    用于执行DML, DDL语句.

    或者:

    ResultSet rs = pstmt.executeQuery(sql);
    

    用于执行DQL语句.

  6. 处理返回结果

  7. 释放资源:

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

    rs.close();
    stmt.close();
    conn.close();
    

大致代码如下:

import java.sql.*;

public class JDBCDemo {
  
    public static void main(String[] args) throws Exception {

        // - 接收用户输入的用户名和密码
        String name = "...";
        String pwd = "...";
    
        // 1. 注册驱动(装载类,并实例化)
        Class.forName("com.mysql.jdbc.Driver");
    
        // 2. 获取连接
        String url = "jdbc:mysql://127.0.0.1:3306/test" + 
                "?useServerPrepStmts=true";
        String username = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, username, password);
    
        // 3. 定义SQL语句 (用?作占位符)
        String sql = "SELECT id,username,password" +
                " FROM tb_user" +
                " WHERE username = ?" +
                " AND password = ?";
    
        // 4. 获取执行SQL的PreparedStatement对象
        PreparedStatement pstmt = conn.prepareStatement(sql);
        // 设置参数(?)的值 pstmt.setXxx(index, value)
        pstmt.setString(1, name);
        pstmt.setString(2, pwd);
    
        // 5. 执行SQL
        ResultSet rs = pstmt.executeQuery();
    
        // 6. 处理结果
        while (rs.next) {
            /*
                ...
            */
        }

        // 7. 释放资源
        rs.close();
        pstmt.close();
        conn.close();
    }
}

PreparedStatement 对象

PreparedStatement 对象可以:

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

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

// SQL语句中的参数值,使用 ? 占位符替代
String sql = "SELECT id,username,password" +
        " FROM tb_user" +
        " WHERE username = ?" +
        " AND password = ?";

// 通过Connection对象获取PreparedStatement, 并传入对应的SQL语句
PreparedStatement 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() 方法:

int executeUpdate() 
            throws SQLException

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

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

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

ResultSet executeQuery()
                throws SQLException

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

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

Statement 和 PreparedStatement

Statement 对象的一般用法如下:

String sql = "UPDATE tb_user SET password = \"abc\" WHERE id = 1";
Statement stmt = conn.createStatement();
int 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注入:

class Demo {
    public static void main(String[] args) {
        // useServerPrepStmts=true开启预编译
        String url = "jdbc:mysql:///test?useSSL=false&useServerPrepStmts=true";
        String username = "root";
        String password = "n546,Lin0";
        Connection conn = DriverManager.getConnection(url, username, password);

        // - 接收用户输入的用户名和密码
        String name = "zhangsan";
        String pwd = "' OR '1' = '1";

        // - 定义SQL(用?作占位符)
        String sql = "SELECT id,username,password" +
                " FROM tb_user" +
                " WHERE username = ?" +
                " AND password = ?";

        // - 获取PreparedStatement对象
        //   - 预编译SQL,性能更高
        //     默认关闭,在url加上参数useServerPrepStmts=true开启
        //   - 防止SQL注入
        PreparedStatement pstmt = conn.prepareStatement(sql);

        // - 设置参数(?)的值
        //   - 防注入原理:
        //     字符串参数在setString中会被转义,
        //     即整个参数被当成sql里面的字符串,而不是java的字符串
        pstmt.setString(1, name);
        // 从mysql日志文件可以发现:
        // ' OR '1' = '1 转义成了 \' OR \'1\' = \'1
        pstmt.setString(2, pwd);

        // - 执行SQL
        ResultSet rs = pstmt.executeQuery();

        // - 判读登录是否成功
        if (rs.next()) {
            System.out.println("登录成功!");
        }
        else {
            System.out.println("登陆失败!");
        }

        rs.close();
        pstmt.close();
        conn.close();
    }
}

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

class LoginInject {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:mysql:///test";
        String username = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, username, password);

        // 接收用户输入的用户名和密码
        String name = "abcdefg"; // 用户名随意
        String pwd = "' OR '1' = '1"; // 密码传入SQL代码片段

        String sql = "SELECT id,username,password" +
                " FROM tb_user" +
                " WHERE username = '" + name +
                "' AND password = '"+ pwd + "'";
        // 将sql语句where部分展开:
        // WHERE username = 'abcdefg' AND password = '' OR '1' = '1'
        // 发现where语句条件始终为真
        System.out.println(sql);

        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);

        // 判读登录是否成功
        if (rs.next()) {
            System.out.println("登录成功!");
        }
        else {
            System.out.println("登陆失败!");
        }
        // 返回的是登录成功

        rs.close();
        stmt.close();
        conn.close();
    }
}

ResultSet 对象

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

ResultSet对象有三个方法:

  • next():

    boolean next()
            throws SQLException
    

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

  • getXxx():

    xxx getXxx(arg)
            throws SQLException
    

    arg类型:

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

    void close()
            throws SQLException
    

    释放 ResultSet对象.

下面演示了 ResultSet的使用:

class Demo {
    public static void main(String[] args) {
        // ...

        String sql = "SELECT id,username,password FROM tb_user";
        Statement stmt = conn.createStatement();
        PreparedStatement pstmt = conn.prepareStatement(sql);
        // - 处理结果,遍历rs中的所有数据
        //   - rs.next():光标向下移动一行,并判断当前行是否有效
        while (rs.next()) {
            // - 获取数据 getXxx()
            int id = rs.getInt(1);
            // getXxx()方法可以使用列索引(从1开始)也可以使用列名
            String usrname = rs.getString("username");
            String passwd = rs.getString(3);

            System.out.println("id: " + id);
            System.out.println("username: " + usrname);
            System.out.println("passwd: " + passwd);
            System.out.println("-----------------------");
        }
        // - 释放资源
        // ResultSet、Statement和Connection都要按照顺序释放资源
        // 先释放ResultSet, 再释放Statement, 最后是Connection
        rs.close();
        stmt.close();
        conn.close();
    }
}

操作实例

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

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

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

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

    package pojo; // pojo包存放实体类
    
    public class User {
    
        private Integer id;
        private String username;
        private String password;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "Account{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    

增删改操作

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

package dao;

import pojo.User;

import java.sql.*;

public class UserDAO {

    private static String URL = "jdbc:mysql:///test" +
                "?useSSL=false&useServerPrepStmts=true";
    private static String USERNAME = "root";
    private static String PASSWORD = "1234";

    /**
     * 根据用户名和密码查询
     * @param username
     * @param password
     * @return User
     * @throws SQLException
     */
    public User select(String username, String password) throws SQLException {

        // 参数有null值时
        if (username == null || password == null) {
            return null;
        }

        // 连接数据库
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        // 获取PreparedStatement对象, 并设置SQL语句
        String sql = "SELECT id, username, password" +
                " FROM tb_user" +
                " WHERE username = ?" +
                " AND password = ?";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, username);
        pstmt.setString(2, password);

        // 获取ResultSet
        ResultSet rs = pstmt.executeQuery();

        User user = null;
        if (rs.next()) {
            user = new User();

            Integer id = rs.getInt("id");
            String name = rs.getString("username");
            String pw = rs.getString("password");

            user.setId(id);
            user.setUsername(name);
            user.setPassword(pw);
        }

        rs.close();
        pstmt.close();
        conn.close();

        return user;
    }

    /**
     * 根据用户名和密码添加数据
     * @param username
     * @param password
     * @return boolean
     * @throws SQLException
     */
    public boolean add(String username, String password) throws SQLException {

        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        String sql = "INSERT INTO tb_user" +
                " VALUE(null, ?, ?)";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, username);
        pstmt.setString(2, password);

        int count = pstmt.executeUpdate();

        pstmt.close();
        conn.close();

        return count > 0;
    }

    /**
     * 根据用户名和密码删除数据
     * @param username
     * @param password
     * @return boolean
     * @throws SQLException
     */
    public boolean delete(String username, String password) throws SQLException {

        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        String sql = "DELETE FROM tb_user" +
                " WHERE username = ?" +
                " AND password = ?";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, username);
        pstmt.setString(2, password);

        int count = pstmt.executeUpdate();

        pstmt.close();
        conn.close();

        return count > 0;
    }
}