1. JDBC外键处理的核心原理与实现
在Java数据库编程中,外键约束是维护数据完整性的重要机制。通过JDBC操作外键时,我们需要理解数据库层面的约束机制和JDBC API的配合方式。
1.1 外键约束的数据库实现原理
关系型数据库通过外键约束实现表间的引用完整性。当我们在子表中定义外键时,数据库引擎会自动创建以下约束规则:
- 插入检查:子表插入记录时,外键值必须在主表存在
- 更新检查:主表主键更新时,根据规则级联更新或拒绝操作
- 删除检查:主表记录删除时,根据规则级联删除或设置NULL
MySQL的InnoDB引擎通过以下数据结构实现外键:
- 数据字典:存储约束定义
- 锁机制:处理并发操作
- 引用计数器:跟踪依赖关系
1.2 JDBC中外键操作的三种模式
通过JDBC操作外键时,我们通常采用以下模式:
模式1:显式事务控制
java复制Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
// 先插入主表记录
PreparedStatement masterStmt = conn.prepareStatement(
"INSERT INTO orders(order_id) VALUES (?)");
masterStmt.setInt(1, orderId);
masterStmt.executeUpdate();
// 再插入子表记录
PreparedStatement detailStmt = conn.prepareStatement(
"INSERT INTO order_items(order_id, product_id) VALUES (?, ?)");
detailStmt.setInt(1, orderId);
detailStmt.setInt(2, productId);
detailStmt.executeUpdate();
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
conn.close();
}
模式2:批处理优化
java复制Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO order_items(order_id, product_id) VALUES (?, ?)");
for (OrderItem item : items) {
stmt.setInt(1, item.getOrderId());
stmt.setInt(2, item.getProductId());
stmt.addBatch();
}
stmt.executeBatch();
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.close();
}
模式3:级联操作配置
sql复制-- 建表时指定级联删除
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT,
product_id INT,
FOREIGN KEY (order_id)
REFERENCES orders(id)
ON DELETE CASCADE
);
1.3 外键操作的最佳实践
- 索引优化:确保外键列都有索引
- 批量处理:使用addBatch()减少网络往返
- 错误处理:捕获SQLIntegrityConstraintViolationException
- 连接池配置:合理设置隔离级别
- 延迟检查:考虑DEFERRABLE约束
重要提示:生产环境应避免在应用层完全依赖外键约束,建议在业务逻辑层增加校验
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JDBC时间处理的深度解析
Java时间API与数据库时间类型的映射是JDBC编程中的常见痛点。我们需要理解各时间类型的特性和转换规则。
2.1 时间类型映射矩阵
| Java类型 | JDBC类型 | SQL标准类型 | 范围 | 精度 |
|---|---|---|---|---|
| java.sql.Date | DATE | DATE | 日期 | 天 |
| java.sql.Time | TIME | TIME | 时间 | 毫秒 |
| java.sql.Timestamp | TIMESTAMP | TIMESTAMP | 日期+时间 | 纳秒 |
| java.time.LocalDate | DATE | DATE | 日期 | 天 |
| java.time.LocalTime | TIME | TIME | 时间 | 纳秒 |
| java.time.LocalDateTime | TIMESTAMP | TIMESTAMP | 日期+时间 | 纳秒 |
| java.time.Instant | TIMESTAMP | TIMESTAMP | 时间戳 | 纳秒 |
2.2 时区处理方案
方案1:统一使用UTC
java复制// 存储时转换为UTC
Instant utcInstant = Instant.now();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO events(event_time) VALUES (?)");
stmt.setObject(1, utcInstant);
// 读取时转换为本地时区
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Instant dbInstant = rs.getObject("event_time", Instant.class);
ZonedDateTime localTime = dbInstant.atZone(ZoneId.systemDefault());
}
方案2:数据库时区配置
sql复制-- MySQL时区设置
SET GLOBAL time_zone = '+8:00';
SET SESSION time_zone = '+8:00';
方案3:应用层转换
java复制// 使用Joda-Time或java.time进行转换
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
2.3 时间处理性能优化
- 批量处理时间数据
java复制PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO sensor_data(record_time, value) VALUES (?, ?)");
for (SensorData data : dataList) {
stmt.setTimestamp(1, Timestamp.from(data.getTime()));
stmt.setDouble(2, data.getValue());
stmt.addBatch();
}
stmt.executeBatch();
- 时间范围查询优化
sql复制-- 为时间列创建函数索引
CREATE INDEX idx_log_created_month ON logs(EXTRACT(MONTH FROM created_at));
-- 使用分区表按时间范围分区
CREATE TABLE sensor_data (
id BIGINT,
record_time TIMESTAMP,
value DOUBLE
) PARTITION BY RANGE (UNIX_TIMESTAMP(record_time)) (
PARTITION p202301 VALUES LESS THAN (UNIX_TIMESTAMP('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (UNIX_TIMESTAMP('2023-03-01'))
);
- 缓存时间转换结果
java复制// 使用DateTimeFormatter线程安全实例
private static final DateTimeFormatter CACHED_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatTimestamp(Timestamp timestamp) {
return CACHED_FORMATTER.format(timestamp.toLocalDateTime());
}
3. 面向对象设计在JDBC中的实践
将JDBC操作封装为面向对象的形式,可以提高代码的可维护性和复用性。
3.1 DAO模式实现
基础DAO接口
java复制public interface BaseDAO<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
ID save(T entity);
void update(T entity);
void delete(ID id);
}
具体实现示例
java复制public class UserDAO implements BaseDAO<User, Long> {
private final DataSource dataSource;
public UserDAO(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Optional<User> findById(Long id) {
String sql = "SELECT * FROM users WHERE user_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return Optional.of(mapRowToUser(rs));
}
return Optional.empty();
} catch (SQLException e) {
throw new DataAccessException(e);
}
}
private User mapRowToUser(ResultSet rs) throws SQLException {
return new User(
rs.getLong("user_id"),
rs.getString("username"),
rs.getTimestamp("created_at").toInstant()
);
}
}
3.2 实体关系映射策略
1:1关系映射
java复制public class UserProfileDAO {
public Optional<UserProfile> findByUserId(Long userId) {
String sql = "SELECT * FROM user_profiles WHERE user_id = ?";
// 实现类似findById
}
public void save(UserProfile profile) {
String sql = "INSERT INTO user_profiles(user_id, avatar) VALUES (?, ?)";
// 实现保存逻辑
}
}
1:N关系映射
java复制public class OrderDAO {
public List<OrderItem> findItemsByOrderId(Long orderId) {
String sql = "SELECT * FROM order_items WHERE order_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setLong(1, orderId);
ResultSet rs = stmt.executeQuery();
List<OrderItem> items = new ArrayList<>();
while (rs.next()) {
items.add(mapRowToOrderItem(rs));
}
return items;
} catch (SQLException e) {
throw new DataAccessException(e);
}
}
}
N:M关系映射
java复制public class UserRoleDAO {
public void assignRole(Long userId, Long roleId) {
String sql = "INSERT INTO user_roles(user_id, role_id) VALUES (?, ?)";
// 实现关联关系保存
}
public List<Role> findRolesByUserId(Long userId) {
String sql = "SELECT r.* FROM roles r " +
"JOIN user_roles ur ON r.role_id = ur.role_id " +
"WHERE ur.user_id = ?";
// 实现关联查询
}
}
3.3 事务管理的高级用法
声明式事务模板
java复制public class TransactionTemplate {
private final DataSource dataSource;
public <T> T execute(TransactionCallback<T> action) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
T result = action.doInTransaction(conn);
conn.commit();
return result;
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw new DataAccessException(e);
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
}
}
// 使用示例
transactionTemplate.execute(conn -> {
OrderDAO orderDAO = new OrderDAO(conn);
OrderItemDAO itemDAO = new OrderItemDAO(conn);
Long orderId = orderDAO.save(order);
for (OrderItem item : order.getItems()) {
item.setOrderId(orderId);
itemDAO.save(item);
}
return orderId;
});
保存点(Savepoint)使用
java复制public void complexOperation() {
Connection conn = dataSource.getConnection();
Savepoint savepoint = null;
try {
conn.setAutoCommit(false);
// 第一步操作
step1(conn);
savepoint = conn.setSavepoint("STEP1_COMPLETE");
// 第二步操作
step2(conn);
conn.commit();
} catch (SQLException e) {
if (savepoint != null) {
conn.rollback(savepoint);
// 尝试恢复操作
recoveryStep(conn);
conn.commit();
} else {
conn.rollback();
}
} finally {
conn.close();
}
}
4. 性能优化与异常处理
JDBC操作的性能问题和异常处理是实际开发中的重点难点。
4.1 连接池配置要点
HikariCP推荐配置
java复制HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setLeakDetectionThreshold(30000);
config.setPoolName("MyAppPool");
// 特定于MySQL的优化
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.addDataSourceProperty("useServerPrepStmts", "true");
DataSource dataSource = new HikariDataSource(config);
连接池监控指标
- 活跃连接数
- 空闲连接数
- 等待获取连接的线程数
- 连接获取平均时间
- 连接泄漏检测
4.2 SQL异常分类处理
异常处理策略表
| 异常类型 | 原因 | 处理策略 |
|---|---|---|
| SQLSyntaxErrorException | SQL语法错误 | 记录日志,返回用户友好提示 |
| SQLIntegrityConstraintViolationException | 违反唯一/外键约束 | 业务逻辑处理或提示用户 |
| SQLTimeoutException | 查询超时 | 重试或降级处理 |
| SQLTransientConnectionException | 临时连接问题 | 重试机制 |
| SQLNonTransientConnectionException | 永久连接问题 | 报警并人工干预 |
| BatchUpdateException | 批量操作部分失败 | 事务回滚或部分补偿 |
异常处理示例
java复制try {
// JDBC操作
} catch (SQLIntegrityConstraintViolationException e) {
if (e.getMessage().contains("foreign key")) {
throw new BusinessException("关联数据不存在", e);
} else if (e.getMessage().contains("unique")) {
throw new BusinessException("数据已存在", e);
}
throw new DataAccessException(e);
} catch (SQLTimeoutException e) {
if (retryCount < MAX_RETRY) {
retryCount++;
Thread.sleep(100 * retryCount);
continue;
}
throw new DataAccessException("操作超时,请稍后重试", e);
} catch (SQLException e) {
throw new DataAccessException(e);
}
4.3 性能监控与调优
JDBC性能指标采集
java复制public class JdbcMonitor {
private static final Map<String, AtomicLong> metricMap = new ConcurrentHashMap<>();
public static void recordQuery(String sql, long duration) {
metricMap.computeIfAbsent(sql, k -> new AtomicLong())
.addAndGet(duration);
}
public static void printStats() {
metricMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(10)
.forEach(entry -> {
System.out.printf("SQL: %s%nTotal Time: %dms%nAvg Time: %.2fms%n%n",
entry.getKey(),
entry.getValue().get(),
(double)entry.getValue().get() /
metricMap.get(entry.getKey()).get());
});
}
}
// 使用AOP或拦截器记录执行时间
@Around("execution(* com..*DAO.*(..))")
public Object monitorJdbc(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
String sql = // 获取执行的SQL
JdbcMonitor.recordQuery(sql, duration);
}
}
慢查询优化方案
- 添加合适的索引
- 重写复杂查询
- 使用分页查询
- 引入缓存层
- 考虑读写分离
- 使用物化视图
- 优化连接查询
5. 现代Java生态中的JDBC演进
随着Java生态的发展,JDBC也在不断演进以适应新的需求。
5.1 JDBC与Java时间API的整合
java.time与JDBC 4.2的完美配合
java复制// 写入时间数据
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO events(event_name, event_time) VALUES (?, ?)");
stmt.setString(1, "Product Launch");
stmt.setObject(2, LocalDateTime.now()); // 直接使用java.time类型
// 读取时间数据
ResultSet rs = stmt.executeQuery("SELECT event_time FROM events");
while (rs.next()) {
LocalDateTime eventTime = rs.getObject("event_time", LocalDateTime.class);
// 处理时间数据
}
时区敏感处理方案
java复制// 存储带时区的时间
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO global_events(event_time) VALUES (?)");
stmt.setObject(1, zonedDateTime);
// 读取时转换为本地时区
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
ZonedDateTime dbTime = rs.getObject("event_time", ZonedDateTime.class);
ZonedDateTime localTime = dbTime.withZoneSameInstant(ZoneId.systemDefault());
}
5.2 响应式JDBC实践
使用R2DBC实现响应式编程
java复制// 响应式查询示例
public Flux<User> findAllUsers() {
return connectionFactory.create()
.flatMapMany(conn -> conn.createStatement("SELECT * FROM users")
.execute())
.flatMap(result -> result.map((row, meta) ->
new User(
row.get("user_id", Long.class),
row.get("username", String.class),
row.get("created_at", LocalDateTime.class)
)));
}
// 响应式事务
public Mono<Void> transferBalance(Long from, Long to, BigDecimal amount) {
return Mono.usingWhen(
connectionFactory.create(),
connection -> Mono.from(connection.beginTransaction())
.then(Mono.from(connection.createStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?")
.bind(0, amount)
.bind(1, from)
.execute()))
.then(Mono.from(connection.createStatement(
"UPDATE accounts SET balance = balance + ? WHERE id = ?")
.bind(0, amount)
.bind(1, to)
.execute()))
.then(Mono.from(connection.commitTransaction())),
Connection::close);
}
5.3 JDBC与微服务架构的融合
多数据源配置方案
java复制@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("app.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
// 使用注解切换数据源
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceSelector {
String value() default "primary";
}
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(selector)")
public void before(DataSourceSelector selector) {
DataSourceContextHolder.set(selector.value());
}
@After("@annotation(selector)")
public void after(DataSourceSelector selector) {
DataSourceContextHolder.clear();
}
}
分库分表实践
java复制// 使用ShardingSphere-JDBC配置分片规则
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds0
username: root
password:
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1
username: root
password:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..15}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: orders_$->{order_id % 16}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 2}
6. 实战:电商订单系统的JDBC实现
通过一个完整的电商订单系统案例,展示JDBC外键和时间处理的实际应用。
6.1 数据库设计
表结构设计
sql复制CREATE TABLE users (
user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE products (
product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
CREATE TABLE order_items (
item_id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
6.2 核心业务实现
下单业务逻辑
java复制public class OrderService {
private final DataSource dataSource;
public OrderService(DataSource dataSource) {
this.dataSource = dataSource;
}
public long placeOrder(OrderRequest request) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 1. 检查商品库存
checkStock(conn, request.getItems());
// 2. 创建订单主表记录
long orderId = createOrder(conn, request.getUserId());
// 3. 创建订单明细
createOrderItems(conn, orderId, request.getItems());
// 4. 扣减库存
deductStock(conn, request.getItems());
conn.commit();
return orderId;
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw new OrderException("下单失败", e);
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
}
private void checkStock(Connection conn, List<OrderItemRequest> items) throws SQLException {
String sql = "SELECT product_id, stock FROM products WHERE product_id = ? FOR UPDATE";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (OrderItemRequest item : items) {
stmt.setLong(1, item.getProductId());
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
throw new OrderException("商品不存在: " + item.getProductId());
}
int stock = rs.getInt("stock");
if (stock < item.getQuantity()) {
throw new OrderException("库存不足: " + item.getProductId());
}
}
}
}
private long createOrder(Connection conn, long userId) throws SQLException {
String sql = "INSERT INTO orders(user_id, status) VALUES (?, 'CREATED')";
try (PreparedStatement stmt = conn.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setLong(1, userId);
stmt.executeUpdate();
ResultSet rs = stmt.getGeneratedKeys();
if (rs.next()) {
return rs.getLong(1);
}
throw new OrderException("无法获取订单ID");
}
}
private void createOrderItems(Connection conn, long orderId,
List<OrderItemRequest> items) throws SQLException {
String sql = "INSERT INTO order_items(order_id, product_id, quantity, unit_price) " +
"VALUES (?, ?, ?, ?)";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (OrderItemRequest item : items) {
stmt.setLong(1, orderId);
stmt.setLong(2, item.getProductId());
stmt.setInt(3, item.getQuantity());
stmt.setBigDecimal(4, getProductPrice(conn, item.getProductId()));
stmt.addBatch();
}
stmt.executeBatch();
}
}
private void deductStock(Connection conn, List<OrderItemRequest> items) throws SQLException {
String sql = "UPDATE products SET stock = stock - ? WHERE product_id = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (OrderItemRequest item : items) {
stmt.setInt(1, item.getQuantity());
stmt.setLong(2, item.getProductId());
stmt.addBatch();
}
stmt.executeBatch();
}
}
}
6.3 订单查询优化
分页查询实现
java复制public Page<OrderDTO> findUserOrders(long userId, int page, int size) {
String countSql = "SELECT COUNT(*) FROM orders WHERE user_id = ?";
String dataSql = "SELECT o.*, " +
"(SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = o.order_id) AS total_amount " +
"FROM orders o WHERE user_id = ? ORDER BY order_time DESC LIMIT ? OFFSET ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement countStmt = conn.prepareStatement(countSql);
PreparedStatement dataStmt = conn.prepareStatement(dataSql)) {
// 查询总数
countStmt.setLong(1, userId);
ResultSet countRs = countStmt.executeQuery();
countRs.next();
long total = countRs.getLong(1);
// 查询数据
dataStmt.setLong(1, userId);
dataStmt.setInt(2, size);
dataStmt.setInt(3, (page - 1) * size);
ResultSet dataRs = dataStmt.executeQuery();
List<OrderDTO> orders = new ArrayList<>();
while (dataRs.next()) {
orders.add(mapToOrderDTO(dataRs));
}
return new Page<>(orders, page, size, total);
} catch (SQLException e) {
throw new DataAccessException(e);
}
}
时间范围查询优化
java复制public List<OrderStats> getOrderStats(LocalDate startDate, LocalDate endDate) {
String sql = "SELECT DATE(order_time) AS day, COUNT(*) AS order_count, " +
"SUM((SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = o.order_id)) AS total_amount " +
"FROM orders o " +
"WHERE order_time BETWEEN ? AND ? " +
"GROUP BY DATE(order_time) " +
"ORDER BY day";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, startDate.atStartOfDay());
stmt.setObject(2, endDate.plusDays(1).atStartOfDay());
ResultSet rs = stmt.executeQuery();
List<OrderStats> stats = new ArrayList<>();
while (rs.next()) {
stats.add(new OrderStats(
rs.getDate("day").toLocalDate(),
rs.getInt("order_count"),
rs.getBigDecimal("total_amount")
));
}
return stats;
} catch (SQLException e) {
throw new DataAccessException(e);
}
}
7. 测试与调试技巧
完善的测试策略是保证JDBC代码质量的关键。
7.1 单元测试方案
使用H2内存数据库测试
java复制public class UserDAOTest {
private DataSource dataSource;
private UserDAO userDAO;
@BeforeEach
void setUp() throws SQLException {
// 初始化H2内存数据库
dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("test-data.sql")
.build();
userDAO = new UserDAO(dataSource);
}
@Test
void shouldFindUserById() {
Optional<User> user = userDAO.findById(1L);
assertTrue(user.isPresent());
assertEquals("testuser", user.get().getUsername());
}
@AfterEach
void tearDown() {
((EmbeddedDatabase) dataSource).shutdown();
}
}
测试事务回滚
java复制@SpringBootTest
@Transactional
public class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldRollbackWhenStockInsufficient() {
OrderRequest request = new OrderRequest();
request.setUserId(1L);
request.setItems(List.of(
new OrderItemRequest(1L, 100) // 设置超过库存的数量
));
assertThrows(OrderException.class, () -> orderService.placeOrder(request));
// 验证库存未扣减
// 验证订单未创建
}
}
7.2 集成测试策略
Testcontainers实现真实数据库测试
java复制@Testcontainers
public class ProductDAOIntegrationTest {
@Container
private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
private ProductDAO productDAO;
@BeforeAll
static void beforeAll() {
// 初始化数据库结构
// 插入测试数据
}
@BeforeEach
void setUp() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(mysql.getJdbcUrl());
config.setUsername(mysql.getUsername());
config.setPassword(mysql.getPassword());
productDAO = new ProductDAO(new HikariDataSource(config));
}
@Test
void shouldUpdateProductStock() {
long productId = 1L;
int originalStock = productDAO.findById(productId).get().getStock();
productDAO.updateStock(productId, -1);
assertEquals(originalStock - 1, productDAO.findById(productId).get().getStock());
}
}
7.3 性能测试方法
JMeter JDBC测试计划配置
- 添加JDBC连接配置
- 设置连接池参数
- 添加JDBC请求采样器
- 配置参数化查询
- 添加监听器收集结果
性能测试关键指标
- 吞吐量(Transactions/sec)
- 平均响应时间
- 错误率
- 90/95/99百分位响应时间
- 数据库连接池使用情况
8. 扩展与进阶方向
JDBC技术的深入应用还有更多值得探索的方向。
8.1 JDBC与NoSQL的融合
MongoDB JDBC驱动使用
java复制// 配置MongoJDBC连接
String url = "jdbc:mongo://localhost:27017/mydb";
Properties props = new Properties();
props.setProperty("user", "username");
props.setProperty("password", "password");
try (Connection conn = DriverManager.getConnection(url, props);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT * FROM users WHERE age > 25")) {
while (rs.next()) {
// 处理结果
}
}
Elasticsearch JDBC查询
java复制// 配置Elasticsearch JDBC连接
String url = "jdbc:elasticsearch://localhost:9300/clusterName";
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement stmt = conn.prepareStatement(
"SELECT name, price FROM products WHERE price > ?")) {
stmt.setInt(1, 100);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
// 处理结果
}
}
8.2 JDBC元数据高级应用
数据库结构探查
java复制public void inspectDatabase(Connection conn) throws SQLException {
DatabaseMetaData meta = conn.getMetaData();
// 获取所有表
ResultSet tables = meta.getTables(null, null, "%", new String[]{"TABLE"});
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
System.out.println("Table: " + tableName);
// 获取表列信息
ResultSet columns = meta.getColumns(null, null, tableName, "%");
while (columns.next()) {
System.out.printf(" Column: %s %s%n",
columns.getString("COLUMN_NAME"),
columns.getString("TYPE_NAME"));
}
}
}
外键关系分析
java复制public void analyzeForeignKeys(Connection conn, String tableName) throws SQLException {
DatabaseMetaData meta = conn.getMetaData();
// 获取导入的外键
ResultSet importedKeys = meta.getImportedKeys(null, null, tableName);
while (importedKeys.next()) {
System.out.printf("FK: %s.%s -> %s.%s%n",
importedKeys.getString("PKTABLE_NAME"),
importedKeys.getString("PKCOLUMN_NAME"),
importedKeys.getString("FKTABLE_NAME"),
importedKeys.getString("FKCOLUMN_NAME"));
}
// 获取导出的外键
ResultSet exportedKeys = meta.getExportedKeys(null, null, tableName);
while (exportedKeys.next()) {
System.out.printf("Referenced by: %s.%s%n",
exportedKeys.getString("FKTABLE_NAME"),
exportedKeys.getString("FKCOLUMN_NAME"));
}
}
8.3 JDBC与大数据生态
使用JDBC连接Spark SQL
java复制// 配置Spark JDBC连接
String url = "jdbc:hive2://spark-master:10000/default";
Properties props = new Properties();
props.setProperty("user", "spark");
props.setProperty("password", "password");
try (Connection conn = DriverManager.getConnection(url, props);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT department, AVG(salary) FROM employees GROUP BY department")) {
while (rs.next()) {
System.out.printf("%s: %.2f%n",
rs.getString(1),
rs.getDouble(2));
}
}
Flink JDBC连接器使用
java复制// 配置Flink JDBC Source
JdbcSource.<User>builder()
.setDrivername("com.mysql.jdbc.Driver")
.setDBUrl("jdbc:mysql://localhost:3306/mydb")
.setUsername("user")
.setPassword("password")
.setQuery("SELECT * FROM users WHERE updated_at > ?")
.setParameterProvider(() -> new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setTimestamp(1, Timestamp.from(lastUpdateTime));
}
})
