子集型回溯(2026-01-06)

子集型回溯应用范围:子集/组合/切分字符串/按位选择等“枚举所有可能”的题。

基础题目

  • 子集
  • 电话号码字母组合
  • 分割回文串

扩展题目

  • 二叉树的所有路径
  • 路径总和 II
  • 字母大小写全排列

1. 什么是回溯?

以集合 (1,2,3) 的子集为例:我们可以选择 1,也可以“退回去”不选 1 改选 2,再选 3
这种在构造答案的过程中回退到上一步、去探索其它分支的现象就是回溯。

回溯通常伴随递归,而不是循环:

  • 循环适合固定层数(例如 2 层、3 层嵌套)。
  • 但回溯题的“层数”往往未知或很深(例如字符串切分、树路径、排列组合),循环表达能力有限。
  • 递归天然对应一棵“决策树”的深度优先遍历(DFS),更适合回溯。

2. 用“树”和“路径”理解回溯

把回溯理解成 DFS 遍历一棵决策树最直观:

  • 每走到一个节点,我们都处于一条“根 → 当前节点”的路径上
  • path 维护这条路径
  • 当到达满足条件的位置(通常是叶子,或某些题的节点),把 path 记录到 ans

关键点:回溯 = 递归返回 + 恢复现场


3. 恢复现场:为什么必须做?

在 DFS 过程中我们经常会:

  1. 把一个选择加入 path
  2. 递归深入
  3. 返回时必须撤销这个选择,否则 path 会“污染”后续分支

常见的两种恢复方式:

  • 数据覆盖:适用于“固定长度答案”(常用数组保存)
  • 回滚(removeLast):适用于 List / StringBuilder 这种动态结构

add → dfs → removeLast
只要你“改变了状态”,回来的时候就要“撤销状态”。


4. 子集型回溯的两大思路

回溯题通常两种建模方式:

思路 A:从输入角度——“当前元素选不选”

  • 每个输入元素对应一层决策:选 / 不选
  • 决策树通常是二叉树
  • 答案通常在叶子节点收集

思路 B:从答案角度——“当前位置选哪个元素”

  • 每一层表示“答案的当前位选什么元素”
  • startIndex 控制下一层枚举起点,避免重复
  • 答案通常在节点收集(因为走到任意节点都代表一个合法子集)

两种思路都能做 78 子集;
哪个更顺手取决于题型,有时“答案角度”的树更小,有时“选不选”更直观。


5. 画树:回溯卡住时最有效的解法

当你对“递归参数是什么、何时收集答案、怎么剪枝”不明确时:

  • 先画出决策树
  • 每一层代表什么?
  • 每条边代表什么选择?
  • 哪些节点是合法答案?

画树能直接定位:答案在哪里收集 + 哪一步需要回溯。


6. 结合题目:78 子集(两种思路)

6.1 解法 1:枚举输入元素选不选(叶子收集答案)

特点:二叉决策树;当 i == nums.length 才表示一条路径完全确定,因此在叶子收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void dfs(int i, int[] nums, List<Integer> path, List<List<Integer>> ans) {
// 递归边界:输入枚举完了,path 是一种完整选择
if (i == nums.length) {
ans.add(new ArrayList<>(path)); // 注意要拷贝
return;
}

// 1) 不选 nums[i]
dfs(i + 1, nums, path, ans);

// 2) 选 nums[i]
path.add(nums[i]);
dfs(i + 1, nums, path, ans);
path.removeLast(); // 恢复现场
}

注意:

  • 叶子收集:因为只有到 i==n 才能确定“每个元素选没选”
  • 必须拷贝 path:否则 ans 里存的是同一个引用,后续回滚会影响已保存答案
  • 恢复现场:每次 add 后都要 removeLast

6.2 解法 2:枚举第 i 个答案选哪个元素(节点收集答案)

特点:每个节点都是一个子集,因此走到节点就可记录;用 startIndex 防止重复。

1
2
3
4
5
6
7
8
9
10
11
void dfs(int start, int[] nums, List<List<Integer>> ans, List<Integer> path) {
// 由于每个节点都是一个合法子集,所以直接记录当前 path
ans.add(new ArrayList<>(path));

// 枚举下一位选哪个元素:只能从 start 往后选,保证不出现 (2,1) 这种重复顺序
for (int j = start; j < nums.length; j++) {
path.add(nums[j]);
dfs(j + 1, nums, ans, path);
path.removeLast(); // 回滚
}
}

你需要牢记的点:

  • 节点收集:因为任何时刻的 path 都是一个合法子集
  • startIndex 是去重本质:规定递增选取顺序,避免同一组合的不同排列
  • 这类写法也是“组合类题”通用模板(如组合总和、组合数等)

7. String.join:在回溯里怎么用?和 StringBuilder 有什么关系?

在题解中看到有人用了 String.join(),它很适合把结果“输出成字符串”:

1
2
List<String> path = List.of("a", "b", "c");
String s = String.join("-", path); // "a-b-c"

但需要注意它和 StringBuilder / StringJoiner 的定位不同:

  • String.join:一次性把已有的字符串集合拼起来(更像“格式化输出”)
  • StringBuilder:回溯过程中不断 append / delete(更像“构造过程中的状态”)
  • StringJoiner:更偏“带前后缀的 join”,比如 "[a,b,c]"

在回溯题中一般推荐:

  • 构造过程:用 StringBuilder(append + deleteCharAt 回滚)
  • 输出阶段:需要展示路径时用 String.join(更清爽)

8. 总结

模板 1(选不选,叶子收集)

  • 参数:i
  • 收集:i == n
  • 结构:先不选,再选(选完要回滚)

模板 2(答案角度,节点收集)

  • 参数:start
  • 收集:进入 dfs 就收集
  • 结构:for 从 start 枚举,递归 dfs(start +1),回滚

9. 心得体会

  • 回溯的本质是DFS 遍历决策树path 记录当前路径,ans 收集答案。
  • 回溯一定伴随恢复现场:add → dfs → removeLast。
  • 子集型题最常用两种建模:
    • 选不选(叶子收集)
    • 选哪个(节点收集 + startIndex 去重)
  • 思路不清楚时,画树是最高效的破局方法
  • String.join 更适合输出拼接;回溯构造过程更推荐 StringBuilder 做状态并回滚。

相关代码

本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。

Spring + Redis 学习计划 V3.1(7天闭环,基于AI)

这篇文章不是 Spring 或 Redis 的教程,而是我在学习过程中逐步形成的一套“以实践触发问题、以问题反推原理”的学习方法论记录。然后和 AI 讨论,让 AI 帮我制定并反复修改的一份学习计划。

这份计划并不是一成不变的清单,而是一个可以根据个人基础和时间不断调整的框架。

如果你打算使用这份计划,建议每天只关注一个问题,不要试图一次性学完所有原理;当你能结合代码回答当天的核心问题时,就可以进入下一天。

