昨天的JDBC学习让我掌握了基础的CRUD操作,今天的内容则更加深入------动态SQL拼接、结果集自动封装工具类以及多表关联插入。这些知识点已经非常接近企业级开发的实际场景,让我对JDBC的应用有了更完整的认识。
一、动态SQL拼接------应对复杂查询
问题背景
实际开发中,查询条件往往是动态变化的。用户可能只填用户名,也可能同时填用户名、年龄范围、时间范围,甚至什么都不填。如果为每一种组合都写一条SQL,代码会变得极其臃肿。
核心思路:动态拼接WHERE条件
```java
StringBuilder sql = new StringBuilder("SELECT * FROM user");
String username = searchParamVo.getUsername();
Integer minAge = searchParamVo.getMinAge();
String startTime = searchParamVo.getStartTime();boolean hasWhere = false;
if (username != null && !username.isBlank()) {
sql.append(" WHERE username LIKE ?");
hasWhere = true;
}
if (minAge != null) {
if (!hasWhere) {
sql.append(" WHERE ");
hasWhere = true;
}
sql.append(" AND age BETWEEN ? AND ?");
}
// 处理更多条件...
sql.append(" LIMIT ?, ?");
关键技术细节
- WHERE的添加时机
第一个条件前加`WHERE`,后续条件前加`AND`。使用`flag`变量标记是否已添加过WHERE。
- 末尾多余的AND处理
```java
if (hasWhere) {
sql.delete(sql.lastIndexOf("AND"), sql.length());
}
- ?占位符的动态赋值
```java
int count = 1;
if (username条件成立) {
preparedStatement.setString(count++, "%" + username + "%");
}
if (minAge条件成立) {
preparedStatement.setInt(count++, minAge);
preparedStatement.setInt(count++, maxAge);
}
// 最后设置分页参数
preparedStatement.setInt(count++, (page - 1) * pageSize);
preparedStatement.setInt(count, pageSize);
分层设计
将SQL拼接和参数赋值分离到两个独立方法中:
| 方法 | 职责 |
|------|------|
| `appendSql()` | 根据条件动态拼接SQL语句 |
| `setParamValue()` | 按顺序为所有`?`占位符赋值 |
这种设计让代码逻辑清晰,便于维护和调试。
二、ResultSetUtils------反射封装结果集
痛点分析
在昨天的查询代码中,每次从`ResultSet`取值都要手动调用`getInt()`、`getString()`、`getObject()`等,再逐个`set`到对象中,代码重复度极高。
解决方案:反射+工具类
```java
public class ResultSetUtils {
public static <T> T toBean(ResultSet rs, Class<T> clazz) throws Exception {
// 1. 创建对象(调用无参构造)
T bean = clazz.newInstance();
// 2. 获取所有列信息
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
// 3. 获取列名和列值
String columnName = metaData.getColumnName(i);
Object value = rs.getObject(i);
// 4. 列名转驼峰:role_name → roleName
String fieldName = underlineToCamel(columnName);
// 5. 反射调用setter方法
String setterName = "set" + firstCharUpperCase(fieldName);
Method setter = findSetterMethod(clazz, setterName);
if (setter != null) {
setter.invoke(bean, value);
}
}
return bean;
}
}
核心工具方法
- 下划线转驼峰
```java
private static String underlineToCamel(String columnName) {
StringBuilder sb = new StringBuilder();
boolean nextUpper = false;
for (char c : columnName.toCharArray()) {
if (c == '_') {
nextUpper = true;
} else {
sb.append(nextUpper ? Character.toUpperCase(c) : c);
nextUpper = false;
}
}
return sb.toString();
}
- 反射查找setter方法
```java
private static <T> Method findSetterMethod(Class<T> clazz, String setterName) {
for (Method method : clazz.getMethods()) {
if (method.getName().equals(setterName) && method.getParameterCount() == 1) {
return method;
}
}
return null;
}
使用效果
优化前:
```java
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setAge(rs.getByte("age"));
// ... 更多字段
优化后:
```java
User user = ResultSetUtils.toBean(rs, User.class);
一行代码替代了原来的8行,而且适用于所有实体类!这正是框架的雏形------封装重复代码,提取通用逻辑。
三、多表关联插入------事务的雏形
业务需求
新增角色时,需要同时完成两件事:
-
向`role`表插入角色基本信息
-
向中间表`role_menu`插入角色和菜单的关联关系
核心要求
两步操作必须**同时成功或同时失败**。这就是**事务(Transaction)**的雏形。
```java
Connection conn = DBUtil.getConnection();
try {
// 关闭自动提交------开启事务
conn.setAutoCommit(false);
// 1. 插入角色
String roleSql = "INSERT INTO role (role_name, role_desc) VALUES (?, ?)";
// 执行插入...
// 2. 插入角色-菜单关联
String relSql = "INSERT INTO role_menu (rid, mid) VALUES (?, ?)";
// 批量执行多条关联插入...
// 提交事务
conn.commit();
} catch (SQLException e) {
// 回滚事务
conn.rollback();
throw new RuntimeException(e);
} finally {
conn.setAutoCommit(true);
DBUtil.release(conn);
}
关键点:
-
`conn.setAutoCommit(false)`:关闭自动提交,手动控制事务
-
`conn.commit()`:全部成功才提交
-
`conn.rollback()`:任何一步失败则回滚全部
获取自增ID
```sql
-- 插入角色后获取自增的role_id
SELECT LAST_INSERT_ID();
在JDBC中:
```java
preparedStatement.executeUpdate();
ResultSet rs = preparedStatement.getGeneratedKeys();
if (rs.next()) {
int roleId = rs.getInt(1);
}
需要在使用`prepareStatement`时指定返回生成的键:
```java
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
四、今日总结
今天的内容让我从"会用JDBC"进入了"用好JDBC"的阶段:
| 知识点 | 核心要点 |
|--------|----------|
| 动态SQL拼接 | StringBuilder + flag标记 + 末尾清理 |
| 占位符赋值 | 维护count索引,按顺序匹配 |
| 反射封装 | ResultSetMetaData + 反射调用setter |
| 下划线转驼峰 | 遍历字符数组,处理_标志 |
| 事务控制 | setAutoCommit(false) + commit + rollback |
| 获取自增ID | getGeneratedKeys() |
| 批量插入 | 多条SQL在事务中统一执行 |
三点感悟:
-
框架的本质是封装:像`ResultSetUtils`这样的工具类,通过反射+约定(列名与字段名对应),将重复的"结果集→对象"转换封装成通用方法。这就是MyBatis等ORM框架的底层逻辑。
-
动态SQL是后端开发的常态:企业级应用中,查询条件几乎都是动态组合的。学会用Java拼接SQL是必备技能,但拼接时务必使用`PreparedStatement`防止SQL注入。
-
事务是数据一致性的保障:多表操作必须使用事务。`setAutoCommit(false)` + `commit/rollback` 是标准模式,务必在`finally`中恢复自动提交并释放资源。
明天也继续加油。