MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java JDBC数据库操作

2022-03-141.4k 阅读

Java JDBC 简介

Java 数据库连接(Java Database Connectivity,简称 JDBC)是 Java 编程语言中用于执行 SQL 语句的 API。它提供了一个通用的抽象层,使得 Java 程序能够与各种不同类型的数据库进行交互,如 MySQL、Oracle、SQL Server 等,而无需为每种数据库编写特定的代码。JDBC 是 Java 企业级应用开发中不可或缺的一部分,广泛应用于 Web 应用程序、企业级软件等领域,用于实现数据的持久化存储和查询。

JDBC 架构

JDBC 架构主要分为两层:应用层和驱动层。

  1. 应用层:这是开发人员编写的 Java 代码所在的层次,通过 JDBC API 来执行数据库操作。应用层使用 JDBC 接口来与数据库进行交互,这些接口包括 DriverManagerConnectionStatementResultSet 等。
  2. 驱动层:由各个数据库厂商提供的 JDBC 驱动组成,负责与具体的数据库进行通信。每个数据库都有其特定的 JDBC 驱动,例如 MySQL 有 mysql - connector - java 驱动,Oracle 有 ojdbc 驱动等。这些驱动实现了 JDBC 接口,将应用层的请求转换为数据库能够理解的命令,并将数据库的响应转换回 Java 可以处理的形式。

JDBC 核心接口

  1. DriverManager
    • 作用DriverManager 类是 JDBC 的管理层,作用于用户和驱动程序之间。它跟踪可用的驱动程序,并在数据库和相应驱动之间建立连接。
    • 常用方法
      • static Connection getConnection(String url, String user, String password):尝试建立到给定数据库 URL 的连接。url 是数据库的地址,userpassword 是数据库的登录凭证。例如,对于 MySQL 数据库,url 格式通常为 jdbc:mysql://localhost:3306/mydb,其中 localhost 是数据库服务器地址,3306 是端口号,mydb 是数据库名称。
      • static void registerDriver(Driver driver):向 DriverManager 注册给定的驱动程序。在 JDBC 4.0 及更高版本中,驱动程序可以通过服务提供商接口(SPI)自动注册,无需手动调用此方法。
  2. Connection
    • 作用Connection 接口代表与数据库的连接。一个 Connection 对象包含了数据库的特定配置信息,并提供了创建 Statement 对象的方法,用于执行 SQL 语句。
    • 常用方法
      • Statement createStatement():创建一个 Statement 对象,用于执行不带参数的 SQL 语句。
      • PreparedStatement prepareStatement(String sql):创建一个 PreparedStatement 对象,用于执行预编译的 SQL 语句。预编译语句可以防止 SQL 注入攻击,并且在多次执行相同结构的 SQL 语句时性能更好。
      • CallableStatement prepareCall(String sql):创建一个 CallableStatement 对象,用于执行数据库存储过程。
      • void close():关闭此数据库连接。
  3. Statement
    • 作用Statement 接口用于执行静态 SQL 语句并返回其生成的结果。它提供了执行 SQL 查询(如 SELECT)和更新(如 INSERTUPDATEDELETE)的方法。
    • 常用方法
      • ResultSet executeQuery(String sql):执行给定的 SQL 查询语句,返回一个 ResultSet 对象,该对象包含查询结果集。此方法适用于 SELECT 语句。
      • int executeUpdate(String sql):执行给定的 SQL 更新语句(INSERTUPDATEDELETE),返回受影响的行数。
      • void close():关闭此 Statement 对象。
  4. ResultSet
    • 作用ResultSet 接口表示数据库查询的结果集,它通过迭代的方式提供对查询结果的访问。ResultSet 对象维护一个指向当前数据行的光标,最初光标位于第一行之前。
    • 常用方法
      • boolean next():将光标从当前位置向前移动一行。如果新的当前行有效,则返回 true;否则返回 false,表示结果集已遍历完毕。
      • getXXX(int columnIndex)getXXX(String columnName):获取当前行中指定列的值,XXX 代表数据类型,如 getStringgetIntgetDate 等。columnIndex 是从 1 开始的列索引,columnName 是列的名称。

