Java数据库连接(JDBC)
JDBC(Java Database Connectivity)是Java语言访问数据库的标准API(Application Programming Interface)。
它提供了一组用于执行SQL语句、访问和处理数据库的类和接口。通过JDBC,Java应用程序可以连接到各种数据库管理系统(如MySQL、Oracle、SQL Server等),并执行诸如查询、插入、更新和删除等操作。
# JDBC基础
# JDBC版本演进
JDBC经历了多个版本的发展:
- JDBC 1.0(1997):基础数据库连接和SQL执行功能
- JDBC 2.0(1999):引入
DataSource
、连接池、分布式事务支持 - JDBC 3.0(2001):添加
Savepoint
、参数元数据、自动生成键获取 - JDBC 4.0(2006):自动驱动加载、
SQLException
增强、XML支持 - JDBC 4.1(2011):
try-with-resources
支持、RowSet
增强 - JDBC 4.2(2014):支持
java.time
包、批量更新增强 - JDBC 4.3(2017):分片支持、连接建造器模式
# 1 JDBC架构
JDBC架构由两个重要的组件构成:JDBC驱动程序和JDBC API。JDBC驱动程序负责与特定的数据库管理系统进行通信,而JDBC API则提供了一组类和接口,供Java应用程序与JDBC驱动程序进行交互。
+-----------------+
| JDBC API |
+-----------------+
|
|
V
+-----------------+
| JDBC Driver |
+-----------------+
|
|
V
+---------------------------+
| Database System |
+---------------------------+
# 2 JDBC驱动类型
JDBC定义了四种驱动程序类型:
- Type 1 - JDBC-ODBC桥接驱动:通过ODBC驱动访问数据库(已废弃)
- Type 2 - 本地API驱动:将JDBC调用转换为数据库特定的本地API调用
- Type 3 - 网络协议驱动:通过中间件服务器转换JDBC调用
- Type 4 - 纯Java驱动:直接将JDBC调用转换为数据库网络协议(推荐使用)
现代应用程序主要使用Type 4驱动,如:
- MySQL:
com.mysql.cj.jdbc.Driver
- PostgreSQL:
org.postgresql.Driver
- Oracle:
oracle.jdbc.OracleDriver
- SQL Server:
com.microsoft.sqlserver.jdbc.SQLServerDriver
# 3 JDBC组件介绍
以下的示例均使用MySQL数据,要了解相关知识,请参考:理解MySQL。
- DriverManager:
DriverManager
类是JDBC的基础类之一,它负责管理JDBC驱动程序。通过DriverManager
可以获取数据库连接对象。
// 示例代码
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "username";
String password = "password";
Connection connection = DriverManager.getConnection(url, user, password);
- Driver:
Driver
接口是JDBC驱动程序的核心接口,每个数据库驱动程序都必须实现该接口。通过Driver接口的实现类,可以注册驱动程序到DriverManager
中。
// JDBC 4.0之前需要手动加载驱动
String driverClassName = "com.mysql.cj.jdbc.Driver";
Class.forName(driverClassName);
// JDBC 4.0+自动加载驱动(推荐)
// 驱动jar包中的META-INF/services/java.sql.Driver文件会自动注册
- Connection:
Connection
接口表示与数据库的连接。通过Connection
对象,可以创建Statement
对象、提交事务、关闭连接等操作。
// 示例代码
Connection connection = DriverManager.getConnection(url, user, password);
Statement statement = connection.createStatement();
- Statement:
Statement
接口用于执行SQL语句并返回结果。可以通过Statement
对象执行查询语句(executeQuery)或更新语句(executeUpdate)。
// 示例代码
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM employees");
- ResultSet:
ResultSet
接口用于表示SQL查询的结果集。通过ResultSet对象可以读取和处理查询结果。
// 示例代码
while (resultSet.next()) {
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
// 处理结果
}
# 建立连接与增删改查
# 1 安装JDBC驱动
安装JDBC驱动程序是使用JDBC的前提条件。
以MySQL为例,在Maven项目中,请将以下内容添加到您的项目的pom.xml
文件中的<dependencies>
标签内:
<dependencies>
<!-- 其他依赖项 -->
<!-- MySQL 8 驱动程序 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 其他依赖项 -->
</dependencies>
如果是Gradle项目,请将以下内容添加到您的项目的build.gradle
文件中的dependencies
部分:
dependencies {
// 其他依赖项
// MySQL 8 驱动程序
implementation 'com.mysql:mysql-connector-j:8.0.33'
// 其他依赖项
}
其他常用数据库驱动依赖:
PostgreSQL:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
Oracle:
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>23.2.0.0</version>
</dependency>
SQL Server:
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>12.4.2.jre11</version>
</dependency>
# 2 查询
# 传统方式(不推荐)
以下是使用传统方式的示例:
import java.sql.*;
public class TraditionalExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "your_username";
String password = "your_password";
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
connection = DriverManager.getConnection(url, username, password);
statement = connection.createStatement();
resultSet = statement.executeQuery("SELECT * FROM employees");
while (resultSet.next()) {
System.out.println("ID: " + resultSet.getInt("id") +
", Name: " + resultSet.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 需要手动关闭资源,容易遗漏
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
# 使用try-with-resources(推荐)
Java 7引入的try-with-resources
语句可以自动管理资源:
import java.sql.*;
public class ModernExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC";
String username = "your_username";
String password = "your_password";
String sql = "SELECT * FROM employees WHERE age > ?";
// try-with-resources自动关闭资源
try (Connection connection = DriverManager.getConnection(url, username, password);
PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setInt(1, 25);
try (ResultSet resultSet = pstmt.executeQuery()) {
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
System.out.printf("ID: %d, Name: %s, Age: %d%n", id, name, age);
}
}
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
e.printStackTrace();
}
}
}
关键点说明:
try-with-resources
会自动调用资源的close()
方法- 资源按照声明的相反顺序关闭
- 即使发生异常,资源也会被正确关闭
- 支持JDBC 4.0+的自动驱动加载,无需手动
Class.forName()
这里简单介绍一下ResultSet
,它使用next()
方法将游标移动到结果集的下一行,并使用getXXX()
方法获取当前行的数据。
比如getInt()
、getString()
等方法,这些方法的参数可以是列名,也可以是列索引,比如getInt(1)
。需要注意,列索引是从1开始计数的。
# 3 PreparedStatements和防止SQL注入
看一下PreparedStatements
和Statement
的区别:
- 预编译性能:
PreparedStatement
在执行之前进行预编译,将SQL语句和参数分离,并且数据库可以缓存预编译的语句,提高了执行相同语句的性能。而Statement
在执行时直接将SQL语句发送给数据库执行,没有预编译的过程,因此每次执行都需要解析和编译SQL语句,性能较低。 - 参数设置:
PreparedStatement
允许通过占位符(?)的方式动态设置参数值,参数值可以通过方法设置,并且支持各种数据类型的自动转换。而Statement
没有直接支持参数设置的功能,参数值需要通过字符串拼接的方式嵌入SQL语句中,不够灵活和安全。 - 防止SQL注入攻击:由于
PreparedStatement
的参数设置是通过占位符的方式进行的,参数值会被正确地进行转义和处理,从而防止了SQL注入攻击。而Statement
中的参数值嵌入在SQL语句中,容易受到恶意输入的影响,存在SQL注入的风险。 - 可读性和维护性:
PreparedStatement
在代码中使用了参数占位符,使得SQL语句与参数值的逻辑分离,代码更加清晰和易读。而Statement
的参数值直接嵌入在SQL语句中,使得SQL语句变得冗长,可读性和维护性较差。
综合来看,推荐在开发中尽可能使用PreparedStatement
来执行SQL操作。
以下是一个PreparedStatement
查询的示例:
...
// 建立连接
connection = DriverManager.getConnection(url, username, password);
// 使用PreparedStatement进行查询
String query = "SELECT * FROM employees WHERE age > ? AND name LIKE ? AND salary BETWEEN ? AND ? AND department IS NULL";
PreparedStatement preparedStatement = preparedStatement = connection.prepareStatement(query);
preparedStatement.setInt(1, 30); // 设置年龄大于30
preparedStatement.setString(2, "%John%"); // 设置名字中包含"John"
preparedStatement.setDouble(3, 2000.0); // 设置工资范围下限
preparedStatement.setDouble(4, 5000.0); // 设置工资范围上限
resultSet = preparedStatement.executeQuery();
// 处理查询结果
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
double salary = resultSet.getDouble("salary");
String department = resultSet.getString("department");
// 其他列的处理...
System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age + ", Salary: " + salary + ", Department: " + department);
}
...
# 3 增删改
以下示例展示了如何使用PreparedStatement
进行增删改操作(使用try-with-resources
):
import java.sql.*;
public class CRUDExample {
private static final String URL = "jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC";
private static final String USERNAME = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
// 插入数据
insertEmployee("John Doe", 30, 5000.0, "IT");
// 更新数据
updateSalaryByDepartment("IT", 6000.0);
// 删除数据
deleteEmployeesByAge(40);
}
private static void insertEmployee(String name, int age, double salary, String department) {
String sql = "INSERT INTO employees (name, age, salary, department) VALUES (?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
pstmt.setInt(2, age);
pstmt.setDouble(3, salary);
pstmt.setString(4, department);
int rowsInserted = pstmt.executeUpdate();
System.out.println(rowsInserted + " row(s) inserted.");
} catch (SQLException e) {
System.err.println("Insert failed: " + e.getMessage());
}
}
private static void updateSalaryByDepartment(String department, double newSalary) {
String sql = "UPDATE employees SET salary = ? WHERE department = ?";
try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDouble(1, newSalary);
pstmt.setString(2, department);
int rowsUpdated = pstmt.executeUpdate();
System.out.println(rowsUpdated + " row(s) updated.");
} catch (SQLException e) {
System.err.println("Update failed: " + e.getMessage());
}
}
private static void deleteEmployeesByAge(int age) {
String sql = "DELETE FROM employees WHERE age > ?";
try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, age);
int rowsDeleted = pstmt.executeUpdate();
System.out.println(rowsDeleted + " row(s) deleted.");
} catch (SQLException e) {
System.err.println("Delete failed: " + e.getMessage());
}
}
}
# 获取自增ID
插入数据后获取自动生成的主键:
private static long insertEmployeeAndGetId(String name, int age, double salary, String department) {
String sql = "INSERT INTO employees (name, age, salary, department) VALUES (?, ?, ?, ?)";
long generatedId = -1;
try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, name);
pstmt.setInt(2, age);
pstmt.setDouble(3, salary);
pstmt.setString(4, department);
int rowsInserted = pstmt.executeUpdate();
if (rowsInserted > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
generatedId = generatedKeys.getLong(1);
System.out.println("Generated ID: " + generatedId);
}
}
}
} catch (SQLException e) {
System.err.println("Insert failed: " + e.getMessage());
}
return generatedId;
}
# 事务管理
# 1 什么是事务?
事务是一组对数据库的操作,这些操作要么全部成功执行,要么全部失败回滚。事务具有四个特性,即ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
# 2 JDBC中的事务控制(commit和rollback)
JDBC提供了commit
和rollback
方法来控制事务的提交和回滚。在执行事务操作之前,需要将Connection
对象的自动提交模式关闭,以启用事务支持。
// 示例代码
connection.setAutoCommit(false); // 关闭自动提交
// 执行事务操作
connection.commit(); // 提交事务
connection.rollback(); // 回滚事务
示例:
...
// 建立连接
connection = DriverManager.getConnection(url, username, password);
// 关闭自动提交
connection.setAutoCommit(false);
// 创建Statement对象
Statement statement = connection.createStatement();
// 执行事务操作
try {
// 第一条SQL语句
String sql1 = "UPDATE employees SET salary = salary + 1000 WHERE department = 'IT'";
statement.executeUpdate(sql1);
// 第二条SQL语句
String sql2 = "UPDATE employees SET salary = salary - 500 WHERE department = 'HR'";
statement.executeUpdate(sql2);
// 提交事务
connection.commit();
System.out.println("事务已提交");
} catch (SQLException e) {
// 回滚事务
connection.rollback();
System.out.println("事务已回滚");
}
// 恢复自动提交
connection.setAutoCommit(true);
...
这里解释一下自动提交。
在JDBC中,自动提交(auto-commit)是指每个SQL语句在执行完毕后是否立即提交到数据库。默认情况下,连接对象的自动提交模式是打开的(即connection.setAutoCommit(true)
)。
当自动提交模式为打开时,每个执行的SQL语句都会立即提交到数据库,这意味着每个SQL语句都被视为一个单独的事务。如果SQL语句执行成功,它们的结果将永久保存到数据库中。如果SQL语句执行失败,它们的结果将不会被保存,数据库不会回滚已执行的操作。
如果你想要将多个SQL语句作为一个逻辑单元进行提交或回滚,就需要关闭自动提交模式(即connection.setAutoCommit(false)
)。在关闭自动提交模式后,多个SQL语句将被组合成一个事务。只有在显式调用commit()
提交事务时,事务中的所有SQL语句才会一起提交到数据库。如果在事务中的任何一个SQL语句执行失败,你可以通过调用rollback()
回滚事务,将所有已执行的操作撤消。
# 3 设置事务隔离级别
事务隔离级别定义了多个事务之间的可见性和并发性。JDBC提供了设置事务隔离级别的方法,可以通过Connection
对象的setTransactionIsolation
方法来设置事务隔离级别。
// 示例代码
int isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
connection.setTransactionIsolation(isolationLevel);
# 存储过程和函数
# 1 使用JDBC调用存储过程
存储过程是预先编译的一组SQL语句,可以作为一个单元一起执行。通过JDBC可以调用存储过程并获取其执行结果。
// 示例代码
String sql = "{CALL my_procedure(?, ?)}";
CallableStatement callableStatement = connection.prepareCall(sql);
callableStatement.setString(1, "John Doe");
callableStatement.setInt(2, 30);
callableStatement.execute();
# 2 使用JDBC调用数据库函数
数据库函数是在数据库中定义的可重用的SQL代码片段,可以通过JDBC调用数据库函数并获取返回值。
// 示例代码
String sql = "{? = CALL my_function(?, ?)}";
CallableStatement callableStatement = connection.prepareCall(sql);
callableStatement.registerOutParameter(1, Types.INTEGER);
callableStatement.setString(2, "John Doe");
callableStatement.setInt(3, 30);
callableStatement.execute();
int result = callableStatement.getInt(1); // 获取返回值
# JDBC高级特性
# 1 批处理
批处理允许将一组SQL语句作为批量操作一起执行,可以提高执行效率。JDBC提供了批处理的支持,可以通过Statement
对象或PreparedStatement
对象来执行批处理操作。
// 示例代码
Statement statement = connection.createStatement();
statement.addBatch("INSERT INTO employees (name, age) VALUES ('John Doe', 30)");
statement.addBatch("UPDATE employees SET age = 35 WHERE name = 'Jane Smith'");
statement.executeBatch();
# 2 连接池
连接池是一组预先创建的数据库连接,可以重复使用,从而减少了连接和断开连接的开销。
# 为什么需要连接池?
- 性能提升:避免频繁创建和销毁连接的开销
- 资源管理:限制最大连接数,防止资源耗尽
- 连接复用:多个请求可以共享连接
- 故障恢复:自动检测和恢复失效连接
# HikariCP(推荐)
HikariCP是目前最快的连接池,Spring Boot 2.0+默认使用:
Maven依赖:
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
基本配置示例:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class HikariCPExample {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
// 基本配置
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC");
config.setUsername("username");
config.setPassword("password");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 连接池配置
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接数
config.setIdleTimeout(300000); // 空闲超时时间(5分钟)
config.setConnectionTimeout(30000); // 连接超时时间(30秒)
config.setMaxLifetime(1800000); // 连接最大生命周期(30分钟)
// 性能优化配置
config.setAutoCommit(true);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws Exception {
return dataSource.getConnection();
}
public static void closeDataSource() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
}
// 使用示例
public static void main(String[] args) {
String sql = "SELECT * FROM employees WHERE department = ?";
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "IT");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
System.out.println("Name: " + rs.getString("name"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
# 配置文件方式(推荐)
使用hikari.properties
文件:
# HikariCP配置
dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
dataSource.url=jdbc:mysql://localhost:3306/mydatabase
dataSource.user=username
dataSource.password=password
# 连接池大小
maximumPoolSize=20
minimumIdle=5
# 超时设置
connectionTimeout=30000
idleTimeout=600000
maxLifetime=1800000
# 测试配置
connectionTestQuery=SELECT 1
加载配置:
HikariConfig config = new HikariConfig("/hikari.properties");
HikariDataSource dataSource = new HikariDataSource(config);
# 其他连接池对比
连接池 | 特点 | 适用场景 |
---|---|---|
HikariCP | 最快、轻量级、Spring Boot默认 | 高性能应用 |
Druid | 功能丰富、监控强大、阿里开源 | 需要监控统计 |
C3P0 | 成熟稳定、功能全面 | 传统应用 |
DBCP2 | Apache出品、简单易用 | 小型应用 |
# 连接池最佳实践
- 合理设置连接池大小:一般设置为
CPU核心数 * 2 + 磁盘数
- 设置合适的超时时间:避免连接泄漏
- 启用预编译语句缓存:提高性能
- 定期验证连接有效性:使用
connectionTestQuery
- 监控连接池状态:及时发现问题
- 使用try-with-resources:确保连接正确释放
# 3 元数据
元数据是关于数据库和其对象(表、列、索引等)的数据,可以通过JDBC的元数据API来访问。JDBC的元数据API提供了一组方法,用于获取和操作数据库的元数据信息。这些方法通过DatabaseMetaData
接口和ResultSetMetaData
接口来实现。
DatabaseMetaData
接口:DatabaseMetaData
接口提供了与数据库本身相关的元数据信息。通过Connection
对象的getMetaData()
方法获取DatabaseMetaData
实例。一些常见的DatabaseMetaData
方法包括:getDatabaseProductName()
:获取数据库产品名称。getDatabaseProductVersion()
:获取数据库产品版本。getTables()
:获取数据库中的表信息。getColumns()
:获取指定表的列信息。getIndexInfo()
:获取指定表的索引信息。getProcedures()
:获取数据库中的存储过程信息。
ResultSetMetaData
接口:ResultSetMetaData
接口提供了有关ResultSet
对象中列的元数据信息。通过ResultSet
对象的getMetaData()
方法获取ResultSetMetaData
实例。一些常见的ResultSetMetaData
方法包括:- getColumnCount(): 获取结果集中的列数。
- getColumnName(int column): 获取指定列索引的列名。
- getColumnLabel(int column): 获取指定列索引的列标签。
- getColumnType(int column): 获取指定列索引的列数据类型。
- getColumnTypeName(int column): 获取指定列索引的列数据类型名称。
- getColumnDisplaySize(int column): 获取指定列索引的列显示大小。
- isNullable(int column): 判断指定列索引的列是否可为空。
- isReadOnly(int column): 判断指定列索引的列是否为只读列。
- isAutoIncrement(int column): 判断指定列索引的列是否为自动增长列。
- isCaseSensitive(int column): 判断指定列索引的列是否区分大小写。
ParameterMetaData
接口:ParameterMetaData
接口提供了有关SQL语句参数的元数据信息,包括参数个数、参数类型、参数模式等。这对于动态生成SQL语句、处理可变参数的情况非常有用。一些常用的ParameterMetaData
方法包括:getParameterCount()
:获取SQL语句中的参数个数。getParameterType(int param)
:获取指定参数的SQL类型。getParameterTypeName(int param)
:获取指定参数的SQL类型名称。isNullable(int param)
:判断指定参数是否可为空。
通过使用
ParameterMetaData
,您可以获取关于SQL语句参数的信息,从而更好地构建和处理动态SQL语句。
通过使用元数据API,您可以获取有关数据库和查询结果的详细信息。这些信息对于动态生成SQL语句、进行结果集的处理和分析非常有用。您可以使用这些元数据信息来构建更灵活和可扩展的数据库操作逻辑。
ParameterMetaData
示例
...
// 创建PreparedStatement对象
String sql = "INSERT INTO employees (name, age, salary) VALUES (?, ?, ?)";
preparedStatement = connection.prepareStatement(sql);
// 获取ParameterMetaData
ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
// 获取参数个数
int parameterCount = parameterMetaData.getParameterCount();
System.out.println("参数个数:" + parameterCount);
// 获取参数类型
for (int i = 1; i <= parameterCount; i++) {
String parameterType = parameterMetaData.getParameterTypeName(i);
System.out.println("参数 " + i + " 的类型:" + parameterType);
}
...
DatabaseMetaData
和ResultSetMetaData
示例
演示了如何获取employee表的信息(表名、注释)和字段信息(名称、类型、注释、是否注解、是否空、默认值):
...
// 获取DatabaseMetaData
DatabaseMetaData databaseMetaData = connection.getMetaData();
// 获取表信息
ResultSet tablesResultSet = databaseMetaData.getTables(null, null, "employee", new String[]{"TABLE"});
if (tablesResultSet.next()) {
String tableName = tablesResultSet.getString("TABLE_NAME");
String tableComment = tablesResultSet.getString("REMARKS");
System.out.println("表名:" + tableName);
System.out.println("注释:" + tableComment);
// 获取字段信息
ResultSet columnsResultSet = databaseMetaData.getColumns(null, null, tableName, null);
while (columnsResultSet.next()) {
String columnName = columnsResultSet.getString("COLUMN_NAME");
String columnType = columnsResultSet.getString("TYPE_NAME");
String columnComment = columnsResultSet.getString("REMARKS");
boolean isNullable = (columnsResultSet.getInt("NULLABLE") == 1);
String columnDefault = columnsResultSet.getString("COLUMN_DEF");
System.out.println("字段名:" + columnName);
System.out.println("类型:" + columnType);
System.out.println("注释:" + columnComment);
System.out.println("是否允许为空:" + isNullable);
System.out.println("默认值:" + columnDefault);
System.out.println("------------------------");
}
columnsResultSet.close();
}
tablesResultSet.close();
...
# ORM框架
当涉及到与数据库交互的开发任务时,ORM(对象关系映射)框架是非常有用的工具。
ORM框架提供了一种将对象模型与关系型数据库之间进行映射的机制,使开发人员可以使用面向对象的方式进行数据库操作,而不需要直接处理SQL语句和数据库连接。
两个常见的ORM框架是MyBatis和Spring Data JPA。
# 1. MyBatis
MyBatis是一个开源的Java持久层框架,它允许开发人员通过XML配置或注解来定义SQL语句和数据库映射关系。以下是MyBatis的一些关键特点和优势:
- 灵活性:MyBatis提供了完全的SQL控制权,允许开发人员编写原生SQL语句,并提供了丰富的映射机制,支持将查询结果映射到Java对象。
- 易于集成:MyBatis可以轻松集成到现有的Java应用程序中,不需要依赖复杂的容器或框架。
- 缓存支持:MyBatis提供了缓存机制,可以提高查询性能,减少对数据库的访问。
- 动态SQL:MyBatis支持动态SQL语句的构建,可以根据条件生成不同的SQL语句。
# 2. Spring Data JPA
Spring Data JPA是Spring框架提供的一个子项目,它简化了使用JPA(Java持久化API)进行数据访问的开发任务。以下是Spring Data JPA的一些关键特点和优势:
- 简化数据访问:Spring Data JPA通过提供通用的CRUD(创建、读取、更新、删除)操作方法,大大简化了数据访问层的开发。
- 自动化查询生成:Spring Data JPA根据方法命名约定和查询注解自动生成查询语句,无需手动编写SQL语句。
- 强大的查询功能:Spring Data JPA提供了丰富的查询功能,包括动态查询、分页和排序等。
- 集成Spring生态系统:Spring Data JPA与Spring框架及其他Spring项目(如Spring Boot)紧密集成,可以方便地使用Spring的特性和功能。
如果倾向于对SQL语句和映射具有更高的控制权,或者需要执行复杂的SQL查询和定制化的数据库操作,那么MyBatis可能是一个更适合的选择。
MyBatis允许您直接编写SQL语句,并提供了灵活的映射机制,可以将查询结果映射到自定义的Java对象上。您可以通过XML配置或注解定义SQL语句和映射关系,同时还可以使用动态SQL构建器来处理复杂的查询逻辑。MyBatis还提供了缓存机制,可以提高查询性能,减少对数据库的访问。
另一方面,如果希望简化数据访问层的开发,使用更高级的抽象和自动化的功能来处理常见的数据库操作,那么Spring Data JPA可能更适合。
Spring Data JPA提供了一组通用的CRUD方法,通过方法命名约定和查询注解,可以自动生成SQL查询语句,无需手动编写大量的SQL语句。它还提供了丰富的查询功能,包括动态查询、分页和排序等。
# 性能优化技巧
# 1. SQL优化
- 使用索引:确保查询条件中的列有合适的索引
- **避免SELECT ***:只查询需要的列
- 使用LIMIT:限制返回结果集大小
- 批量操作:使用批处理减少网络往返
# 2. JDBC优化
// 批量插入优化示例
public void batchInsert(List<Employee> employees) {
String sql = "INSERT INTO employees (name, age, salary) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
conn.setAutoCommit(false); // 关闭自动提交
for (Employee emp : employees) {
pstmt.setString(1, emp.getName());
pstmt.setInt(2, emp.getAge());
pstmt.setDouble(3, emp.getSalary());
pstmt.addBatch();
// 每1000条执行一次
if (employees.indexOf(emp) % 1000 == 0) {
pstmt.executeBatch();
pstmt.clearBatch();
}
}
pstmt.executeBatch(); // 执行剩余的
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
# 3. 连接优化
- 重用PreparedStatement:在循环中复用
- 设置合适的FetchSize:控制每次从数据库获取的行数
- 使用连接池:避免频繁创建连接
// 设置FetchSize优化大结果集查询
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setFetchSize(1000); // 每次获取1000行
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
// 处理数据
}
}
}
# 常见问题及解决方案
# 1. 连接泄漏
问题:连接未正确关闭导致连接池耗尽 解决:
- 始终使用
try-with-resources
- 设置连接池的
leakDetectionThreshold
参数 - 使用连接池的监控功能
# 2. 中文乱码
问题:存储或读取中文时出现乱码 解决:
// URL中指定字符编码
String url = "jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8";
# 3. 时区问题
问题:时间类型数据不正确 解决:
// URL中指定时区
String url = "jdbc:mysql://localhost:3306/db?serverTimezone=Asia/Shanghai";
# 4. SSL警告
问题:MySQL 5.7+默认要求SSL连接 解决:
// 开发环境可以禁用SSL(生产环境建议启用)
String url = "jdbc:mysql://localhost:3306/db?useSSL=false";
# 5. 大数据处理
问题:处理大量数据时内存溢出 解决:
// 使用流式查询
Statement stmt = conn.createStatement(
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY
);
stmt.setFetchSize(Integer.MIN_VALUE); // MySQL流式查询
# 6. 死锁处理
问题:多个事务相互等待导致死锁 解决:
- 保持事务简短
- 按相同顺序访问表
- 使用合适的隔离级别
- 添加重试机制
public void executeWithRetry(Runnable operation, int maxRetries) {
int retries = 0;
while (retries < maxRetries) {
try {
operation.run();
return;
} catch (SQLException e) {
if (e.getErrorCode() == 1213 && retries < maxRetries - 1) { // MySQL死锁错误码
retries++;
try {
Thread.sleep(100 * retries); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
} else {
throw new RuntimeException(e);
}
}
}
}
# 总结
本文中,我们深入学习了Java数据库连接(JDBC)技术,包括:
- 基础知识:JDBC架构、版本演进、驱动类型
- 核心操作:使用
try-with-resources
进行CRUD操作 - 高级特性:事务管理、批处理、连接池
- 性能优化:SQL优化、批量操作、连接管理
- 问题解决:常见问题的诊断和解决方案
JDBC是Java与关系型数据库之间进行通信和交互的标准API。通过学习和掌握JDBC,我们可以实现Java应用程序与各种关系型数据库的无缝集成。
同时,ORM框架作为JDBC的上层框架,提供了更丰富的功能,更便捷的开发体验,可以根据具体需求、团队技能和项目背景来进行合适的选择。
祝你变得更强!