每天固定产出:代码(可复现)+ 触发问题(至少1个)+ 博客(面试答案)
Spring 的 6 问法继续用;Redis 也用同样的“是否学够了”问法。


Day 1:IOC(不该用 Spring 的场景)

主问题(Spring 6问 #1):我什么时候不该用 Spring IOC?

练习:纯 Java new vs Spring 管理 Bean(最小项目)
触发问题:工具类要不要交给 Spring?过度工程化的代价是什么?
博客:《我什么时候不该用 Spring?IOC 的边界》

原理后置:Bean 生命周期细节、三级缓存(先不看)


Day 2:DI(注入失败与选择规则)

主问题(Spring 6问 #3):DI 为什么会失败?

练习:多实现类 + @Autowired 不加 @Qualifier,制造 NoUnique
触发问题:Spring 如何选择 Bean?为什么字段注入更“坑”?
博客:《@Autowired 注入失败的真实原因与修复姿势》

原理后置:装配算法细节(按需)


Day 3:AOP(为什么会失效)

主问题(Spring 6问 #3/#4):AOP 在哪些情况下会失效?是框架问题还是使用问题?

练习:private / 本类自调用 / final 类方法(任选两种复现)
触发问题:Spring 拦截的是谁?为什么必须经过代理?
博客:《AOP 失效:你以为拦截了方法,其实你绕过了代理》

原理后置:JDK vs CGLIB(只记结论即可)


Day 4:事务(事务不是关键字)

主问题(Spring 6问 #3/#6):事务为什么失效?事务边界在哪里?

练习:@Transactional + try-catch + 自调用(至少复现一个失效)
触发问题:为什么 catch 了异常不回滚?为什么自调用不生效?
博客:《@Transactional 失效三件套:自调用 / 私有方法 / try-catch》

原理后置:传播行为枚举(后置到面试前)


Day 5:Spring Boot(减法实验:自动配置到底做了啥)

主问题(Spring 6问 #2):Boot 帮我解决了什么?又引入了什么新复杂度?

练习:搭 Boot + MVC + MyBatis + JUnit,刻意删配置观察还能跑什么
触发问题:Tomcat 谁起的?Mapper 谁扫的?
博客:《我删了配置还能跑:Spring Boot 自动配置的边界》

原理后置:自动配置源码(先别啃)


Day 6:Redis 进场(Cache Aside + 一致性“必踩坑”)

从今天开始,Redis 用同样的“6问法”(尤其是:何时不用、不可避免的不一致、补救策略)

主问题(Redis 自测 #2/#3):Redis 与数据库不一致我能不能接受?延迟双删在解决什么?

练习(强工程化)

  • 先写 无缓存:Controller → Service → MyBatis → DB
  • 再加 旁路缓存 Cache Aside
    • 读:先查 Redis,miss 查 DB 回填
    • 写:更新 DB + 删除缓存(或先删后更),两种都做
  • 刻意制造并发读写(哪怕用两个线程/两次请求)

必须触发的问题(至少一个)

  • 更新 DB 后,读到 Redis 旧值:这允许吗?怎么兜底?
  • 先删缓存还是先更新 DB?各自会出什么问题?
  • 延迟双删的意义是什么?它是在补什么洞?

博客(面试级)
《Cache Aside 不是“套路”:我如何复现并解释一次读到旧值》

原理后置:Redis 持久化/集群(先不学)


Day 7:Redis × 事务 × AOP(把 Spring 的问题“放大”)

主问题(Spring 6问 #6 + Redis 自测 #4):Redis 操作失败,事务该不该回滚?边界在哪里?

练习(触发更高级问题)

  • 写一个“下单/扣库存”伪业务:
    • DB:扣库存/写订单(事务内)
    • Redis:删缓存/更新缓存(事务内外各做一版)
  • 人为制造 Redis 异常(比如断开连接/抛 RuntimeException)

必须触发的问题

  • DB 回滚了,Redis 没回滚怎么办?
  • Redis 写成功了,DB 失败了怎么办?
  • 缓存更新到底该放在事务前、事务中、事务后?

博客(面试级)
《Redis 进事务是灾难吗?我用一次“回滚不一致”把边界讲清楚》

原理后置(可选进阶):

  • Lua 原子性(如果你想走并发方向,可以作为加分项)

二叉树的层序遍历总结(2026-01-05)

在二叉树相关算法中,层序遍历(Level Order Traversal) 是 BFS 的典型应用,在涉及「按层处理」「同一层节点关系」「层级统计」等问题时,几乎是第一选择。同时,通过层序遍历序列构造一颗树很好实现。

本文对我在 2026-01-05 完成的一批层序遍历题目进行一次系统总结,并结合做题过程中补充的 Java 集合知识点,形成一份可复盘的学习笔记。

基础题目

  • 二叉树的层序遍历
  • 二叉树的锯齿形层序遍历
  • 找树左下角的值

扩展题目

  • 二叉树的层序遍历 II
  • 二叉树的最大深度
  • 二叉树的最小深度
  • 二叉树中的第 K 大层和
  • 二叉树的右视图
  • 填充每个节点的下一个右侧节点指针
  • 层数最深叶子节点的和
  • 奇偶树
  • 反转二叉树的奇数层
  • 二叉树的堂兄弟节点 II

层序遍历的核心识别信号

在读题时,只要出现以下关键词,就要高度警惕「层序遍历」:

  • “一层一层”
  • “同一层节点”
  • “第 k 层 / 第 k 大层”
  • “左视图 / 右视图”
  • “最深一层 / 最浅一层”

本质:题目要求你按层组织节点,而不是单纯的前序 / 中序 / 后序。


层序遍历的两种经典实现方式

1. 双数组(当前层 / 下一层)写法(最直观)

思路非常清晰:

  1. 用一个数组保存当前层节点
  2. 遍历当前层时,把子节点加入“下一层数组”
  3. 当前层处理完后,令 cur = next
1
2
3
4
5
6
7
8
cur = [root]
while cur 非空:
处理 cur
next = []
for node in cur:
next.add(node.left)
next.add(node.right)
cur = next

优点:

  • 思路清楚,适合初学
  • 非常适合“同层对称处理”的题目(如 2415)

队列写法(更通用、面试更常见)

常用的写法:

核心技巧:
在每一轮循环开始时,记录当前队列长度 size,它就代表当前层的节点个数

1
2
3
4
5
6
7
8
queue.offer(root)
while queue 非空:
size = queue.size()
for i in range(size):
node = queue.poll()
处理 node
queue.offer(node.left)
queue.offer(node.right)

优点:

  • 不需要额外的“当前层数组”
  • 非常适合统计、聚合、视图类问题

一道典型 Trick:2415. 反转二叉树的奇数层

这道题有一个非常重要但容易忽略的点:

题目要求反转的是“奇数层节点的值”,而不是节点本身。

关键 insight

  • 二叉树结构 不能乱改
  • 但交换 node.val 是完全合法的
  • 对称位置的节点只需要交换值即可

这是一个**“值操作”替代“结构操作”**的经典思路,后续很多树题都会用到。


做题过程中补充的 Java 集合知识

在实现层序遍历的过程中,我顺带系统补齐了一些 Java 集合的易混点

1. List.of(...) vs new ArrayList<>()

  • List.of(...)
    • 创建 不可变 List
    • 不能增删改
    • 不允许 null
    • 引用本身可以重新指向其他 List
  • new ArrayList<>()
    • 可变集合
    • 支持增删改

常见用法:

1
List<Integer> a = new ArrayList<>(List.of(1, 2, 3));

2. Collections.reverse(list) vs list.reversed()

  • Collections.reverse(list)
    • 就地反转
    • 会修改原 list
    • list 必须是可变的
  • list.reversed()(Java 21+)
    • 返回一个 倒序视图(view)
    • 不修改原 list
    • O(1) 创建视图
    • 原 list 改变,视图会联动变化

3. Queue.offer / poll vs add / remove

这是 Queue 接口里非常重要的一组 API 设计差异:

  • offer(e):失败返回 false
  • poll():空队列返回 null
  • add(e):失败抛异常
  • remove():空队列抛异常

实际写算法时,offer + poll 更常用,避免异常作为控制流。


总结

总体来看,层序遍历的思想并不复杂,但由于它需要我们显式维护一个队列,通过不断入队、出队来模拟递归过程,因此:

  • 代码相对 DFS 更冗长
  • 但在“同层处理”“层级统计”“视图类问题”中几乎不可替代

当题目强调“层”的概念时,应优先考虑层序遍历;而在实现过程中,对 Java 集合 API 的理解是否扎实,往往直接决定代码是否简洁、健壮

构造本地测试用例:用层序数组构造二叉树

在刷 LeetCode/写本地 main 测试时,经常会遇到题目给出的输入形式是:

  • root = [5,8,9,2,1,3,7,4,6]
  • 或包含缺失节点:root = [1,2,3,null,4]

这种数组表示其实就是层序遍历(level order)的序列化结果
数组从左到右依次给出每一层的节点,null 表示该位置没有节点。

为了让本地调试更高效,我补齐了一个通用的建树方法:
输入 Integer[] levelOrder,输出 TreeNode root(支持 null)

核心思路

  • 用队列保存“等待挂孩子的父节点”
  • 指针 i 从 1 开始扫描数组
  • 每次从队列弹出一个父节点,尝试读取两个位置作为它的 left/right
  • 读到 null 就跳过,不创建节点

Java 建树模板(支持 null)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static TreeNode buildTree(Integer[] levelOrder) {
if (levelOrder == null || levelOrder.length == 0 || levelOrder[0] == null) return null;

TreeNode root = new TreeNode(levelOrder[0]);
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);

int i = 1;
while (i < levelOrder.length && !q.isEmpty()) {
TreeNode cur = q.poll();

// left
if (i < levelOrder.length && levelOrder[i] != null) {
cur.left = new TreeNode(levelOrder[i]);
q.offer(cur.left);
}
i++;

// right
if (i < levelOrder.length && levelOrder[i] != null) {
cur.right = new TreeNode(levelOrder[i]);
q.offer(cur.right);
}
i++;
}
return root;
}

相关代码

本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。

二叉树的最近公共祖先(LCA)问题总结

本文围绕三道经典 LCA 题目:
236. 二叉树的最近公共祖先
235. 二叉搜索树的最近公共祖先
1123. 所有最深叶节点的最近公共祖先
系统总结最近公共祖先解题思路


一、什么是最近公共祖先(LCA)

最近公共祖先(Lowest Common Ancestor) 指的是:

在一棵树中,两个节点 pq 的所有公共祖先中,离它们最近的那个节点

直观做题时,我们通常是:

  1. 找到 pq
  2. 从它们向上回溯
  3. 第一个相交的节点就是答案

而在代码中,这个“向上回溯”的过程,天然适合用 递归 来表达。


二、236. 二叉树的最近公共祖先(母模板)

一句话思路

在左右子树中分别查找 pq
如果左右都找到了,当前节点就是最近公共祖先


递归分类讨论

对于当前节点 root

  1. root == null

    • 返回 null(没找到)
  2. root == proot == q

    • 直接返回当前节点
    • 因为最近公共祖先不可能在它的子树下面
  3. 递归左右子树

    1
    2
    left = LCA(root.left)
    right = LCA(root.right)
    • left != null && right != null
      pq 分别在左右子树中,root 就是 LCA
    • 只有一边非空:表示 q、p 都在非空边,直接返回非空节点,即是答案

本质理解

  • 递(向下):定位 pq
  • 归(向上):判断当前节点是否“第一次同时覆盖目标”

这是后面所有 LCA 题目的核心模板


三、235. 二叉搜索树的最近公共祖先(BST 优化)

与 236 的关键区别

235 给的是 二叉搜索树(BST),多了一个重要信息:

左子树所有值 < 根节点 < 右子树所有值


利用 BST 性质剪枝

设当前节点为 root

  1. root.val > p.val && root.val > q.val
    两个节点一定在 左子树
  2. root.val < p.val && root.val < q.val
    两个节点一定在 右子树
  3. 否则
    一个在左,一个在右(或 root 本身)
    当前节点就是最近公共祖先

小结

题目 是否需要遍历整棵树 关键依据
236 普通二叉树
235 BST 有序性

235 是 236 在 BST 场景下的剪枝优化版


四、1123. 所有最深叶节点的最近公共祖先(难点)

初始直觉(两次遍历)

我的第一反应是:

  1. 先遍历整棵树,找到所有最深的叶子节点
  2. 再利用 236 的思路,求这些节点的最近公共祖先

这个思路完全正确,但本质是 两次遍历,并不优雅。


思路优化

在查看参考答案后,我意识到:

  • 如果最深叶节点只存在于左子树:最近公共祖先一定在左子树
  • 如果左右子树都存在最深叶节点,且深度相同:当前节点才可能成为答案

这说明:

答案一在递归过程中就可以产生的


五、两种递归设计思路

思路一:自顶向下(参数传递)

  • 向下递归时传递当前深度
  • 使用全局变量维护最大深度
  • 当左右子树深度相等,且等于全局最大深度时,更新答案(这里的答案会变,因为随着遍历最大深度会变)

思路二(推荐):自底向上(返回值回溯)

进一步抽象问题后,可以把 1123 完全转化为一个分治问题

每棵子树只需要向父节点返回两件事:

  1. 子树的最大深度
  2. 子树中最深叶节点的最近公共祖先

六、自底向上的核心合并逻辑(重点)

设当前节点 root

  • 左子树返回 (depthL, lcaL)
  • 右子树返回 (depthR, lcaR)

返回值表示当前节点为根的树的深度与最深叶子节点的最近公共祖先节点

情况 1:左子树更深

1
depthL > depthR

所有最深叶节点都在左子树

最近公共祖先也一定在左子树

返回 (depthL + 1, lcaL)


情况 2:左右子树深度相等

1
depthL == depthR
  • 最深叶节点分布在左右子树
  • 当前节点是第一次“同时覆盖”它们的节点
  • 当前节点就是最近公共祖先
  • 返回 (depthL + 1, root)

七、为什么要用 Pair / 二元组,而不是 Map?

在 1123 的自底向上实现中,递归函数需要返回:

  • 一个 int(深度)
  • 一个 TreeNode(最近公共祖先)

这两个值:

  • 没有 key → value 的映射关系
  • 有固定顺序和明确语义

因此它们本质上是一个 有位置关系的二元组

1
(depth, lca)

使用 Pair(或自定义类)比 Map 更贴合问题本质,也更清晰、简洁。


八、从 236 到 1123:思路的统一

可以这样理解:

  • 236:判断左右子树是否分别找到了 pq
  • 1123:判断左右子树是否分别包含“最深叶节点”

本质上,1123 是对 236 思路的自然推广

把“是否找到目标节点”升级成了“子树是否达到最深深度”


九、递归设计的两大核心原则(重要总结)

向下传递 —— 参数传递法(递)

适用场景

  • 深度
  • 路径状态
  • 累计信息

特点

  • 信息从父节点流向子节点
  • 子节点无法反向影响父节点

向上返回 —— 返回值回溯法(归)

适用场景

  • 子树高度
  • 子树统计信息
  • 最近公共祖先

特点

  • 信息从子节点汇总到父节点
  • 父节点基于左右子树返回值做决策

十、总结

  • 236:最近公共祖先的母模板,理解递归回溯的经典题
  • 235:利用 BST 性质进行剪枝优化
  • 1123:递归设计能力的分水岭,考验信息如何在递归中流动

二叉树问题的核心,不是“怎么写递归”,
而是“你希望子树返回什么信息”

一旦子问题定义清晰,答案就会在回溯过程中自然浮现。

相关代码

本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。

MyBatis XML 映射器学习复盘:结合实践 Bug

在学习 MyBatis XML 映射器之前,我以为这部分内容只是“把 SQL 写进 XML”。
但真正动手后,我发现:

我遇到的大多数问题,都不是 SQL 写错,而是「我并不知道 MyBatis 在什么时候、用什么规则去解析这些 XML」。

这篇文章并不是官方文档的复述,而是我在学习过程中真实遇到的问题、修复的 Bug,以及最终形成理解总结。


Bug :为什么必须用 @Param,不然 XML 对不上?

现象

1
2
query(String name, String status);
#{name} // 报错

原因

  • 方法传入的形参名不保存,MyBatis 默认参数名为 param1 / param2

正确做法

1
2
query(@Param("name") String name,
@Param("status") String status);

最终结论

多参数方法,一律使用 @Param,及规范又易读


XML 映射器整体结构认知

一个 SQL 映射文件中,真正重要的顶级元素并不多(按推荐顺序):

  • resultMap:结果集到对象的映射规则(最强大、最复杂)
  • sql:可复用的 SQL 片段
  • select
  • insert
  • update
  • delete

核心思想只有一个:

Mapper XML 的职责是:定义 SQL + 描述 SQL 执行结果如何映射成 Java 对象。


select:最常用,也最容易被“想简单”的地方

一个最基本的查询如下:

1
2
3
<select id="selectById" resultType="com.tingfeng.mybatis.pojo.User">
select * from tb_user where id = #{id}
</select>

#{} 的本质理解

#{id} 并不是字符串拼接,而是 预编译参数占位符,底层等价于 JDBC:

1
2
3
String sql = "select * from tb_user where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, id);

结论

  • #{} 安全(防 SQL 注入)
  • #{} 会使用 PreparedStatement

select 的核心属性

属性 工程级理解
parameterType 可省略,MyBatis 可通过参数类型推断
resultType 适合字段名与属性名一致的简单映射
resultMap 字段名不一致 / 复杂映射时使用(二选一)
statementType 默认 PREPARED,几乎不用改
resultOrdered 针对嵌套结果集的内存优化(进阶)

resultType 和 resultMap 永远只能选一个


insert / update / delete:关注事务与主键

修改语句的共同点

  • insert / update / delete 都会修改数据库
  • 必须提交事务sqlSession.commit()

这是我忽略了很多次的一点。


useGeneratedKeys:主键回填

1
2
3
4
5
6
<insert id="insertUser"
useGeneratedKeys="true"
keyProperty="id">
insert into tb_user(name, phone)
values(#{name}, #{phone})
</insert>

执行完成后:

1
user.getId(); // 已自动回填

最常用的主键获取方式


selectKey:数据库不支持自增时的备用方案

  • BEFORE:先生成 key,再 insert
  • AFTER:先 insert,再查询 key(类似 Oracle)

sql 标签:SQL 复用的本质是字符串拼接

1
2
3
4
5
6
7
8
9
10
<sql id="userColumns">
${alias}.id, ${alias}.username
</sql>
<select id="selectUsers" resultType="map">
select
<include refid="userColumns">
<property name="alias" value="t1"/>
</include>
from tb_user t1
</select>

关键理解

  • <sql> 本质是 字符串模板
  • 所以只能使用 ${}(字符串替换)
  • 不能用于接收用户输入

结论

  • ${} 只用于 SQL 结构
  • #{} 只用于业务参数

参数传递机制:最容易踩坑,踩坑后要通过 sql 日志来分析

一个关键前提

Mapper 方法的形参名默认不会被保留下来!

编译后,参数名变成:

1
param1 / param2 / arg0 / arg1

单参数为什么“随便写都能用”

1
2
User selectUser(int id);
where id = #{id}

因为 只有一个参数,不存在歧义


多参数的真实规则

1
User select(String name, String status);

错误写法:

1
where name = #{name} and status = #{status}

正确写法(不推荐):

1
where name = #{param1} and status = #{param2}

推荐写法(易读,明确):

1
2
3
User select(@Param("name") String name,
@Param("status") String status);
where name = #{name} and status = #{status}

foreach 中 collection 的真相

1
2
int batchInsert(@Param("users") List<User> users);
<foreach collection="users" item="user">

collection 指的是映射过程中的“参数名”,不是 Java 变量名


结果映射:自动映射 vs resultMap

自动映射的前提

  • 列名 == 属性名(忽略大小写)
  • 或开启下划线转驼峰:
1
<setting name="mapUnderscoreToCamelCase" value="true"/>

resultMap 的使用场景

  • 列名与属性名完全不一致
  • 多表查询(列名冲突)
  • 嵌套对象 / 集合映射
1
2
3
4
<resultMap id="userMap" type="User">
<id column="user_id" property="id"/>
<result column="user_name" property="username"/>
</resultMap>

<id><result> 的区别在于:
id 会被标记为对象标识,对缓存和嵌套映射有优化作用


自动映射的工作机制

  • 自动映射先执行
  • 手动 resultMap 后执行(可覆盖)

自动映射等级:

  • NONE
  • PARTIAL(默认)
  • FULL

总结

动态 SQL / XML 映射的实践原则

  1. 参数永远用 #{},结构才用 ${}
  2. 多参数一律使用 @Param
  3. 字段不一致优先用别名,其次 resultMap
  4. insert/update/delete 记得提交事务
  5. 通过 SQL 日志反推问题,而不是盯 XML 发呆
  6. MyBatis 并不“智能”,它只是严格执行规则

写在最后

通过这一阶段对 XML 映射器 的系统学习,我最大的收获不是记住了多少标签,而是:

真正理解了 MyBatis 是如何把 SQL、参数和 Java 对象串联起来的

回头看这些 Bug,我发现它们有一个共同点:

不是我不会写 SQL,而是我不知道 MyBatis 在“什么时候、用什么身份”去解析这些 XML。

当我开始用“框架视角”理解 MyBatis,而不是“语法视角”,这些问题才真正消失。


学习资料与完整代码

  • XML 映射器理论学习笔记
  • 可运行的 MyBatis Demo

已整理并上传至 GitHub 仓库(含 README 说明)

leetcode 通过先序 / 中序 / 后序遍历构造二叉树(

刷题范围

基础题目

98.验证二叉搜索树

扩展题目

  • 二叉搜索树中的众数
  • 二叉搜索树中第 K 小的元素
  • 二叉搜索子树的最大键值和
  • 从前序与中序遍历序列构造二叉树(重点)
  • 从中序与后序遍历序列构造二叉树
  • 根据前序和后序遍历构造二叉树
  • 删点成林

这些题目从 BST 的性质利用,逐步过渡到 通过遍历序列反向构造树结构,对理解递归非常有帮助。


BST 性质在遍历中的典型应用

501. 二叉搜索树中的众数

最直观的想法

  • HashMap 统计每个值出现次数
  • 再遍历 map 找最大值

但这种方式 没有利用 BST 的任何性质


利用 BST 的优化思路

BST 的一个关键性质是:

中序遍历结果是一个非递减(有序)序列

因此:

  • 相同值一定是 连续出现的
  • 众数一定出现在连续相同节点中

实现思路

  • 使用中序遍历
  • 维护:
    • pre:前一个节点值
    • cnt:当前值连续出现次数
    • maxCnt:历史最大频次
    • ans:答案集合

关键细节

  • 当前值 == pre → cnt++
  • 否则 → cnt = 1
  • cnt == maxCnt → 加入答案
  • cnt > maxCnt清空答案 + 更新 maxCnt

这一步让我意识到:题目给出二叉搜索树,大概率要用到中序遍历。


230. 二叉搜索树中第 K 小的元素

这是一个非常“教科书级”的题目:

  • BST 中序遍历 = 递增序列
  • 第 K 小元素 = 中序遍历第 K 个访问的节点

直接中序遍历即可,不需要额外结构。


1373. 二叉搜索子树的最大键值和

这道题开始明显体现“遍历方式选择”的重要性。

问题拆解

  • 需要判断:

    以当前节点为根的子树是否是 BST

  • 如果是:

    • 计算其键值和
    • 与全局最大值比较

为什么用后序遍历?

  • 判断 BST 需要 左右子树的信息
  • 键值和也是 由左右子树向上汇总

这是一个典型的 自底向上问题,非常适合后序遍历。

需要返回的信息

  • 子树最小值 / 最大值
  • 子树键值和

特殊情况

  • 节点值为负数时,允许返回 0,因为空树也是 BST,其“最大和”为 0

通过遍历序列构造二叉树(核心部分)

105. 从前序与中序遍历序列构造二叉树(重点)

这是今天收获最大的题目之一。

一开始的困难

  • 手写在纸上非常直观
  • 但转成代码时容易“无从下手”

核心思路(和手写过程完全一致)

  1. 前序遍历的第一个元素一定是根节点
  2. 在中序遍历中找到该根节点的位置
  3. 中序中:
    • 左边部分 → 左子树
    • 右边部分 → 右子树
  4. 对左右子数组递归执行同样的过程

本质是:不断确定根节点,然后把问题缩小到左右子数组


优化点

  • HashMap 预处理中序遍历中: “节点值 → 下标”
  • 将查找根节点位置从 O(n) 降为 O(1)

106. 从中序与后序遍历序列构造二叉树

这道题与 105 非常类似:

  • 后序遍历的 最后一个元素是根节点
  • 在中序遍历中划分左右子树
  • 递归构造

进一步加深了我对: “遍历序列 + 根节点定位 + 子数组递归” 这一套路的理解。


889. 根据前序和后序遍历构造二叉树

这道题的变化点在于:

  • 已知根节点
  • 无法唯一确定左右子树的边界

题目允许:

返回任意一种满足条件的二叉树


关键约定思路

  • 若当前节点不是叶子节点
  • 则:
    • 前序序列中下一个节点(index + 1)
    • 一定可以视为左子树的根

基于这个约定:

  • 在后序中找到该左子树根的位置
  • 确定左子树大小
  • 再递归构造左右子树

这道题让我意识到: **不是所有构造题都要求“唯一解”,要学会读题,**自己给出一些不影响答案的假设。


1110. 删点成林

这是一道遍历顺序选择非常典型的题目。

为什么必须用后序遍历?

  • 如果用先序:
    • 当前节点删掉后
    • 无法再访问左右子树
  • 如果用中序:
    • 右子树访问会受影响

后序遍历

  • 先处理左右子树
  • 再决定是否删除当前节点
  • 若删除:将左右子树加入结果集

今日学习总结(方法论层面)

今天的题目虽然多,但核心收获非常集中:

  1. BST 题目,优先考虑中序遍历
  2. 构造二叉树的题目,本质是:
    • 找根
    • 划分左右子树
    • 在子数组中递归
  3. 后序遍历适合:
    • 需要子树返回信息
    • 删除 / 汇总 / 判断类问题
  4. 写递归时要学会:
    • 抓大放小
    • 先想整体递归关系
    • 再补边界条件与细节
  5. 如果题目已经限定“二叉搜索树”,一定要考虑用 BST 的性质,否则条件就显得多余

今天让我最深刻的一点是:

很多二叉树难题,并不是逻辑复杂,而是“把纸上推演的过程,忠实地翻译成递归代码”。

只要:

  • 根节点是谁
  • 什么时候递
  • 什么时候归
  • 边界条件是什么

想清楚了,代码自然就出来了。

相关代码

本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。

MyBatis 动态 SQL 五个 Level 实战复盘:从语法理解到踩坑总结

在学习 MyBatis 动态 SQL 的过程中,我没有只停留在“看文档 + 记语法”,而是通过 5 个循序渐进的练习 Level,把 if / where / trim / choose / foreach 等标签真正用到了代码中。

这篇文章不是对官方文档的复述,而是对 实际编写动态 SQL 时遇到的问题、报错和解决方案的系统复盘


一、为什么要写这篇文章?

MyBatis 动态 SQL 看起来很简单:

1
<if>、<where>、<trim>、<foreach>

但真正写起来,会频繁遇到:

  • SQL 语法错误
  • 查不到数据但不报错
  • 参数传了却没生效
  • AND / WHERE / , 到底谁该写谁不该写
  • <foreach>collection 到底写什么

这些问题,光看文档是很难意识到的,必须通过实战踩坑。


二、动态 SQL 常用标签速览(不展开)

本文默认你已经了解以下标签的基本用法:

  • <if>
  • <where>
  • <trim>
  • <choose / when / otherwise>
  • <foreach>
  • <set>

完整的理论学习笔记 + 示例代码我已整理在 GitHub 仓库中,本文重点放在「实践中遇到的问题」。


Exercise 1:<if> + WHERE —— SQL 语法错误的第一个坑

问题现象

执行查询时报错:

1
You have an error in your SQL syntax

日志中看到实际 SQL:

1
2
3
4
select * from tb_user
WHERE name = ?,
and gender = ?,
and status = ?

错误原因

  • , 当成条件连接符(这是 INSERT / UPDATE 才用的)
  • 手写 WHERE,又在 <if> 中手写 AND
  • 导致 SQL 结构非法

正确写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="queryUser" resultType="User">
select * from tb_user
<where>
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>

经验总结

条件查询不要手写 WHERE,让 <where> 帮你管理 AND

可以先写一条正常的 sql,那这道题举例:

​ select * from tb_user where name = ? and gender = ? and status = ?;

这样对照这条 sql 去写动态 sql 会比较不容易出错一点


Exercise 2:<where> vs <trim> —— 前缀修剪机制

疑问

1
<trim prefix="WHERE" prefixOverrides="AND |OR ">
  • prefixOverrides 是干什么的?
  • 为什么后面有空格?

正确理解

MyBatis 是字符串拼接 SQL

1
2
3
4
5
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">
AND name = #{name}
</if>
</trim>

执行逻辑是:

  1. 先拼出:AND name = ?
  2. 删除前缀 AND
  3. 再加上 WHERE

经验总结

<where> 本质是 MyBatis 封装好的 <trim>


Exercise 3:数据库 char(1) × Java char —— 查不到数据但不报错

问题现象

  • SQL 正常执行
  • Total: 0
  • 数据库中明明存在数据却查不到

日志中曾出现:

1
and gender = '\0'

根因分析(关键坑)

数据库字段:

1
gender char(1)

Java 实体类写成了:

1
private char gender;

但:

  • Java char 默认值是 '\0'
  • JDBC 会绑定成空字符
  • 数据库中不存在这种值

正确映射方式

数据库 char / varchar → Java 用 String,不要用 char

1
2
3
4
private String gender;
private String status;
user.setGender("1");
user.setStatus("0");

经验总结(非常重要)

数据库 char ≠ Java char
Java char 是 Unicode 字符,不是字符串


Exercise 4:<choose> —— 只会执行一个分支

疑问

如果多个条件同时满足,会不会都执行?

实际行为

1
2
3
4
5
6
7
8
9
10
11
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null">
AND author = #{author}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
  • 只执行第一个满足条件的 when
  • 后面的直接忽略

经验总结

<choose> = SQL 版 switch-case


Exercise 5:<foreach> —— collection 到底写什么?

疑问

collection 不是 Java 形参名吗?

易错点

1
int batchInsert(List<User> users);

如果不加 @Param

  • XML 中 不能写 users

  • MyBatis 默认参数名是 list

MyBatis 运行时默认拿不到方法形参名(比如 users),所以它只能用自己生成的默认名字(list / collection / param1…)

因为 Java 编译后的 .class 文件里,默认不会保留形参名(会变成 arg0/arg1

加了 @Param 后,因为 @Param 是你主动给 MyBatis 一个稳定的参数名,它不需要依赖反射去猜

推荐写法(最清晰)

1
2
3
4
int batchInsert(@Param("users") List<User> users);
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.phone})
</foreach>

经验总结

collection = 集合参数名
item = 集合中的单个元素


六、五个 Level 练习后的工程级总结

动态 SQL 编写原则

  1. 条件查询永远用 <where> / <trim>
  2. 更新语句永远用 <set>
  3. 条件连接符只有 and/or,没有 ,
  4. 数据库 char / varchar → Java 用 String
  5. <choose> 只会命中一个分支
  6. <foreach> 建议配合 @Param

七、学习资料与完整代码

  • 动态 SQL 理论学习笔记
  • 五个 Level 的完整练习代码
  • 可运行的 MyBatis Demo

👉 已整理并上传至 GitHub 仓库(含 README 说明)


八、写在最后

通过这五个 Level 的练习,我最大的收获不是“记住了多少标签”,而是:

学会了如何通过 SQL 日志反推 XML 和 Java 代码的问题,然后根据问题动态完善自己的 sql 语句编写

这也是我认为学习 MyBatis 动态 SQL 最重要的能力

leetcode 二叉树的先序 / 中序 / 后序遍历:在二叉搜索树中的选择与应用

刷题范围

基础题目

98.验证二叉搜索树

扩展题目

700.二叉搜索树中的搜索

938.二叉搜索树的范围和

530.二叉搜索树的最小绝对差

2476.二叉搜索树最近节点查询


三种遍历方式的本质差异

在验证二叉搜索树(98 题)时,我分别用 先序 / 中序 / 后序 都实现了一遍,从而对三者有了更直观的理解。

先序遍历(Root → Left → Right)

特点

  • 在“访问当前节点”时,就已经掌握了关键信息
  • 有机会提前返回,不一定遍历完整棵树

直观理解

“我先看看当前节点行不行,再决定要不要往下走”

在某些 BST 题目中,这意味着剪枝能力强、效率高


中序遍历(Left → Root → Right)

特点(BST 专属)

  • 中序遍历结果是一个严格递增的有序序列

直观理解

“我不急着处理当前节点,先把左边都看完,保证顺序”

只要题目和“有序性 / 相邻关系 / 二分”有关,中序遍历往往是第一选择。


后序遍历(Left → Right → Root)

特点

  • 一定会遍历完整棵树
  • 信息在子树返回时才产生
  • 适合“自底向上”的问题

直观理解

“我要先拿到左右子树的结果,才能决定当前节点的结果”

后序遍历是最通用、有返回值的一种方式。


不同题目中遍历方式的选择

理解三种遍历的本质后,再回头看具体题目,会发现“顺序选择”并不是随意的。


700. 二叉搜索树中的搜索 —— 先序遍历更优

核心原因

  • 一旦 root.val == target,可以立刻返回
  • 完全没必要遍历无关子树

先序遍历在这里的优势是: “能提前命中并终止递归”


2️⃣ 938. 二叉搜索树的范围和 —— 先序 + 剪枝思想

题目要求:

计算所有值在 [low, high] 范围内节点的和

关键观察(BST 性质)

  • 当前值 < low → 只可能在右子树
  • 当前值 > high → 只可能在左子树
  • 当前值在区间内 → 左右子树都可能有贡献

这使得解法可以在“访问当前节点”时直接决定递归方向,非常适合 先序遍历 + 提前返回。本质是: 在“递”的阶段就完成剪枝决策


530. 二叉搜索树的最小绝对差 —— 中序遍历

BST 中序遍历是严格递增序列,那么:

  • 最小绝对差
  • 一定只会出现在相邻两个节点之间

因此只需要:

  1. 中序遍历
  2. 记录前一个节点
  3. 比较当前值与前一个值的差

这是一个典型“利用 BST 有序性”的题目


2476. 二叉搜索树最近节点查询 —— 中序 + 二分

这道题让我收获最大的不是遍历本身,而是复杂度分析的思维

两种思路对比

思路一:

  • 每次查询都遍历整棵树
  • 时间复杂度:O(n × q)

思路二:

  1. 中序遍历 BST → 得到有序数组
  2. 对每个 query 在数组中二分查找
  • 时间复杂度:O(n + q log n)

为什么必须选思路二?

根据题目提示:

  • queries.length节点数 n 是同一个数量级

所以:

  • 思路一 ≈ O(n²)(直接超时)
  • 思路二 ≈ O(n log n)(可接受)

结论

当 BST 查询规模大、查询次数多时,一定要先转成有序数组(这种方式逻辑也很清晰,把逻辑都揉在树的遍历过程中十分复杂)


今天的核心学习总结

通过今天这组题目,我对二叉树遍历有了一个更“工程化”的理解:

  1. 遍历顺序的本质是“信息产生的时机”
  2. 先序遍历适合:提前判断、剪枝、搜索类问题
  3. 中序遍历在 BST 中有特性:有序性、相邻关系、二分优化
  4. 后序遍历最通用:自底向上、依赖子树返回值(今天体会不明显)
  5. 在数据规模较大时,复杂度分析往往比写法本身更重要

相关代码

本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。

MyBatis 入门学习笔记:从 JDBC 到 Mapper 映射

一句话导读
MyBatis 的核心价值不在于“帮你写 SQL”,而在于 消除 JDBC 中大量的硬编码问题,让 SQL 与 Java 解耦,同时保持对 SQL 的完全掌控


一、为什么需要 MyBatis?—— 从 JDBC 的痛点说起

在直接使用 JDBC 操作数据库时,很快会遇到以下问题:

SQL 硬编码问题严重

  • SQL 直接写在 Java 代码中
  • 一旦 SQL 发生变化,必须修改并重新编译 Java 代码
  • 实际项目中,SQL 变化非常频繁,维护成本高

参数绑定不灵活

  • 使用 PreparedStatement 需要手动设置占位符
  • where 条件的数量和顺序变化时:
    • SQL 要改
    • Java 代码也要跟着改
  • 可维护性较差

结果集解析硬编码

  • 查询结果需要手动从 ResultSet 中取值
  • 强依赖数据库字段名
  • 一旦 SQL 查询字段变化,解析逻辑也要修改

总结一句话

JDBC 的问题本质是:硬编码太多、关注点不分离、维护成本高。

因此,引入 MyBatis 来解决这些问题。


二、MyBatis 是什么?

MyBatis 是一个持久层(DAO 层)框架,对 JDBC 操作过程进行了封装,使开发者:

  • 只需要关注 SQL 本身
  • 不再关心:
    • 驱动注册
    • Connection 创建
    • Statement / PreparedStatement 创建
    • 参数设置
    • 结果集解析

MyBatis 的核心思想

  • 使用 XML 或注解 描述 SQL
  • 将 SQL 与 Java 对象进行映射
  • 框架负责:
    • 执行 SQL
    • 参数映射
    • 结果集映射
    • 返回 Java 对象

三、MyBatis 整体架构与工作流程

核心配置文件与映射文件

  • mybatis-config.xml

    全局配置文件,配置:

    • 数据源
    • 事务管理器
    • Mapper 映射文件位置
  • mapper.xml

    • SQL 映射文件
    • 一个 SQL 对应一个 MappedStatement

SqlSessionFactory 与 SqlSession

  • SqlSessionFactoryBuilder
    → 构建 SqlSessionFactory
  • SqlSessionFactory
    → 创建 SqlSession
  • 所有数据库操作都通过 SqlSession 完成

Executor(执行器)

MyBatis 底层通过 Executor 接口执行 SQL:

  • SimpleExecutor:基本执行器
  • CachingExecutor:带缓存的执行器

MappedStatement(核心抽象)

  • 每一条 SQL 都会被封装成一个 MappedStatement
  • 包含:
    • SQL 本身
    • 输入参数映射规则
    • 输出结果映射规则

本质类比 JDBC:

  • 参数映射 ≈ PreparedStatement.setXxx
  • 结果映射 ≈ ResultSet 解析

MyBatis 工作流程总结

  1. 构建 SqlSessionFactory
  2. 获取 SqlSession
  3. 获取 Mapper 接口代理对象
  4. 调用接口方法执行 SQL
  5. 提交事务并关闭 Session

四、MyBatis 的核心特性

  • SQL 与 Java 解耦
  • 自动结果映射
  • 强大的动态 SQL 能力
  • 内置缓存机制(一级 / 二级)

五、MyBatis 缓存机制

一级缓存(SqlSession 级别)

  • 默认开启、不可关闭
  • 同一个 SqlSession 中,相同查询只执行一次 SQL

触发清空的情况:

  • 执行 INSERT / UPDATE / DELETE
  • 调用 clearCache()
  • 事务回滚
  • 不同的 StatementId

二级缓存(Mapper 级别)

  • 多个 SqlSession 共享
  • 需要显式开启

全局配置

1
2
3
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

Mapper 配置

1
2
3
4
5
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>

常见缓存策略

策略 说明
LRU 最近最少使用(默认)
FIFO 先进先出
SOFT 软引用
WEAK 弱引用

六、MyBatis 入门实践

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>

构建 SqlSessionFactory(XML)

1
2
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

Mapper XML 示例

1
2
3
4
5
<mapper namespace="UserMapper">
<select id="getUser" resultType="User">
SELECT * FROM tb_user WHERE id = #{id}
</select>
</mapper>

使用 SqlSession 执行 SQL(旧方式)

1
List<User> list = session.selectList("UserMapper.getUser", 10);

问题:

  • namespace, select ID 硬编码
  • 参数类型不安全

Mapper 接口(推荐方式)

1
2
3
4
5
6
public interface UserMapper {
List<User> getUser(int id);
}

UserMapper mapper = session.getMapper(UserMapper.class);
mapper.getUser(10);

底层通过动态代理生成实现类,接口方法 ↔ SQL 映射一一对应。


Mapper 接口与 XML 的目录约定(重要)

  • Mapper 接口在 java 目录
  • Mapper XML 在 resources 目录
  • 包路径保持一致

这是 MyBatis 正确定位 SQL 的关键。


七、注解开发方式

基本示例

1
2
@Select("SELECT * FROM tb_user WHERE id = #{id}")
User getUser(int id);

常用注解

  • @Select / @Insert / @Update / @Delete
  • @Param:多参数映射
  • @Results / @Result:字段映射
1
2
3
4
@Results({
@Result(property = "id", column = "user_id"),
@Result(property = "name", column = "user_name")
})

官方建议

  • 简单 SQL → 注解
  • 复杂 SQL → XML

八、工具类封装 SqlSessionFactory

由于获取连接流程固定,可以封装工具类:

1
2
3
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();

九、总结与个人理解

通过 MyBatis 的学习,我对 Java 数据访问层有了更清晰的认识:

  1. MyBatis 的核心价值是 解耦,而不是隐藏 SQL
  2. 它通过配置与映射,消除了 JDBC 的大量硬编码
  3. Mapper 接口 + 动态代理,是非常优雅的设计
  4. XML 与注解并不是对立关系,而是各司其职

Maven 与 JDBC 学习笔记:从依赖管理到数据库访问实践

一句话导读
本文系统梳理了 Maven 的核心机制(依赖、生命周期、冲突解决)以及 JDBC 的完整使用流程(连接、CRUD、事务、批处理、连接池),重点理解 Java 程序是如何通过 JDBC 与数据库交互的。


一、Maven

1. Maven 是什么?

Maven 是一个 项目构建与依赖管理工具
当项目需要使用第三方库时,不再手动下载 .jar 放进 lib 目录,而是通过 Maven 声明依赖 → 自动下载 → 统一管理


2. Maven 坐标(Dependency Coordinates)

每一个依赖在 Maven 中都由一组唯一坐标标识:

  • groupId:组织或公司名(通常反域名)
  • artifactId:项目/模块名
  • version:版本号

示例(EasyExcel):

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>

常用坐标查询网站:
https://mvnrepository.com/


3. Maven 依赖配置与 scope

依赖的基本结构

1
2
3
4
5
6
7
8
9
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<scope>...</scope>
<exclusions>...</exclusions>
</dependency>
</dependencies>

常见依赖范围(scope)

scope 作用说明
compile 默认值,编译 / 测试 / 运行都生效
test 仅测试阶段使用(如 JUnit)
provided 编译和测试有效,运行时由容器提供
runtime 编译不需要,运行时需要(JDBC 驱动
system 手动指定 jar 路径(不推荐)

实践建议
​ MySQL JDBC 驱动应使用 runtime,避免与 Java 标准 JDBC 接口混淆。


4. 依赖传递性与依赖冲突

4.1 同一 pom 中声明多个版本

  • 后声明的版本生效

4.2 传递依赖冲突的解决规则(Maven 依赖调解)

  1. 路径最短优先
  2. 声明顺序优先

如果自动调解不满足需求,需要使用 <exclusions> 手动排除依赖。

  • 通常优先保留较高版本
  • 若高版本不兼容,考虑升级上层依赖

5. Maven 仓库机制

Maven 仓库类型:

  • 本地仓库:缓存已下载依赖
  • 远程仓库
    • 中央仓库
    • 私服
    • 镜像仓库(如阿里云)

依赖查找顺序:

  1. 本地仓库
  2. 远程仓库
  3. 找不到则报错

6. Maven 生命周期与插件

生命周期

  • clean
  • default
  • site

常用阶段:

  • compile
  • install
  • deploy
1
mvn clean install

插件机制

Maven 本质是 插件执行框架,如:

  • 编译插件
  • 测试插件
  • Jacoco、Checkstyle、Sonar 等

7. Maven 多模块与最佳实践

  • 父模块:统一管理版本、插件
  • 子模块:只引入实际需要的依赖
  • 使用 dependencyManagement 统一版本
  • 将版本号集中在 <properties>

二、JDBC

1. JDBC 是什么?

JDBC(Java Database Connectivity)是 Java 访问数据库的标准接口

  • 接口定义在 Java 标准库中
  • 具体实现由数据库厂商提供(JDBC Driver)
  • Java 程序通过 JDBC 驱动与数据库通信

2. JDBC 连接数据库

JDBC URL 格式(MySQL)

1
jdbc:mysql://host:port/db?param=value

示例:

1
jdbc:mysql://localhost:3306/db01?useSSL=false&characterEncoding=utf8

建立连接示例

1
Connection conn = DriverManager.getConnection(url, user, password);

JDBC 连接是昂贵资源,必须及时释放,推荐使用 try-with-resources


3. JDBC 查询流程(核心)

标准 4 步:

  1. 加载驱动
  2. 获取连接
  3. 使用 PreparedStatement
  4. 使用 ResultSet 读取结果
1
2
3
4
5
6
7
8
9
10
11
12
Class.forName("com.mysql.cj.jdbc.Driver");

try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement ps = conn.prepareStatement("select name, age from tb_user where id <= ?")) {

ps.setObject(1, 20);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name") + " 年龄:" + rs.getInt("age"));
}
}
}

4. PreparedStatement 与 SQL 注入

错误示例(存在 SQL 注入风险):

1
"SELECT * FROM user WHERE name='" + name + "'"

正确方式:

1
2
3
4
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM user WHERE name = ?"
);
ps.setString(1, name);

优势:

  • 防止 SQL 注入
  • 可复用 SQL 执行计划
  • 性能更好

5. JDBC 更新操作

  • INSERT / UPDATE / DELETE
  • 使用 executeUpdate()
  • 返回影响行数

插入并获取自增主键

1
2
3
4
5
6
7
8
9
PreparedStatement ps = conn.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS
);

try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
long id = rs.getLong(1);
}
}

6. JDBC 事务

1
2
3
4
5
6
7
8
9
conn.setAutoCommit(false);
try {
// 多条 SQL
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.close();
}

我的理解 JDBC 事务的本质:

让多条 SQL 在同一个数据库事务中执行


7. JDBC 批处理(Batch)

适用于 同一 SQL,多组参数 的场景:

1
2
ps.addBatch();
ps.executeBatch();
  • 比循环执行效率高
  • 返回 int[] 表示每条语句影响行数

8. JDBC 连接池

频繁创建/关闭连接成本高 → 使用连接池复用连接。

常见连接池实现

  • HikariCP
  • Druid
  • C3P0

HikariCP 示例

1
2
3
4
5
6
7
8
9
10
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/db01");
config.setUsername("root");
config.setPassword("password");

DataSource ds = new HikariDataSource(config);

try (Connection conn = ds.getConnection()) {
...
}

注意:

  • DataSource 应全局唯一
  • conn.close() 并不是真正关闭,而是归还连接池

三、总结与个人理解

通过今天的学习,我对 Java + 数据库 + Maven 的理解更加清晰:

  1. 体会到了 Maven 的便捷,解决了 依赖与构建的工程问题
  2. JDBC 是 Java 与数据库之间的 标准桥梁(java 核心思想:一次编译到处运行的体现)
  3. 事务、批处理、连接池是性能与一致性的关键
  4. 而后续要学习的框架(MyBatis / Spring)本质都是在 JDBC 之上做封装