JDBC 操作步骤

  1. 加载 JDBC 驱动 在 JDBC 4.0 之前,需要显式加载驱动程序,例如对于 MySQL 驱动:
try {
    Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

在 JDBC 4.0 及更高版本中,驱动程序会自动加载,无需手动调用 Class.forName。 2. 建立数据库连接

String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
    // 在这里进行后续数据库操作
} catch (SQLException e) {
    e.printStackTrace();
}
  1. 创建 Statement 对象
    • 创建普通 Statement
Statement stmt = conn.createStatement();
  • 创建 PreparedStatement
String sql = "INSERT INTO users (name, age) VALUES (?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
  1. 执行 SQL 语句
    • 执行查询语句(SELECT
String query = "SELECT * FROM users";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
    String name = rs.getString("name");
    int age = rs.getInt("age");
    System.out.println("Name: " + name + ", Age: " + age);
}
  • 执行更新语句(INSERTUPDATEDELETE
String insertSql = "INSERT INTO users (name, age) VALUES ('John', 25)";
int rowsInserted = stmt.executeUpdate(insertSql);
System.out.println("Rows inserted: " + rowsInserted);

对于 PreparedStatement 执行更新:

pstmt.setString(1, "Jane");
pstmt.setInt(2, 30);
int rowsUpdated = pstmt.executeUpdate();
System.out.println("Rows updated: " + rowsUpdated);
  1. 处理结果集(如果有) 如上述查询示例中,通过 ResultSetnext 方法和 getXXX 方法来遍历和获取结果集的数据。
  2. 关闭资源 在操作完成后,需要关闭 ResultSetStatementConnection,以释放资源。在 Java 7 及更高版本中,可以使用 try - with - resources 语句自动关闭资源:
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(query)) {
    // 处理结果集
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 事务处理

  1. 事务概念 事务是一组数据库操作的逻辑单元,这些操作要么全部成功执行,要么全部回滚(撤销)。例如,在银行转账操作中,从一个账户扣款和向另一个账户存款必须作为一个事务处理,以确保数据的一致性。
  2. JDBC 中的事务处理
    • 自动提交模式:默认情况下,JDBC 连接处于自动提交模式,即每执行一条 SQL 语句就会立即提交到数据库。可以通过 Connection 对象的 setAutoCommit(false) 方法关闭自动提交。
    • 提交事务:调用 Connection 对象的 commit() 方法来提交事务,将所有在事务中的操作永久保存到数据库。
    • 回滚事务:调用 Connection 对象的 rollback() 方法来回滚事务,撤销在事务中执行的所有操作。 示例代码:
try (Connection conn = DriverManager.getConnection(url, user, password)) {
    conn.setAutoCommit(false);
    try {
        String sql1 = "UPDATE accounts SET balance = balance - 100 WHERE account_id = 1";
        stmt.executeUpdate(sql1);
        String sql2 = "UPDATE accounts SET balance = balance + 100 WHERE account_id = 2";
        stmt.executeUpdate(sql2);
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        e.printStackTrace();
    }
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 连接池

  1. 连接池概念 在传统的 JDBC 操作中,每次建立数据库连接都需要消耗一定的资源和时间。连接池是一种缓存数据库连接的技术,它在应用程序启动时预先创建一定数量的数据库连接,并将这些连接保存在池中。当应用程序需要与数据库交互时,从连接池中获取一个连接,使用完毕后再将其归还到连接池中,而不是每次都创建和销毁连接,从而提高了应用程序的性能和资源利用率。
  2. 常见的 JDBC 连接池
    • HikariCP:是一个高性能的 JDBC 连接池,具有快速的连接获取速度和低资源消耗的特点。它被广泛应用于各种 Java 项目中。
    • C3P0:是一个开源的 JDBC 连接池库,提供了丰富的配置选项,易于使用和集成到项目中。
    • Tomcat JDBC Pool:是 Apache Tomcat 服务器自带的 JDBC 连接池,与 Tomcat 服务器紧密集成,也可以在其他 Java 应用中使用。
  3. 使用 HikariCP 连接池示例
    • 添加依赖:如果使用 Maven,在 pom.xml 文件中添加以下依赖:
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
</dependency>
  • 配置和使用连接池
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class HikariCPExample {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        dataSource = new HikariDataSource(config);
    }

    public static void main(String[] args) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users");
             ResultSet rs = pstmt.executeQuery()) {
            while (rs.next()) {
                String name = rs.getString("name");
                int age = rs.getInt("age");
                System.out.println("Name: " + name + ", Age: " + age);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

JDBC 与数据库元数据

  1. 数据库元数据概念 数据库元数据是关于数据库结构和对象的信息,如数据库中有哪些表、表中有哪些列、列的数据类型是什么、数据库支持哪些特性等。JDBC 提供了访问数据库元数据的接口,使得开发人员可以在运行时获取这些信息,从而编写更灵活和通用的数据库操作代码。
  2. 获取数据库元数据 通过 Connection 对象的 getMetaData() 方法可以获取 DatabaseMetaData 对象,该对象提供了一系列方法来查询数据库元数据。 示例代码:
try (Connection conn = DriverManager.getConnection(url, user, password)) {
    DatabaseMetaData metaData = conn.getMetaData();
    System.out.println("Database Product Name: " + metaData.getDatabaseProductName());
    System.out.println("Database Product Version: " + metaData.getDatabaseProductVersion());
    ResultSet tables = metaData.getTables(null, null, null, new String[]{"TABLE"});
    while (tables.next()) {
        String tableName = tables.getString("TABLE_NAME");
        System.out.println("Table Name: " + tableName);
    }
} catch (SQLException e) {
    e.printStackTrace();
}
  1. 获取结果集元数据 通过 ResultSet 对象的 getMetaData() 方法可以获取 ResultSetMetaData 对象,该对象提供了关于结果集列的信息,如列名、列的数据类型等。 示例代码:
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    for (int i = 1; i <= columnCount; i++) {
        System.out.println("Column Name: " + metaData.getColumnName(i) + ", Data Type: " + metaData.getColumnTypeName(i));
    }
    while (rs.next()) {
        // 处理结果集数据
    }
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 中的批处理

  1. 批处理概念 批处理是指将多个 SQL 语句组合成一个批处理,然后一次性发送到数据库执行。这样可以减少应用程序与数据库之间的通信次数,提高数据库操作的效率,特别是在需要执行大量相同结构的 SQL 语句时。
  2. 使用 Statement 进行批处理
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement()) {
    for (int i = 0; i < 100; i++) {
        String insertSql = "INSERT INTO users (name, age) VALUES ('User" + i + "', " + (20 + i) + ")";
        stmt.addBatch(insertSql);
    }
    stmt.executeBatch();
} catch (SQLException e) {
    e.printStackTrace();
}
  1. 使用 PreparedStatement 进行批处理
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name, age) VALUES (?,?)")) {
    for (int i = 0; i < 100; i++) {
        pstmt.setString(1, "User" + i);
        pstmt.setInt(2, 20 + i);
        pstmt.addBatch();
    }
    pstmt.executeBatch();
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 高级特性

  1. LOB(Large Object)处理
    • 概念:LOB 用于存储大型数据对象,如二进制大对象(BLOB)和字符大对象(CLOB)。BLOB 通常用于存储图片、音频、视频等二进制数据,CLOB 用于存储大量的文本数据。
    • 处理方法
      • 插入 LOB 数据:使用 PreparedStatementsetBlobsetClob 方法。例如,插入 BLOB 数据:
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement("INSERT INTO files (file_name, file_data) VALUES (?,?)")) {
    File file = new File("image.jpg");
    FileInputStream fis = new FileInputStream(file);
    pstmt.setString(1, "image.jpg");
    pstmt.setBlob(2, fis);
    pstmt.executeUpdate();
} catch (SQLException | FileNotFoundException e) {
    e.printStackTrace();
}
 - **读取 LOB 数据**:使用 `ResultSet` 的 `getBlob` 或 `getClob` 方法。例如,读取 BLOB 数据:
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT file_data FROM files WHERE file_name = 'image.jpg'")) {
    if (rs.next()) {
        Blob blob = rs.getBlob("file_data");
        InputStream is = blob.getBinaryStream();
        // 处理输入流,例如保存为文件
    }
} catch (SQLException e) {
    e.printStackTrace();
}
  1. 存储过程调用
    • 概念:存储过程是存储在数据库中的预编译 SQL 代码块,它可以接受输入参数、返回输出参数,并执行一系列数据库操作。调用存储过程可以提高数据库操作的安全性和效率,因为存储过程在数据库服务器端执行,减少了网络传输和代码重复。
    • 调用方法:使用 CallableStatement 接口。例如,假设数据库中有一个存储过程 get_user_by_id,接受一个用户 ID 作为输入参数,并返回用户的姓名和年龄:
