首页 > 技术文章 > JDBC

swifthao 2020-03-25 22:48 原文

JDBC是一套接口规范,它在哪呢?就在Java的标准库java.sql里放着,不过这里面大部分都是接口。接口并不能直接实例化,而是必须实例化对应的实现类,然后通过接口引用这个实例。

要获取数据库连接,使用如下代码:

Class.forName("com.mysql.jdbc.Driver");//固定写法,加载驱动
String JDBC_URL = "jdbc:mysql://localhost:3306/test";// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();

核心代码是DriverManager提供的静态方法getConnection()。DriverManager会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。
因为JDBC连接是一种昂贵的资源,所以使用后要及时释放。使用try (resource)来自动释放JDBC连接是一个好方法:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    ...
}

JDBC查询

获取到JDBC连接后,下一步我们就可以查询数据库了。查询数据库分以下几步:

第一步,通过Connection提供的createStatement()方法创建一个Statement对象,用于执行一个查询;

第二步,执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;

第三步,反复调用ResultSet的next()方法并读取每一行结果。

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) {
    try (Statement stmt = conn.createStatement()) {
        try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=\'M\'")) {
            while (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从1开始
                long grade = rs.getLong(2);
                String name = rs.getString(3);
                String gender = rs.getString(4);
            }
        }
    }
}

ResultSet获取列时,索引从1开始而不是0


SQL注入

避免SQL注入攻击的办法就是使用PreparedStatement。使用PreparedStatement可以完全避免SQL注入的问题,因为PreparedStatement始终使用?作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。上述登录SQL如果用PreparedStatement可以改写如下:

User login(String name, String pass) {
    ...
    String sql = "SELECT * FROM user WHERE login=? AND pass=?";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setObject(1, name);
    ps.setObject(2, pass);
    ...
}

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) {
    try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
        ps.setObject(1, "M"); // 注意:索引从1开始
        ps.setObject(2, 3);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                long id = rs.getLong("id");
                long grade = rs.getLong("grade");
                String name = rs.getString("name");
                String gender = rs.getString("gender");
            }
        }
    }
}

使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!

注意到JDBC查询的返回值总是ResultSet,即使我们写这样的聚合查询SELECT SUM(score) FROM ...,也需要按结果集读取:

ResultSet rs = ...
if (rs.next()) {
    double sum = rs.getDouble(1);
}

JDBC更新

插入:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) {
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {
        ps.setObject(1, 999); // 注意:索引从1开始
        ps.setObject(2, 1); // grade
        ps.setObject(3, "Bob"); // name
        ps.setObject(4, "M"); // gender
        int n = ps.executeUpdate(); // 1
    }
}

更新:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) {
    try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
        ps.setObject(1, "Bob"); // 注意:索引从1开始
        ps.setObject(2, 999);
        int n = ps.executeUpdate(); // 返回更新的行数
    }
}

删除:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) {
    try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
        ps.setObject(1, 999); // 注意:索引从1开始
        int n = ps.executeUpdate(); // 删除的行数
    }
}

使用JDBC执行INSERT、UPDATE和DELETE都可视为更新操作;
更新操作使用PreparedStatement的executeUpdate()进行,返回受影响的行数。


JDBC事务

Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

JDBC Batch

使用JDBC操作数据库的时候,经常会执行一些批量操作:

try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
    // 对同一个PreparedStatement反复设置参数并调用addBatch():
    for (String name : names) {
        ps.setString(1, name);
        ps.setBoolean(2, gender);
        ps.setInt(3, grade);
        ps.setInt(4, score);
        ps.addBatch(); // 添加到batch
    }
    // 执行batch:
    int[] ns = ps.executeBatch();
    for (int n : ns) {
        System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量
    }
}

执行batch和执行一个SQL不同点在于,需要对同一个PreparedStatement反复设置参数并调用addBatch(),这样就相当于给一个SQL加上了多组参数,相当于变成了“多行”SQL。

第二个不同点是调用的不是executeUpdate(),而是executeBatch(),因为我们设置了多组参数,相应地,返回结果也是多个int值,因此返回类型是int[ ],循环int[ ]数组即可获取每组参数执行后影响的结果数量。


JDBC 连接池

我们在讲多线程的时候说过,创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。

类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。常用的JDBC连接池有:

  • HikariCP
  • C3P0
  • BoneCP
  • Druid

目前使用最广泛的是HikariCP。以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖

需要创建一个DataSource实例,这个实例就是连接池:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("password");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);

注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。

有了连接池以后,我们如何使用它呢?和前面的代码类似,只是获取Connection时,把DriverManage.getConnection()改为ds.getConnection()

try (Connection conn = ds.getConnection()) { // 在此获取连接
    ...
} // 在此“关闭”连接

调用conn.close()方法时(在try(resource){...}结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。

因此,连接池内部维护了若干个Connection实例,如果调用ds.getConnection(),就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection调用close(),那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的SQL语句。

通常连接池提供了大量的参数可以配置,例如,维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,需要根据应用程序的负载合理地配置这些参数。此外,大多数连接池都提供了详细的实时状态以便进行监控。

推荐阅读