try (Connection conn = DriverManager.getConnection(url, user, password);
     CallableStatement cstmt = conn.prepareCall("{call get_user_by_id(?,?,?)}")) {
    cstmt.setInt(1, 1);
    cstmt.registerOutParameter(2, Types.VARCHAR);
    cstmt.registerOutParameter(3, Types.INTEGER);
    cstmt.execute();
    String name = cstmt.getString(2);
    int age = cstmt.getInt(3);
    System.out.println("Name: " + name + ", Age: " + age);
} catch (SQLException e) {
    e.printStackTrace();
}

JDBC 常见问题及解决方法

  1. SQL 注入问题
    • 问题描述:SQL 注入是一种常见的安全漏洞,攻击者通过在输入字段中插入恶意的 SQL 语句,从而改变原 SQL 语句的逻辑,达到获取敏感数据或执行非法操作的目的。例如,在登录验证的 SQL 语句中,如果用户名和密码的输入没有进行正确的处理,攻击者可以通过输入特殊字符来绕过验证。
    • 解决方法:使用 PreparedStatement 代替 StatementPreparedStatement 会对输入参数进行预编译,将参数值与 SQL 语句的结构分离,从而防止 SQL 注入攻击。
  2. 连接泄漏问题
    • 问题描述:连接泄漏是指应用程序在使用完数据库连接后,没有正确关闭连接,导致连接资源无法释放,最终可能耗尽连接池中的所有连接,使应用程序无法再获取新的连接,从而影响系统的正常运行。
    • 解决方法:使用 try - with - resources 语句自动关闭连接、StatementResultSet 等资源,确保在代码块结束时资源被正确释放。另外,在使用连接池时,连接池通常也会提供一些机制来检测和回收泄漏的连接。
  3. 性能问题
    • 问题描述:在处理大量数据或高并发请求时,JDBC 操作可能会出现性能瓶颈,如频繁的连接创建和销毁、低效的 SQL 语句等。
    • 解决方法:使用连接池技术减少连接创建和销毁的开销;对 SQL 语句进行优化,例如使用索引、避免全表扫描;使用批处理操作减少数据库通信次数;合理设置连接池的参数,如最大连接数、最小连接数等,以适应应用程序的负载。

通过以上对 Java JDBC 数据库操作的详细介绍,希望能帮助开发者深入理解 JDBC 的原理和使用方法,在实际项目中能够高效、安全地进行数据库交互。