Mybatis 持久层框架
动态 SQL 与缓存机制¶

MyBatis 的一级缓存和二级缓存有什么区别?¶
很多同学在使用 MyBatis 时能感受到“同一个查询好像不会重复查库”,但细说不清。我们一次性搞明白。
🔎 先看两张缓存的作用域¶
🧠 一级缓存深入¶
在同一个 SqlSession 里,执行 select 时,MyBatis 会先查 LocalCache(一个 PerpetualCache,底层是 HashMap)。如果命中,直接返回,不再查库。
重要特性:
-
在执行
update、delete、insert提交后,或者执行SqlSession.clearCache()后,缓存会被清空。 -
如果多个
SqlSession同时操作同一数据,一级缓存会导致读到旧数据(因为另一个会话修改了数据库,本会话缓存未更新)。所以在并发环境下,不能依赖一级缓存来保证数据一致性。
🗄️ 二级缓存机制与配置¶
二级缓存是跨会话的。当某个 SqlSession 提交时,会将当前 select 的结果存入二级缓存区域;下一个新的 SqlSession 在相同 namespace 下执行相同查询时,就能从二级缓存直接取。
开启方式:
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 在 Mapper.xml 中添加 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
或者在接口上标注:
@CacheNamespace(eviction = LruCache.class, flushInterval = 60000, size = 512, readOnly = true)
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User getUser(int id);
}
常见配置属性:
-
eviction:回收策略LRU/FIFO/SOFT/WEAK。 -
flushInterval:刷新间隔(毫秒),到时间自动清空。 -
size:缓存对象数量上限。 -
readOnly:只读时性能更好,因为对象本身可被共享;读写时需要序列化拷贝。
二级缓存的查询流程:二级缓存 → 一级缓存 → 数据库。如果二级缓存命中,直接返回,不走一级缓存。
⚠️ 避坑指南¶
-
不要在分布式环境直接用内置缓存:默认二级缓存基于本地内存,多实例间数据不一致。务必换成 Redis、Hazelcast 等分布式缓存实现(如 mybatis-redis 适配)。
-
只有单表查询的 Mapper 才适合开二级缓存:如果 Mapper 中涉及复杂关联查询,可能由于一条关联更新导致该 namespace 整个缓存刷新,得不偿失。
-
一级缓存的“坑”:在 Spring 集成时,每次查询默认用新的
SqlSession(通过SqlSessionTemplate),所以一级缓存基本失效(每次查询都是新会话)。这就是为什么你在 Service 层调用两次mapper.getUser(1),会执行两次 SQL 的原因——Spring 中的一级缓存几乎是不起作用的。
📌 一句话总结:一级缓存是“单次会话的临时便签”,二级缓存是“跨会话的全局布告栏”。选型时,根据是否跨会话、是否分布式、数据一致性要求来抉择,别为了“缓存”而缓存。
MyBatis 的插件(Interceptor)机制是如何实现的?¶
MyBatis 插件让你可以插手 SQL 执行的各个环节,比如分页、慢 SQL 监控、数据脱敏。它的核心是责任链模式 + 动态代理,我们手动拆开看看。
⚙️ 工作原理四步走¶
-
可拦截的四大对象:MyBatis 允许拦截
Executor(执行器)、ParameterHandler(参数处理)、ResultSetHandler(结果集处理)、StatementHandler(SQL 语句处理)。 -
插件签名声明:每个插件必须用
@Intercepts+@Signature声明要拦截哪个类的哪个方法,以及参数类型。 -
生成代理对象:MyBatis 在创建这四大核心对象时,会调用
InterceptorChain.pluginAll(target),内部遍历所有插件,对目标对象应用Plugin.wrap(),最终返回一个层层包裹的 JDK 动态代理。 -
执行链路:调用目标方法时,会先进入插件代理的
invoke方法,在里面可以修改参数、执行额外逻辑,最后调用真实对象的方法。
🔨 写一个记录慢 SQL 的插件¶
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SlowSqlInterceptor implements Interceptor {
// 慢查询阈值,单位毫秒
private long threshold;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 StatementHandler,拿到绑定的 SQL
StatementHandler handler = (StatementHandler) invocation.getTarget();
String sql = handler.getBoundSql().getSql();
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 执行原始方法
long time = System.currentTimeMillis() - start;
if (time > threshold) {
System.err.println("慢查询 [" + time + "ms]: " + sql);
}
return result;
}
// 可以在配置时设置阈值
@Override
public void setProperties(Properties properties) {
this.threshold = Long.parseLong(properties.getProperty("threshold", "1000"));
}
}
注册到 MyBatis:
<plugins>
<plugin interceptor="com.example.SlowSqlInterceptor">
<property name="threshold" value="500"/>
</plugin>
</plugins>
🧪 责任链如何串联¶
来看 Plugin 的核心代理逻辑:
// 简化版 Plugin.invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果当前调用的方法匹配我们声明要拦截的方法,就交给插件的 intercept 方法
if (methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
// 否则直接走原始对象的方法
return method.invoke(target, args);
}
如果有多个插件,依次包裹:PluginA 代理 PluginB 代理 PluginC 代理 原始对象。调用时顺序为 A.intercept → B.intercept → C.intercept → 真实方法。这就是典型的责任链。
💡 实用场景扩展¶
-
分页插件(如 PageHelper)就是拦截
Executor.query,判断参数中是否需要分页,修改 SQL 并重新设置参数。 -
读写分离插件:拦截
Executor.update/query方法,根据方法类型选择不同的数据源。 -
数据加密插件:拦截
ParameterHandler.setParameters,对敏感字段值加密;拦截ResultSetHandler.handleResultSets对字段值解密。
🔧 进阶提醒:插件中如果修改了 BoundSql 的 SQL 和参数,务必保持参数和占位符一致。而且 Interceptor 的实现需要考虑线程安全,因为核心对象可能被多线程共享。
理解了责任链模式,你也能写出优雅的横向切面,让 SQL 执行变成一块可以任意编排的积木。
Agent 需要根据运行时条件动态拼接查询 SQL,MyBatis 如何安全实现?¶
Agent 类应用(比如 ChatBI、智能查询助手)经常要根据用户的自然语言生成不同的查询条件,比如:
“查一下上个月消费超过 1000 的VIP用户” → 动态生成 WHERE 条件。
MyBatis 提供了多种动态 SQL 方案,但安全第一,必须防注入。
🛡️ 安全前提:绝对不用 ${} 拼接用户输入¶
MyBatis 中 #{} 会使用预编译参数,安全;${} 是直接字符串替换,等于给 SQL 注入大开后门。动态拼接时,若必须拼接表名、列名这种无法预编译的部分,要做严格白名单校验。
📝 方案一:XML 动态标签(最安全、推荐)¶
利用 <if>, <where>, <foreach> 等标签,把运行时条件映射为参数。
示例:根据多个可选条件查询用户
<select id="queryUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAmount != null">
AND total_amount >= #{minAmount}
</if>
<if test="vip != null and vip == true">
AND is_vip = 1
</if>
</where>
<if test="orderBy != null">
ORDER BY ${orderBy} <!-- 这里必须白名单校验 -->
</if>
</select>
对应的 Mapper 接口:
List<User> queryUsers(@Param("name") String name,
@Param("minAmount") BigDecimal minAmount,
@Param("vip") Boolean vip,
@Param("orderBy") String orderBy);
对于 orderBy 这种必须用 ${} 的地方,要在服务层做白名单过滤:
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "create_time", "total_amount");
public List<User> safeQuery(String name, BigDecimal minAmount, Boolean vip, String orderBy) {
if (orderBy != null && !ALLOWED_COLUMNS.contains(orderBy)) {
throw new IllegalArgumentException("Invalid column: " + orderBy);
}
return mapper.queryUsers(name, minAmount, vip, orderBy);
}
📝 方案二:使用 SQL 构建器(Java API 动态拼接)¶
当条件太多、组合复杂,XML 会变得臃肿,这时可以用 MyBatis 自带的 SQL 类(org.apache.ibatis.jdbc.SQL)或以 SelectProvider 的方式在 Java 代码中构建。
动态构建的 Provider 类:
public class UserSqlProvider {
public String buildQuery(Map<String, Object> params) {
String name = (String) params.get("name");
BigDecimal minAmount = (BigDecimal) params.get("minAmount");
Boolean vip = (Boolean) params.get("vip");
return new SQL() {{
SELECT("*");
FROM("users");
if (name != null && !name.isEmpty()) {
WHERE("name LIKE CONCAT('%', #{name}, '%')");
}
if (minAmount != null) {
WHERE("total_amount >= #{minAmount}");
}
if (vip != null && vip) {
WHERE("is_vip = 1");
}
// 排序字段必须从白名单取值后再拼
}}.toString();
}
}
Mapper 接口:
@SelectProvider(type = UserSqlProvider.class, method = "buildQuery")
List<User> queryUsersDynamic(Map<String, Object> params);
⚡ 方案三:Agent 复杂场景——基于元数据的查询引擎¶
如果你的 Agent 允许用户任意组合字段、操作符(>、<、=、IN等),就需要一套查询模板 + 元数据校验的机制。
简单设计思路:
- 前端/Agent 生成一个结构化的查询描述对象,如:
{
"conditions": [
{ "field": "total_amount", "op": "GE", "value": 1000 },
{ "field": "is_vip", "op": "EQ", "value": true }
],
"orderBy": "create_time",
"orderDir": "DESC"
}
-
后端定义一个字段元数据表(或枚举),记录每个允许查询的字段名、数据类型、是否支持排序。
-
服务层遍历 conditions,通过白名单验证
field合法,根据类型参数化拼接 SQL。绝不能直接把用户的字段名插入 SQL,必须从元数据中映射后才可拼接。
伪代码示例:
@SelectProvider(type = AgentQueryProvider.class, method = "buildDynamicQuery")
List<Map<String, Object>> executeQuery(QueryRequest request);
// 在 Provider 内部使用 StringBuilder + 参数化构建,所有值用 #{paramX}
MyBatis 提供的 #{param} 将始终使用 PreparedStatement,所以只要不把用户值拼到 SQL 字符串中,基本没有注入风险。
🔐 安全铁律¶
-
值:一律
#{xxx}。 -
列名、表名、操作符:白名单映射,不用
#{}而用常量替换。 -
IN 查询:
<foreach>配合#{item},安全。 -
排序、分页:对参数做枚举校验。
💡 把“安全”刻在每次拼接的肌肉记忆里,你交付的 Agent 才不会是定时炸弹。动态 SQL 的能力越强,安全防护就要越硬。
MyBatis 的 Mapper 接口没有实现类,为什么能直接调用?底层原理是什么?¶
这个问题是 MyBatis 的灵魂拷问,理解了它,框架在你面前基本就是透明的了。
🔍 一句话原理¶
MyBatis 在运行时通过 JDK 动态代理 为 Mapper 接口生成了一个代理对象。当你调用接口方法时,真正干活的是 MapperProxy,它把方法调用翻译成对应的 SQL 执行。
🧩 拆解:从接口到 SQL 的全过程¶
① 启动时:解析 XML / 注解,建立映射关系
MyBatis 启动时会解析每个 mapper.xml 或 Mapper 接口上的注解,把每个 SQL 语句封装成一个 MappedStatement 对象,并以 “接口全限定名 + 方法名” 作为唯一标识(namespace.id)存入全局 Configuration 中。
② 获取 Mapper 对象:SqlSession.getMapper()
当你调用 sqlSession.getMapper(UserMapper.class) 时,MyBatis 会调用 MapperRegistry 获取或创建一个代理实例。核心代码:
// 简化版 MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
③ 方法调用:被 MapperProxy.invoke() 拦截
每次调用 userMapper.getUser(1) 时,会被代理对象的 invoke 方法拦截。它会根据当前方法找到对应的 MappedStatement,然后判断方法返回类型和 SQL 类型(SELECT/INSERT/UPDATE/DELETE),最终委托给 SqlSession 执行。
④ 真正执行 SQL
SqlSession 会通过 Executor 执行器,经过缓存、插件拦截等,最终通过 JDBC 的 PreparedStatement 执行 SQL,并把结果集转换为方法返回值类型(List、对象等)。
📊 流程图¶
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
|
v
MapperRegistry.getMapper()
-> Proxy.newProxyInstance()
|
返回动态代理对象 $Proxy
|
userMapper.getUser(1)
|
进入 MapperProxy.invoke()
|
获取 MappedStatement (statementId = "com.xxx.UserMapper.getUser")
|
判断 SQL 命令类型 (SELECT) 与返回值类型
|
调用 sqlSession.selectOne(statementId, param)
|
Executor 处理缓存、插件 -> StatementHandler -> JDBC
|
返回结果集 -> 映射为 User 对象
🧪 验证:自己写一个“迷你版 MapperProxy”¶
public class MiniMapperProxy implements InvocationHandler {
private SqlSession sqlSession;
public MiniMapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 假设约定接口方法名就是 statement id
String statement = method.getDeclaringClass().getName() + "." + method.getName();
// 判断返回值类型决定调用 selectOne 还是 selectList
if (method.getReturnType().isAssignableFrom(List.class)) {
return sqlSession.selectList(statement, args[0]);
} else {
return sqlSession.selectOne(statement, args[0]);
}
}
}
// 手动创建代理
UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class[]{UserMapper.class},
new MiniMapperProxy(sqlSession));
这就是 MyBatis “零实现类” 的魔法本质。你写的每一个接口方法,最终都被翻译成了 sqlSession 上的操作。
🧠 思考延伸¶
-
既然接口没有实现类,那接口里的
default方法可以直接运行,因为代理无法拦截default方法,执行的是接口本身的字节码。 -
MyBatis-Plus 的
BaseMapper就是在 Mapper 接口上增加了一层抽象,内部依旧是这套动态代理机制。 -
理解了这一套,以后遇到“MyBatis 方法重载”相关问题(不推荐重载,因为 statement id 只认方法名)就豁然开朗了。
📌 这一题,你如果能从动态代理讲到 MappedStatement 映射,再到 SqlSession 的执行,面试官就知道你不是只会用,而是吃透了。
Agent 的对话记录需要存储到 MySQL,消息体是变长 JSON,如何用 MyBatis 优雅处理?¶
Agent 的对话历史通常是一条记录里包含一个变长、嵌套的 JSON 消息数组(如对话轮次、tool_calls、参数等)。MySQL 从 5.7 开始原生支持 JSON 类型,但 MyBatis 如何优雅地把它映射到 Java 对象并写入/读取?
🗄️ 场景示例:消息表结构¶
CREATE TABLE agent_conversation (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL,
messages JSON NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
messages 字段存储类似:
[
{"role": "user", "content": "帮我查一下订单"},
{"role": "assistant", "content": "好的,订单号是?", "tool_calls": null}
]
对应的 Java 模型:
public class Conversation {
private Long id;
private String sessionId;
private List<Message> messages; // 映射到 JSON 列
private LocalDateTime createdAt;
}
public class Message {
private String role;
private String content;
private Object toolCalls; // 可进一步定义结构
}
✨ 优雅处理方案:自定义 TypeHandler¶
MyBatis 默认不认识 List<Message> 到 JSON 的转换。我们需要写一个 TypeHandler,利用 Jackson 做序列化/反序列化。
定义 TypeHandler
@MappedTypes(List.class) // 可指定为 List<Message> 但泛型擦除,需注意
@MappedJdbcTypes(JdbcType.VARCHAR) // MySQL JSON 类型在 JDBC 中映射为 VARCHAR 或 LONGVARCHAR
public class MessageListTypeHandler extends BaseTypeHandler<List<Message>> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
List<Message> parameter, JdbcType jdbcType)
throws SQLException {
try {
ps.setString(i, objectMapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("Failed to convert messages to JSON", e);
}
}
@Override
public List<Message> getNullableResult(ResultSet rs, String columnName)
throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public List<Message> getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public List<Message> getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private List<Message> parseJson(String json) {
if (json == null || json.isEmpty()) return Collections.emptyList();
try {
return objectMapper.readValue(json, new TypeReference<List<Message>>() {});
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse JSON to messages", e);
}
}
}
注册 TypeHandler
在 mybatis-config.xml 中:
<typeHandlers>
<typeHandler handler="com.example.handler.MessageListTypeHandler"
javaType="java.util.List" jdbcType="VARCHAR"/>
</typeHandlers>
或者 MyBatis-Plus/Spring Boot 项目,可以在配置类中手动注册:
configuration.getTypeHandlerRegistry().register(List.class, JdbcType.VARCHAR, new MessageListTypeHandler());
更方便的是在 Mapper XML 中直接指定 typeHandler。
使用方式
在 Mapper XML 的 resultMap 或字段映射中引用:
<resultMap id="conversationMap" type="Conversation">
<id property="id" column="id"/>
<result property="sessionId" column="session_id"/>
<result property="messages" column="messages"
typeHandler="com.example.handler.MessageListTypeHandler"/>
<result property="createdAt" column="created_at"/>
</resultMap>
或者更简洁的注解方式(MyBatis 3.5+ 支持注解指定 TypeHandler):
@Results({
@Result(property = "messages", column = "messages",
typeHandler = MessageListTypeHandler.class)
})
@Select("SELECT * FROM agent_conversation WHERE session_id = #{sessionId}")
Conversation getBySessionId(String sessionId);
这样,你的 Java 代码中就可以直接读写强类型的 List<Message>,MyBatis 在中间完成透明的 JSON 转换。
🚀 进阶:利用 MySQL 的 JSON 函数查询¶
有时候我们需要对 JSON 内部字段进行查询,比如查询包含某个 tool_call 的会话。MyBatis 依然可以安全使用预编译:
<select id="findByToolName" resultMap="conversationMap">
SELECT * FROM agent_conversation
WHERE JSON_CONTAINS(messages, #{toolName}, '$.tool_calls[*].function.name')
</select>
参数用 #{toolName} 安全预编译。建议在业务层对这些函数做一层封装,避免 SQL 散落各处。
🧰 替代方案:把 JSON 当作字符串,Service 层手动转换¶
如果项目很简单,不想写 TypeHandler,也可以在 Entity 里把 messages 定义为 String,然后在 Service 层用 Jackson 手动转换。但这样破坏了持久层的对象语义,个人更推荐 TypeHandler 方案,它把转换逻辑收归到 MyBatis 类型系统内,代码更干净。
🔐 安全性补丁¶
-
JSON 内容可能很大,注意 MySQL
max_allowed_packet大小限制。 -
反序列化时注意多态问题,如果你的
Message有子类,需要配置 Jackson 的多态解析。 -
TypeHandler 中使用的
ObjectMapper应该共用同一个实例(static final),避免创建开销。
💡 总结¶
通过自定义 TypeHandler,我们把 MySQL 的 JSON 列映射为强类型的 Java 对象集合,让数据库层的半结构化数据在业务代码中以完全面向对象的形式出现。这是一个典型的“基础设施即工具”思路:花一点力气做好适配,上层就能享受到类型安全和编写便利。对于 Agent 这种消息结构灵活多变的场景,这一招能让你的持久层代码简洁又健壮。
一级缓存与二级缓存¶
1、MyBatis 的一级缓存和二级缓存有什么区别?¶
难度级别:⭐⭐(缓存级别、失效条件、脏读风险)
1️⃣ Common Answer 一级缓存是 SqlSession 级别的,默认开启,同一个 SqlSession 里查询相同数据会走缓存。二级缓存是 Mapper 级别的,需要配置开启,多个 SqlSession 可以共享缓存。一级缓存会在增删改操作后失效,二级缓存也是一样。
2️⃣ Impressive Answer
-
一级缓存(SqlSession 级别):基于 PerpetualCache 实现,默认开启,缓存 key 是 Statement ID + 参数 + SQL。在同一个 SqlSession 内,相同查询直接返回缓存结果,避免重复查询数据库。失效条件:执行 insert/update/delete、调用 sqlSession.clearCache()、SqlSession 关闭。
-
二级缓存(Mapper 级别):基于 Mapper 的 namespace,需要配置
<cache/>开启,多个 SqlSession 共享同一 Mapper 的缓存。核心区别:一级缓存只在 SqlSession 内有效,二级缓存跨 SqlSession 共享;二级缓存需要序列化对象(要求实体类实现 Serializable)。 -
多表查询脏读风险:一级缓存可能脏读(多表关联时,只查询主表可能返回缓存的旧数据),二级缓存通过
flushCache配置避免。最佳实践:多表关联查询时关闭二级缓存或设置flushCache="true",避免脏数据。 -
Spring 整合后的行为:Spring 整合 MyBatis 后,SqlSession 由 Spring 管理,一级缓存在事务内有效,事务提交后 SqlSession 关闭,一级缓存清空。二级缓存独立于事务,适合读多写少的场景。
3️⃣ Key Differences
2、MyBatis 一级缓存在 Spring 事务中的行为是什么?为什么开启事务后缓存命中率更高?¶
难度级别:⭐⭐⭐(事务管理、缓存共享、命中机制)
1️⃣ Common Answer Spring 事务中,SqlSession 是共享的,所以一级缓存在事务内有效。开启事务后,多次查询会走缓存,命中率更高。事务提交后 SqlSession 关闭,缓存清空。
2️⃣ Impressive Answer
-
Spring 整合后的 SqlSession 管理:Spring 使用
SqlSessionTemplate代理 SqlSession,默认每次操作创建新 SqlSession。但在事务内,Spring 通过SqlSessionHolder绑定当前 SqlSession 到 ThreadLocal,整个事务共享同一个 SqlSession,一级缓存生效。 -
缓存命中率提升的原因:无事务时,每次数据库操作都创建新 SqlSession,一级缓存无法跨操作共享;开启事务后,同一事务内的多次查询共享 SqlSession,相同查询直接命中一级缓存,避免重复查库。性能提升:高频查询场景(如 Agent 对话记录查询),事务内缓存命中率可达 90%+。
-
事务提交后的缓存清理:事务提交时,Spring 会调用
SqlSessionUtils.closeSqlSession()清理 SqlSession,一级缓存自动清空,避免脏读。二级缓存独立于事务,事务提交后二级缓存仍然有效。 -
Agent 场景实践:Agent 查询对话记录时,在
@Transactional方法内多次查询相同 Session,一级缓存生效,减少 DB 压力。但注意:事务内避免大对象查询(如加载全量历史消息),防止一级缓存占用过多内存。
3️⃣ Key Differences
3、场景题:Agent 的知识库查询接口 QPS 很高,如何合理利用 MyBatis 缓存减少数据库压力?¶
难度级别:⭐⭐⭐(缓存策略、性能优化、Agent 场景)
1️⃣ Common Answer 开启 MyBatis 的二级缓存,知识库查询走缓存,减少数据库压力。设置缓存过期时间,避免数据过期。用 Redis 做缓存,提高性能。
2️⃣ Impressive Answer
-
二级缓存配置:在知识库 Mapper XML 中配置
<cache eviction="LRU" flushInterval="60000" size="1024"/>,使用 LRU 淘汰策略,60 秒刷新,缓存 1024 个对象。注意:实体类实现Serializable,二级缓存需要序列化。 -
一级缓存 + 事务优化:在高频查询接口(如 Agent 知识库检索)使用
@Transactional(readOnly = true),同一事务内多次查询共享一级缓存,避免重复查库。性能提升:单次请求内多次查询相同知识库条目,一级缓存命中率可达 100%。 -
缓存失效策略:知识库更新时,调用
sqlSession.clearCache()清空一级缓存;二级缓存通过flushCache="true"在 update/delete 时自动失效。Agent 场景:知识库更新频率低,二级缓存命中率高,QPS 1000+ 时 DB 压力可降低 80%。 -
混合缓存方案:热点知识库条目(如 FAQ)用二级缓存,冷数据用 Redis 分布式缓存;大对象(如向量数据)不缓存,避免内存溢出。监控指标:缓存命中率、缓存大小、DB QPS,通过 MyBatis 插件统计缓存效果。
3️⃣ Key Differences
4、容易一起考的题¶
SQL 执行全链路¶
1、基础题:MyBatis 中 SqlSession 的作用是什么?它和 SqlSessionFactory 的关系?¶
难度级别:⭐⭐(SqlSession 作用、工厂模式、生命周期)
1️⃣ Common Answer SqlSession 是 MyBatis 的核心接口,用于执行 SQL、获取 Mapper、管理事务。SqlSessionFactory 是工厂,用来创建 SqlSession。SqlSessionFactory 是单例的,SqlSession 每次用完要关闭。
2️⃣ Impressive Answer
-
SqlSession 的作用:SqlSession 相当于 JDBC 的 Connection,封装了 Executor、StatementHandler、Transaction 等组件,提供
selectList()、insert()、update()、delete()等方法执行 SQL,以及getMapper()获取 Mapper 代理对象,commit()/rollback()管理事务。 -
SqlSessionFactory 的职责:SqlSessionFactory 是重量级对象,创建时加载配置文件(mybatis-config.xml)、Mapper XML,构建 Configuration 对象,内部维护 Executor、StatementHandler 等组件的工厂方法。生命周期:应用级别,全局单例,线程安全。
-
关系与生命周期:SqlSessionFactory 创建 SqlSession,SqlSession 是轻量级对象,非线程安全,每次请求创建新实例,用完必须关闭(
finally块中调用close())。Spring 整合:Spring 管理 SqlSessionFactory(单例),SqlSession 由 SqlSessionTemplate 代理,自动管理生命周期。 -
底层实现:SqlSession 默认实现是
DefaultSqlSession,内部持有Configuration、Executor、Transaction;SqlSessionFactory 默认实现是DefaultSqlSessionFactory,通过openSession()创建 SqlSession,可指定 Executor 类型(SIMPLE、REUSE、BATCH)。
3️⃣ Key Differences
2、进阶题:MyBatis 的 SQL 执行流程是什么?从 sqlSession.selectList() 到 JDBC 的完整链路?¶
难度级别:⭐⭐⭐(执行链路、组件协作、JDBC 封装)
1️⃣ Common Answer SqlSession 调用 selectList,然后 Executor 执行 SQL,StatementHandler 处理 JDBC,最后返回结果。中间还有 ParameterHandler 和 ResultSetHandler 处理参数和结果。
2️⃣ Impressive Answer
-
SqlSession.selectList():调用
Configuration.getMappedStatement()获取 MappedStatement(包含 SQL、参数映射、结果映射),然后调用executor.query()。 -
Executor.query():BaseExecutor 先查一级缓存(
localCache),命中直接返回;未命中则调用doQuery()。二级缓存:CachingExecutor 先查二级缓存,未命中再查一级缓存,最后查数据库。 -
StatementHandler.prepare():创建 JDBC Statement(PreparedStatement),调用
ParameterHandler.setParameters()设置参数(处理#{}占位符),然后StatementHandler.query()执行 SQL。 -
ResultSetHandler.handleResultSets():处理 JDBC ResultSet,根据 ResultMap 映射结果对象,支持嵌套结果、延迟加载。返回结果:List
-
完整链路:SqlSession → Executor → StatementHandler → ParameterHandler → JDBC Statement → ResultSet → ResultSetHandler → Result Object。
3️⃣ Key Differences
3、场景题:Agent 服务出现慢 SQL,如何利用 MyBatis 执行链路快速定位问题?¶
难度级别:⭐⭐⭐(性能排查、插件拦截、Agent 场景)
1️⃣ Common Answer 开启 MyBatis 日志,看 SQL 执行时间。用数据库慢查询日志定位问题。优化 SQL,加索引。
2️⃣ Impressive Answer
- MyBatis 插件拦截:实现
Interceptor接口,拦截Executor.update()和Executor.query(),记录 SQL 执行时间、参数、结果行数。关键代码:
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class SlowSqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
if (cost > 1000) log.warn("Slow SQL: {}, cost: {}ms", invocation.getArgs()[0], cost);
return result;
}
}
- 执行链路分析:慢 SQL 可能出现在以下环节:
- Executor:缓存未命中,重复查库 → 优化缓存策略
- StatementHandler:参数设置慢(如大对象序列化)→ 优化参数传递
- JDBC:SQL 本身慢(如全表扫描)→ 优化 SQL,加索引
-
ResultSetHandler:结果映射慢(如大量数据)→ 分页查询,减少数据量
-
Agent 场景优化:Agent 对话记录查询慢,通过插件定位到
ResultSetHandler处理大量历史消息耗时,改用分页查询(RowBounds或PageHelper),单页 20 条,性能提升 80%。 -
监控与告警:集成 Prometheus,采集 SQL 执行时间、QPS、缓存命中率,设置慢 SQL 告警(>1s)。最佳实践:开发环境开启 SQL 日志(
logging.level.org.mybatis=DEBUG),生产环境用插件监控。
3️⃣ Key Differences
4、容易一起考的题¶
懒加载与 N+1 问题¶
1、基础题:什么是 MyBatis 的懒加载?如何开启?¶
难度级别:⭐⭐(懒加载概念、配置方式、延迟查询)
1️⃣ Common Answer 懒加载就是用到的时候才查询关联数据,不用的时候不查。在 mybatis-config.xml 里配置 lazyLoadingEnabled=true 开启。关联查询用 association 或 collection。
2️⃣ Impressive Answer
-
懒加载概念:懒加载(延迟加载)指在查询主对象时,不立即查询关联对象,而是在访问关联对象属性时才触发查询。好处:避免一次性加载大量数据,减少内存占用和数据库压力。
-
配置方式:在
mybatis-config.xml中配置:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
lazyLoadingEnabled=true 开启懒加载,aggressiveLazyLoading=false 关闭激进懒加载(只加载被调用的属性,而非所有属性)。
- 关联查询配置:在 Mapper XML 中使用
association或collection,设置fetchType="lazy":
<resultMap id="sessionResultMap" type="Session">
<id property="id" column="id"/>
<collection property="messages" ofType="Message" fetchType="lazy">
<id property="id" column="msg_id"/>
</collection>
</resultMap>
- 底层实现:懒加载通过 CGLIB 代理实现,代理对象拦截
getMessages()方法,触发 SQL 查询。注意:懒加载要求 SqlSession 未关闭,否则报错;Spring 整合后需在事务内访问懒加载属性。
3️⃣ Key Differences
2、进阶题:MyBatis 懒加载的底层实现原理是什么?¶
难度级别:⭐⭐⭐(CGLIB 代理、代理拦截、aggressiveLazyLoading)
1️⃣ Common Answer 懒加载用 CGLIB 生成代理对象,访问属性时触发查询。aggressiveLazyLoading 控制是否加载所有属性。
2️⃣ Impressive Answer
-
CGLIB 代理生成:MyBatis 使用
ProxyFactory创建代理对象,默认用 CGLIB(proxyFactory可配置为 JAVASSIST)。代理对象继承目标类,拦截getter方法,触发延迟查询。 -
代理拦截逻辑:代理对象内部持有
ResultLoader,拦截getMessages()方法时,调用ResultLoader.loadResult()执行 SQL 查询,填充属性值。关键代码:
public class CglibProxyFactory implements ProxyFactory {
@Override
public Object createProxy(Object target, ResultLoaderMap lazyLoader) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new LazyLoaderInterceptor(lazyLoader));
return enhancer.create();
}
}
- aggressiveLazyLoading 配置:
aggressiveLazyLoading=true(默认):调用任意getter方法时,加载所有懒加载属性(性能差,不推荐)。-
aggressiveLazyLoading=false:只加载被调用的属性,其他懒加载属性保持未加载状态(推荐,性能好)。 -
SqlSession 生命周期限制:懒加载依赖 SqlSession,如果 SqlSession 已关闭,访问懒加载属性会报
SqlSessionException。Spring 整合:在@Transactional方法内访问懒加载属性,确保 SqlSession 未关闭;或在事务外使用fetchType="eager"预加载。
3️⃣ Key Differences
3、场景题:Agent 多轮对话中,每次查询 Session 都要关联查询所有 Message,出现了 N+1 问题,如何优化?¶
难度级别:⭐⭐⭐(N+1 问题、懒加载、批量查询、Agent 场景)
1️⃣ Common Answer 用懒加载,访问 Message 时再查。或者用 JOIN 查询,一次性查出所有数据。用 MyBatis 的 batch 查询。
2️⃣ Impressive Answer
-
N+1 问题分析:查询 10 个 Session,每个 Session 关联查询 100 条 Message,共执行 1 + 10 × 100 = 1001 次 SQL(1 次查 Session,1000 次查 Message),性能极差。根本原因:循环中逐个查询关联数据。
-
优化方案 1:懒加载 + 批量查询:
- 开启懒加载(
lazyLoadingEnabled=true),查询 Session 时不立即查询 Message。 - 使用
MyBatis-Plus的in查询批量加载 Message:
List<Long> sessionIds = sessions.stream().map(Session::getId).collect(Collectors.toList());
List<Message> messages = messageMapper.selectList(new LambdaQueryWrapper<Message>().in(Message::getSessionId, sessionIds));
-
手动组装 Session 和 Message 的关联关系,减少 SQL 次数到 2 次(1 次查 Session,1 次批量查 Message)。
-
优化方案 2:JOIN 查询:
- 在 Mapper XML 中使用
collection嵌套查询,配置fetchType="eager":
<resultMap id="sessionWithMessagesMap" type="Session">
<id property="id" column="id"/>
<collection property="messages" ofType="Message" fetchType="eager">
<id property="id" column="msg_id"/>
</collection>
</resultMap>
<select id="selectSessionWithMessages" resultMap="sessionWithMessagesMap">
SELECT s.*, m.id as msg_id FROM session s LEFT JOIN message m ON s.id = m.session_id WHERE s.id = #{id}
</select>
-
一次 SQL 查询出 Session 和所有 Message,避免 N+1 问题。
-
Agent 场景最佳实践:
- 多轮对话查询:用懒加载 + 批量查询,避免加载全量历史消息,只加载最近 20 条(分页)。
- 知识库关联查询:用 JOIN 查询,一次性加载知识库条目和关联标签。
- 监控与优化:通过 MyBatis 插件统计 SQL 次数,设置告警(单次请求 SQL > 10 次),持续优化。
3️⃣ Key Differences
4、容易一起考的题¶
多数据源与事务协同¶
1、基础题:MyBatis 如何配置多数据源?¶
难度级别:⭐⭐(多数据源配置、SqlSessionFactory 分离、Mapper 扫描)
1️⃣ Common Answer 配置多个 DataSource,每个 DataSource 对应一个 SqlSessionFactory。用 @Qualifier 指定使用哪个数据源。Mapper 分开扫描。
2️⃣ Impressive Answer
- 多数据源配置:在 Spring Boot 中配置多个
DataSource,分别创建SqlSessionFactory和SqlSessionTemplate:
@Configuration
public class DataSourceConfig {
@Bean(name = "primaryDataSource")
@Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().url("jdbc:mysql://primary-db").build();
}
@Bean(name = "secondaryDataSource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().url("jdbc:mysql://secondary-db").build();
}
@Bean(name = "primarySqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml"));
return factory.getObject();
}
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml"));
return factory.getObject();
}
}
- Mapper 扫描分离:使用
@MapperScan分别扫描不同数据源的 Mapper:
@MapperScan(basePackages = "com.example.primary.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryMapperConfig {}
@MapperScan(basePackages = "com.example.secondary.mapper", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryMapperConfig {}
- 使用方式:在 Service 中注入对应的 Mapper,自动使用对应的数据源:
@Service
public class AgentService {
@Autowired
private PrimaryUserMapper primaryUserMapper;
@Autowired
private SecondaryVectorMapper secondaryVectorMapper;
}
- 动态数据源:使用
AbstractRoutingDataSource实现动态切换数据源(进阶题详细讲解)。
3️⃣ Key Differences
2、进阶题:AbstractRoutingDataSource 的原理是什么?如何与 Spring 事务协同工作?¶
难度级别:⭐⭐⭐(动态数据源、ThreadLocal、事务传播)
1️⃣ Common Answer AbstractRoutingDataSource 是动态数据源,根据 key 切换数据源。用 ThreadLocal 存数据源 key。事务内不能切换数据源。
2️⃣ Impressive Answer
-
AbstractRoutingDataSource 原理:继承
AbstractRoutingDataSource,实现determineCurrentLookupKey()方法,返回数据源 key。内部维护Map<Object, Object> targetDataSources,存储多个数据源。getConnection()时,根据determineCurrentLookupKey()返回的 key 从targetDataSources获取对应数据源。 -
ThreadLocal 存储数据源 key:
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
}
使用时:DynamicDataSource.setDataSourceKey("secondary"),后续 SQL 操作使用 secondary 数据源。
- 与 Spring 事务协同:
- 事务内切换数据源:事务开始前切换数据源,事务内所有操作使用同一数据源。注意:事务内不能切换数据源,否则报错(事务绑定的是初始数据源的 Connection)。
- 事务传播行为:
REQUIRED传播行为下,子事务继承父事务的数据源;REQUIRES_NEW传播行为下,子事务可以切换数据源。 -
最佳实践:在
@Transactional方法外切换数据源,事务内不切换;或使用@Transactional(propagation = Propagation.REQUIRES_NEW)开启新事务切换数据源。 -
Agent 场景实践:Agent 工具调用需要读写业务库和向量库,使用动态数据源切换:
@Service
public class AgentToolService {
@Transactional
public void executeTool(ToolRequest request) {
DynamicDataSource.setDataSourceKey("primary");
businessMapper.insert(request);
DynamicDataSource.setDataSourceKey("secondary");
vectorMapper.insert(request.getVector());
}
}
注意:上述代码会报错,因为事务内切换数据源无效。正确做法:拆分成两个事务方法,或使用 REQUIRES_NEW。
3️⃣ Key Differences
3、场景题:Agent 工具调用需要同时读写业务库和向量库,如何保证跨库操作的数据一致性?¶
难度级别:⭐⭐⭐(分布式事务、两阶段提交、Agent 场景)
1️⃣ Common Answer 用分布式事务,如 Seata。或者用消息队列,最终一致性。先写业务库,再写向量库,失败回滚。
2️⃣ Impressive Answer
-
场景分析:Agent 工具调用需要同时写入业务库(订单、用户)和向量库(向量数据),两个数据库独立部署,无法使用本地事务保证一致性。挑战:网络分区、数据库宕机、部分提交导致数据不一致。
-
方案 1:本地事务 + 补偿机制(推荐):
- 主流程:先写业务库(本地事务),成功后写向量库(远程调用)。
- 补偿机制:向量库写入失败时,记录失败日志,通过定时任务重试;或发送消息到 MQ,消费者重试。
- 代码示例:
@Transactional
public void executeTool(ToolRequest request) {
businessMapper.insert(request);
try {
vectorService.insertVector(request.getVector());
} catch (Exception e) {
log.error("向量库写入失败,记录重试日志", e);
retryLogMapper.insert(new RetryLog(request));
}
}
- 方案 2:Seata 分布式事务:
- 使用 Seata AT 模式,业务库和向量库都接入 Seata,通过
@GlobalTransactional保证分布式事务一致性。 - 缺点:性能开销大(锁机制、日志记录),不适合高并发场景。
-
适用场景:强一致性要求高、并发量低的场景(如金融系统)。
-
Agent 场景最佳实践:
- 弱一致性:工具调用允许短暂不一致,用本地事务 + 补偿机制,性能好。
- 强一致性:关键操作(如支付)用 Seata,保证数据一致。
- 监控与告警:监控跨库操作的成功率、失败重试次数,设置告警(失败率 > 1%)。
3️⃣ Key Differences
4、容易一起考的题¶
MyBatis-Plus 选型与实践¶
1、基础题:MyBatis-Plus 的条件构造器(LambdaQueryWrapper)和手写 XML 动态 SQL 各有什么优缺点?¶
难度级别:⭐⭐(条件构造器、动态 SQL、代码可读性)
1️⃣ Common Answer LambdaQueryWrapper 用 Java 代码写查询,方便,不用写 XML。XML 动态 SQL 灵活,复杂查询用 XML。LambdaQueryWrapper 类型安全,XML 容易写错。
2️⃣ Impressive Answer
- LambdaQueryWrapper 优点:
- 类型安全:编译期检查字段名,避免 SQL 拼写错误。
- 代码可读性:Java 代码直观,IDE 支持重构和跳转。
- 动态条件:支持
if条件判断,动态构建查询:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, "ACTIVE")
.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(User::getCreateTime, startTime);
-
链式调用:API 设计优雅,代码简洁。
-
LambdaQueryWrapper 缺点:
- 复杂查询限制:多表关联、子查询、复杂聚合查询不如 XML 灵活。
- 性能问题:动态拼接 SQL,可能生成低效 SQL(如
OR条件过多)。 -
学习成本:需要熟悉 MyBatis-Plus API。
-
XML 动态 SQL 优点:
- 灵活性高:支持复杂 SQL(多表关联、子查询、存储过程调用)。
- SQL 优化:可以手动优化 SQL,避免低效查询。
-
可维护性:SQL 与代码分离,DBA 可以直接优化 SQL。
-
XML 动态 SQL 缺点:
- 类型不安全:字段名是字符串,容易拼写错误。
- 代码冗余:需要编写 XML 文件,增加维护成本。
-
重构困难:修改字段名需要同步修改 XML。
-
选型建议:
- 简单查询:用
LambdaQueryWrapper,提高开发效率。 - 复杂查询:用 XML 动态 SQL,保证 SQL 性能和灵活性。
- 混合使用:简单条件用
LambdaQueryWrapper,复杂逻辑用 XML。
3️⃣ Key Differences
2、进阶题:MyBatis-Plus 的自动填充(MetaObjectHandler)和逻辑删除是如何实现的?¶
难度级别:⭐⭐⭐(自动填充、逻辑删除、拦截器、Agent 场景)
1️⃣ Common Answer 自动填充用 MetaObjectHandler,在插入和更新时自动填充字段。逻辑删除用 @TableLogic,删除时改标记字段,不是真删除。
2️⃣ Impressive Answer
- 自动填充实现:
- 实现
MetaObjectHandler接口,重写insertFill()和updateFill()方法:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUserId());
}
}
-
实体类字段添加
@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.INSERT_UPDATE),触发自动填充。 -
自动填充原理:
- MyBatis-Plus 通过
MybatisPlusInterceptor拦截 SQL 执行,在插入和更新前调用MetaObjectHandler填充字段。 -
strictInsertFill()严格填充(字段存在且非空才填充),fillStrategy()策略填充(覆盖已有值)。 -
逻辑删除实现:
- 实体类字段添加
@TableLogic:
- 配置逻辑删除值和未删除值:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
-
删除操作自动转为
UPDATE:UPDATE user SET deleted = 1 WHERE id = ?;查询自动添加条件:WHERE deleted = 0。 -
Agent 场景实践:
- 自动填充:Agent 对话记录自动填充
createTime、createBy(Agent ID),避免手动设置,减少代码冗余。 - 逻辑删除:Agent 工具调用记录逻辑删除,保留历史数据用于审计和分析,查询时自动过滤已删除记录。
3️⃣ Key Differences
3、场景题:Agent 需要根据 LLM 返回的结构化条件动态构建查询,MyBatis-Plus 和 XML 动态 SQL 如何选型?¶
难度级别:⭐⭐⭐(动态查询、LLM 场景、选型策略)
1️⃣ Common Answer 用 MyBatis-Plus 的条件构造器,动态拼接条件。LLM 返回什么条件就加什么条件。或者用 XML 动态 SQL,灵活。
2️⃣ Impressive Answer
-
场景分析:LLM 返回结构化条件(如
{"name": "张三", "age": ">18", "status": "ACTIVE"}),需要动态构建 SQL 查询。挑战:条件数量不固定、操作符多样(=、>、LIKE)、字段类型多样。 -
方案 1:MyBatis-Plus 条件构造器(推荐):
- 解析 LLM 返回的 JSON,动态构建
LambdaQueryWrapper:
public List<User> queryByLLMCondition(String conditionJson) {
Map<String, Object> condition = JSON.parseObject(conditionJson, new TypeReference<Map<String, Object>>() {});
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
condition.forEach((key, value) -> {
if (value instanceof String) {
wrapper.like(User::getName, value);
} else if (value instanceof Number) {
wrapper.eq(User::getAge, value);
} else if (value instanceof Map) {
Map<String, String> opMap = (Map<String, String>) value;
if (opMap.containsKey("$gt")) {
wrapper.gt(User::getAge, opMap.get("$gt"));
}
}
});
return userMapper.selectList(wrapper);
}
-
优点:类型安全、代码简洁、支持动态条件。
-
缺点:复杂操作符(如
IN、BETWEEN)需要手动处理。 -
方案 2:XML 动态 SQL:
- 在 Mapper XML 中使用
<if>、<choose>、<foreach>动态构建 SQL:
<select id="queryByLLMCondition" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="age != null">AND age = #{age}</if>
<if test="ageMin != null">AND age > #{ageMin}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</select>
-
优点:SQL 灵活、支持复杂操作符。
-
缺点:字段名是字符串,容易拼写错误。
-
选型建议:
- 简单动态查询:用 MyBatis-Plus 条件构造器,提高开发效率。
- 复杂动态查询:用 XML 动态 SQL,保证 SQL 灵活性。
-
混合使用:基础条件用
LambdaQueryWrapper,复杂逻辑用 XML。 -
Agent 场景最佳实践:
- LLM 返回简单条件(如
name=张三、age>18):用LambdaQueryWrapper,代码简洁。 - LLM 返回复杂条件(如
IN (1,2,3)、BETWEEN 10 AND 20):用 XML 动态 SQL,保证 SQL 性能。 - 监控与优化:通过 MyBatis 插件统计 SQL 执行时间,优化慢查询。
3️⃣ Key Differences
4、容易一起考的题¶
ResultMap 高级映射¶
1、基础题:MyBatis 的 ResultMap 和 resultType 有什么区别?什么时候必须用 ResultMap?¶
难度级别:⭐⭐(自动映射 vs 手动映射、字段名不一致、复杂映射场景)
1️⃣ Common Answer resultType 是自动映射,数据库字段名和 Java 属性名一样就能直接映射。ResultMap 是手动映射,需要自己配置字段对应关系。如果字段名不一样或者需要关联查询,就用 ResultMap。
2️⃣ Impressive Answer
-
resultType 自动映射:
resultType="com.example.User"基于 Java Bean 规范,通过反射自动映射,要求数据库列名和 Java 属性名完全一致(或遵循驼峰命名转换)。适用场景:简单查询、单表映射、字段名规范。 -
ResultMap 手动映射:
<resultMap id="userMap" type="User">精确控制字段映射关系,通过<result column="db_name" property="javaName"/>指定映射。必须用 ResultMap 的场景:字段名不一致、多表关联、复杂类型映射、需要延迟加载。 -
高级映射能力:ResultMap 支持
association(一对一)、collection(一对多)、discriminator(鉴别器)等高级映射,能解决复杂对象关系映射问题。resultType 无法处理这些场景。 -
性能差异:resultType 性能略优(直接反射),ResultMap 需要解析配置;但 ResultMap 的可维护性更好,字段变更只需修改配置,不改 Java 代码。
3️⃣ Key Differences
2、进阶题:ResultMap 的 association(一对一)和 collection(一对多)如何配置?嵌套查询和嵌套结果的区别?¶
难度级别:⭐⭐⭐(嵌套查询 vs 嵌套结果、N+1 问题、延迟加载)
1️⃣ Common Answer association 用在一对一,collection 用在一对多。嵌套查询是再查一次数据库,嵌套结果是一次查询把数据都查出来。嵌套查询可能会有 N+1 问题,所以一般用嵌套结果。
2️⃣ Impressive Answer
-
association 一对一配置:
<association property="tool" javaType="Tool" column="tool_id" select="selectToolById"/>嵌套查询方式,或<association property="tool" resultMap="toolMap"/>嵌套结果方式,通过resultMap复用映射规则。 -
collection 一对多配置:
<collection property="params" ofType="ToolParam" column="tool_id" select="selectParamsByToolId"/>嵌套查询,或<collection property="params" resultMap="paramMap"/>嵌套结果,一对多返回 List。 -
嵌套查询 vs 嵌套结果:嵌套查询执行多条 SQL(主查询 + N 条子查询),N+1 问题:查 100 条工具调用记录,会执行 101 条 SQL;嵌套结果执行一条 SQL,通过 JOIN 查询,性能更好,但 SQL 复杂度增加。
-
延迟加载优化:嵌套查询配合
lazyLoadingEnabled="true",只有在访问关联对象时才执行子查询,减少不必要的查询。Agent 场景:工具调用记录列表只显示基本信息,点击详情才加载工具详情和参数,延迟加载避免性能浪费。
3️⃣ Key Differences
3、场景题:Agent 的工具调用记录需要关联查询工具详情和参数列表(一对多),如何用 ResultMap 优雅映射?¶
难度级别:⭐⭐⭐(复杂映射、性能优化、Agent 场景)
1️⃣ Common Answer 可以写一个 ResultMap,用 association 关联工具详情,用 collection 关联参数列表。查询的时候用 JOIN 把表连起来,一次查询把数据都查出来。
2️⃣ Impressive Answer
- ResultMap 配置:
<resultMap id="toolCallMap" type="ToolCall">
<id property="id" column="call_id"/>
<result property="toolName" column="tool_name"/>
<association property="tool" javaType="Tool" column="tool_id" select="selectToolById"/>
<collection property="params" ofType="ToolParam" column="call_id" select="selectParamsByCallId"/>
</resultMap>
- 嵌套结果优化:为避免 N+1 问题,使用嵌套结果方式,一条 SQL 查询所有数据:
SELECT c.id as call_id, c.tool_name, t.*, p.*
FROM tool_call c
LEFT JOIN tool t ON c.tool_id = t.id
LEFT JOIN tool_param p ON c.id = p.call_id
WHERE c.session_id = #{sessionId}
-
Agent 场景性能优化:工具调用记录查询频率高,参数列表只在详情页展示,使用延迟加载:
<collection fetchType="lazy" .../>,列表页不加载参数,减少数据传输量。缓存策略:工具详情用二级缓存,避免重复查询。 -
分页处理:一对多关联查询时,主记录分页需要特殊处理(如使用
DISTINCT或子查询),避免分页数据不准确。Agent 最佳实践:工具调用列表只返回主记录,参数列表通过独立接口按需加载。
3️⃣ Key Differences
4、容易一起考的题¶
MyBatis 批量操作优化¶
1、基础题:MyBatis 如何实现批量插入?foreach 标签和 ExecutorType.BATCH 有什么区别?¶
难度级别:⭐⭐(批量插入方式、性能对比、SQL 长度限制)
1️⃣ Common Answer 批量插入可以用 foreach 标签,把多条数据拼成一条 INSERT 语句。或者用 ExecutorType.BATCH,循环调用 insert 方法,MyBatis 会自动批量执行。foreach 性能好一些。
2️⃣ Impressive Answer
-
foreach 标签批量插入:
INSERT INTO table (col1, col2) VALUES <foreach item="item" collection="list" separator=",">(#{item.col1}, #{item.col2})</foreach>,生成一条长 SQL,性能最优(一次网络交互),但有 SQL 长度限制(MySQL 默认 4MB)。 -
ExecutorType.BATCH 批量:
sqlSessionFactory.openSession(ExecutorType.BATCH)开启批处理,循环调用mapper.insert(),MyBatis 缓存 SQL 和参数,最后session.flushStatements()一次性提交。优点:无 SQL 长度限制,适合大批量数据;缺点:性能略低于 foreach。 -
性能对比:foreach 一次网络交互,性能最佳(1000 条数据约 50ms);BATCH 多次网络交互但批量提交,性能次之(1000 条约 100ms);普通循环插入性能最差(1000 条约 2000ms)。
-
Agent 场景选择:批量保存 100+ 条消息记录,优先用 foreach(性能最佳);批量保存 10000+ 条历史归档数据,用 BATCH(避免 SQL 过长)。注意:BATCH 模式下,
insert()返回值不准确(可能返回 -1 或 0),需用session.flushStatements()获取实际影响行数。
3️⃣ Key Differences
2、进阶题:MyBatis 的 BatchExecutor 和 ReuseExecutor 的工作原理是什么?如何选择合适的 Executor 类型?¶
难度级别:⭐⭐⭐(Executor 类型、批处理原理、Statement 复用)
1️⃣ Common Answer BatchExecutor 是批量执行,ReuseExecutor 是复用 Statement。BatchExecutor 适合批量插入,ReuseExecutor 适合重复执行相同的 SQL。一般用默认的 SimpleExecutor 就行。
2️⃣ Impressive Answer
-
SimpleExecutor(默认):每次执行 SQL 都创建新的
PreparedStatement,执行完立即关闭。适用场景:普通 CRUD 操作,SQL 不重复,简单直接。 -
ReuseExecutor:内部维护
Map<String, Statement>,相同 SQL 复用同一个PreparedStatement,只重新设置参数。原理:StatementId = SQL + 数据库作为 key,执行时先从缓存获取,不存在则创建。适用场景:循环执行相同 SQL(如批量更新不同记录),减少 PreparedStatement 创建开销。 -
BatchExecutor:批量执行 SQL,内部维护
List<BatchResult>,每次update/insert将 SQL 和参数加入批处理队列,flushStatements()时一次性提交。原理:JDBC 的addBatch()+executeBatch(),减少网络交互。适用场景:大批量插入/更新(如 Agent 消息记录归档)。 -
选择策略:普通操作用 SimpleExecutor;循环执行相同 SQL 用 ReuseExecutor;大批量操作用 BatchExecutor。Spring 配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="configuration"><bean class="org.apache.ibatis.session.Configuration"><property name="defaultExecutorType" value="BATCH"/></bean></property></bean>。
3️⃣ Key Differences
3、场景题:Agent 每次对话结束需要批量保存 100+ 条消息记录,如何用 MyBatis 实现高性能批量写入?¶
难度级别:⭐⭐⭐(批量优化、性能调优、Agent 场景)
1️⃣ Common Answer 可以用 foreach 标签,把 100 条消息拼成一条 INSERT 语句,一次性插入数据库。或者用 ExecutorType.BATCH,循环调用 insert 方法,然后 flushStatements。
2️⃣ Impressive Answer
- foreach 批量插入:
<insert id="batchInsertMessages">
INSERT INTO agent_message (session_id, role, content, create_time)
VALUES
<foreach collection="messages" item="msg" separator=",">
(#{msg.sessionId}, #{msg.role}, #{msg.content}, #{msg.createTime})
</foreach>
</insert>
性能:100 条数据约 50ms,一次网络交互,最优方案。
- BATCH 模式兜底:如果消息量超过 1000 条(SQL 长度超限),切换到 BATCH 模式:
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
AgentMessageMapper mapper = batchSession.getMapper(AgentMessageMapper.class);
for (AgentMessage msg : messages) {
mapper.insert(msg);
}
batchSession.flushStatements();
batchSession.commit();
} finally {
batchSession.close();
}
-
Agent 场景优化:消息记录按
session_id分表,避免单表过大;批量插入前去重校验,避免重复插入相同消息;异步化处理:对话结束后异步批量保存,不阻塞用户响应。 -
性能监控:通过 MyBatis 插件统计批量操作耗时,设置超时告警(如 100 条消息超过 100ms 告警);连接池调优:批量操作占用连接时间长,适当增加连接池最大连接数(如从 10 增加到 20)。
3️⃣ Key Differences
4、容易一起考的题¶
MyBatis 与 Spring 事务集成¶
1、基础题:MyBatis 的 SqlSession 和 Spring 的事务管理器是如何集成的?¶
难度级别:⭐⭐(事务管理器、SqlSessionTemplate、事务传播)
1️⃣ Common Answer Spring 提供了 SqlSessionTemplate,它封装了 SqlSession,可以在 Spring 事务中使用。配置 DataSourceTransactionManager 事务管理器,用 @Transactional 注解开启事务。
2️⃣ Impressive Answer
-
SqlSessionTemplate 的作用:
SqlSessionTemplate是线程安全的 SqlSession 封装,内部代理了DefaultSqlSession,确保每次操作在事务内共享同一个 SqlSession,非事务时创建新 SqlSession。核心方法:getMapper()、selectList()、insert()等,自动处理事务边界。 -
事务管理器集成:
DataSourceTransactionManager管理事务,通过@Transactional注解开启事务;事务开始时,SqlSessionUtils创建 SqlSession 并绑定到 ThreadLocal;事务提交/回滚时,自动关闭 SqlSession。 -
事务传播行为:
@Transactional(propagation = Propagation.REQUIRED)默认行为,加入已有事务或创建新事务;REQUIRES_NEW挂起当前事务,创建新事务;SUPPORTS有事务则加入,无事务则非事务执行。Agent 场景:工具调用链中,多个 Mapper 操作默认 REQUIRED,确保原子性。 -
配置示例:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
3️⃣ Key Differences
2、进阶题:SqlSessionTemplate 和 DefaultSqlSession 的区别是什么?Spring 如何保证同一事务内使用同一个 SqlSession?¶
难度级别:⭐⭐⭐(SqlSession 管理、ThreadLocal、事务同步)
1️⃣ Common Answer SqlSessionTemplate 是 Spring 提供的,DefaultSqlSession 是 MyBatis 原生的。SqlSessionTemplate 是线程安全的,DefaultSqlSession 不是。Spring 用 ThreadLocal 保证同一事务内用同一个 SqlSession。
2️⃣ Impressive Answer
-
DefaultSqlSession:MyBatis 原生实现,非线程安全,每次操作创建新实例,用完必须关闭。生命周期:方法级别,不能跨方法共享。
-
SqlSessionTemplate:Spring 封装,线程安全,内部通过动态代理代理
DefaultSqlSession,每次操作前检查 ThreadLocal 是否有 SqlSession,有则复用,无则创建。核心机制:SqlSessionInterceptor拦截方法调用,委托给SqlSessionUtils.getSession()获取 SqlSession。 -
ThreadLocal 绑定机制:
SqlSessionUtils.getSession()内部调用TransactionSynchronizationManager.getResource(dataSource),从 ThreadLocal 获取当前事务绑定的 SqlSession;事务开始时,SqlSessionUtils.registerSession()将 SqlSession 绑定到 ThreadLocal;事务结束时,SqlSessionUtils.closeSqlSession()清理 ThreadLocal。 -
事务同步管理:
TransactionSynchronizationManager维护事务同步资源(DataSource → SqlSession),确保同一事务内所有 Mapper 操作共享同一个 SqlSession,一级缓存生效;事务提交后,自动关闭 SqlSession,清理资源。
3️⃣ Key Differences
3、场景题:Agent 工具调用链中,多个 Mapper 操作需要在同一个事务内完成,如何确保事务一致性?¶
难度级别:⭐⭐⭐(事务一致性、异常处理、Agent 场景)
1️⃣ Common Answer 用 @Transactional 注解开启事务,多个 Mapper 操作在同一个方法里调用,如果有异常就回滚。事务传播用默认的 REQUIRED 就行。
2️⃣ Impressive Answer
- 事务配置:
@Transactional(rollbackFor = Exception.class)
public ToolCallResult executeToolChain(ToolChainRequest request) {
// 1. 保存工具调用记录
toolCallMapper.insert(toolCall);
// 2. 执行工具调用
ToolResult result = toolExecutor.execute(toolCall);
// 3. 更新调用结果
toolCallMapper.updateStatus(toolCall.getId(), "SUCCESS", result);
// 4. 保存工具参数
toolParamMapper.batchInsert(params);
return result;
}
关键点:rollbackFor = Exception.class 确保所有异常回滚,默认只回滚 RuntimeException。
-
事务传播策略:工具调用链涉及多个 Service 方法,使用
@Transactional(propagation = Propagation.REQUIRED)确保加入同一事务;如果需要独立事务(如日志记录),用REQUIRES_NEW挂起当前事务。 -
异常处理:工具调用失败时抛出
ToolExecutionException,触发事务回滚;注意:try-catch捕获异常后不会回滚,需手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。 -
Agent 场景优化:工具调用链可能耗时较长(如 LLM 生成),设置事务超时
@Transactional(timeout = 30),避免长时间占用数据库连接;异步化处理:非核心操作(如日志记录)异步执行,减少事务时间。
3️⃣ Key Differences
4、容易一起考的题¶
MyBatis 配置优化与调优¶
1、基础题:MyBatis 的 settings 配置有哪些常用项?cacheEnabled、lazyLoadingEnabled 分别控制什么?¶
难度级别:⭐⭐(settings 配置、缓存控制、延迟加载)
1️⃣ Common Answer settings 是 MyBatis 的配置项,可以配置缓存、延迟加载等。cacheEnabled 控制二级缓存是否开启,lazyLoadingEnabled 控制是否延迟加载关联对象。
2️⃣ Impressive Answer
- 常用 settings 配置:
cacheEnabled="true":全局开启二级缓存(默认 true),可被 Mapper 级别覆盖lazyLoadingEnabled="true":全局开启延迟加载(默认 false),按需加载关联对象aggressiveLazyLoading="false":按需加载(默认 true,加载任一属性则加载所有)multipleResultSetsEnabled="true":允许单条语句返回多个结果集(默认 true)useColumnLabel="true":使用列标签代替列名(默认 true)-
defaultExecutorType="SIMPLE":默认执行器类型(SIMPLE/REUSE/BATCH) -
cacheEnabled 控制二级缓存:
cacheEnabled="true"时,Mapper XML 中配置<cache/>生效,多个 SqlSession 共享缓存;cacheEnabled="false"时,即使配置<cache/>也不生效。Agent 场景:知识库查询开启二级缓存,减少 DB 压力。 -
lazyLoadingEnabled 控制延迟加载:
lazyLoadingEnabled="true"时,ResultMap 中的association和collection按需加载,访问关联对象时才执行子查询;lazyLoadingEnabled="false"时,立即加载所有关联对象。性能优化:工具调用记录列表页,参数列表延迟加载,减少数据传输。 -
配置示例:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="defaultExecutorType" value="REUSE"/>
</settings>
3️⃣ Key Differences
2、进阶题:MyBatis 的连接池(PooledDataSource)工作原理是什么?如何调优连接池参数?¶
难度级别:⭐⭐⭐(连接池原理、参数调优、性能优化)
1️⃣ Common Answer MyBatis 自带了连接池,可以复用数据库连接,避免频繁创建和销毁连接。可以配置最大连接数、初始连接数等参数。
2️⃣ Impressive Answer
-
PooledDataSource 工作原理:内部维护
idleConnections(空闲连接池)和activeConnections(活跃连接池),请求连接时先从空闲池获取,无空闲则创建新连接(不超过最大连接数);连接释放时归还到空闲池,超过最大空闲数则关闭。 -
核心参数:
poolMaximumActiveConnections:最大活跃连接数(默认 10),超过则等待poolMaximumIdleConnections:最大空闲连接数(默认 10),超过则关闭poolMaximumCheckoutTime:最大等待时间(默认 20000ms),超时抛异常poolTimeToWait:等待重试间隔(默认 20000ms)poolPingQuery:连接检测 SQL(默认 "SELECT 1"),检测连接有效性-
poolPingEnabled:是否开启连接检测(默认 false) -
调优策略:
- 高峰期调优:Agent 服务 QPS 1000+,设置
poolMaximumActiveConnections=50,避免连接耗尽 - 空闲连接优化:低峰期设置
poolMaximumIdleConnections=5,减少资源占用 - 连接检测:
poolPingEnabled=true,poolPingQuery="SELECT 1",避免使用失效连接 -
等待时间:
poolMaximumCheckoutTime=5000ms,快速失败,避免长时间阻塞 -
Agent 场景优化:工具调用链耗时较长,适当增加
poolMaximumCheckoutTime到 10000ms;知识库查询频率高,增加poolMaximumActiveConnections到 30;监控连接池指标(活跃连接数、等待队列长度),动态调整参数。
3️⃣ Key Differences
3、场景题:Agent 服务高峰期数据库连接耗尽,如何通过 MyBatis 和连接池配置优化解决?¶
难度级别:⭐⭐⭐(连接耗尽问题、配置优化、监控告警)
1️⃣ Common Answer 增加连接池的最大连接数,减少连接等待时间。优化 SQL 查询,减少查询时间。用连接池监控工具,看连接使用情况。
2️⃣ Impressive Answer
- 连接池配置优化:
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/agent_db"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
<property name="poolMaximumActiveConnections" value="50"/>
<property name="poolMaximumIdleConnections" value="10"/>
<property name="poolMaximumCheckoutTime" value="10000"/>
<property name="poolPingEnabled" value="true"/>
<property name="poolPingQuery" value="SELECT 1"/>
</dataSource>
关键点:增加最大连接数到 50,等待时间 10 秒,开启连接检测。
- MyBatis 配置优化:
- 二级缓存:知识库查询开启二级缓存,减少 DB 连接占用
- 延迟加载:工具调用记录参数列表延迟加载,减少长事务
- Executor 类型:批量操作用
BATCH,减少连接占用时间 -
超时设置:
defaultStatementTimeout=5000ms,避免慢查询占用连接 -
SQL 优化:
- 索引优化:为
session_id、tool_id等高频查询字段添加索引 - 分页优化:大数据量查询使用分页,避免全表扫描
-
批量操作:消息记录批量插入,减少连接次数
-
监控告警:
- 连接池监控:通过 JMX 或 Prometheus 监控活跃连接数、等待队列长度
- 慢查询告警:设置慢查询阈值(5 秒),超过则告警
- 连接泄露检测:定期检查活跃连接数,发现异常增长告警
3️⃣ Key Differences
4、容易一起考的题¶
MyBatis Generator 与代码生成¶
1、基础题:MyBatis Generator(MBG)能生成哪些文件?如何配置?¶
难度级别:⭐⭐(代码生成、配置文件、生成内容)
1️⃣ Common Answer MBG 可以生成 Mapper 接口、Mapper XML、实体类。配置一个 generatorConfig.xml 文件,指定数据库连接、要生成的表,然后运行 MBG 就能生成代码。
2️⃣ Impressive Answer
- MBG 生成内容:
- 实体类:
User.java,包含所有字段,支持@Generated注解 - Mapper 接口:
UserMapper.java,包含insert、updateByPrimaryKey、selectByPrimaryKey、deleteByPrimaryKey等基础方法 - Mapper XML:
UserMapper.xml,包含 SQL 映射配置 -
Example 类:
UserExample.java,用于动态构建查询条件(如WHERE id = ? AND name LIKE ?) -
配置文件 generatorConfig.xml:
<generatorConfiguration>
<context id="mysql" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/agent_db"
userId="root" password="password"/>
<javaModelGenerator targetPackage="com.example.model" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java"/>
<table tableName="tool_call" domainObjectName="ToolCall"/>
</context>
</generatorConfiguration>
- 运行方式:
- 命令行:
java -jar mybatis-generator-core-x.x.x.jar -configfile generatorConfig.xml - Maven 插件:
mvn mybatis-generator:generate -
Java 代码:
GeneratorGenerator.generate(null, config, null, null, null, true) -
生成策略:
<table schema="agent_db" tableName="tool_call" domainObjectName="ToolCall" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"/>禁用 Example 类,只生成基础 CRUD。
3️⃣ Key Differences
2、进阶题:MBG 生成的 Example 类是什么?如何自定义 MBG 插件扩展生成逻辑?¶
难度级别:⭐⭐⭐(Example 类、插件机制、自定义扩展)
1️⃣ Common Answer Example 类是用来构建查询条件的,可以动态拼接 WHERE 条件。MBG 支持插件,可以自定义生成逻辑,比如生成注释、自定义方法等。
2️⃣ Impressive Answer
-
Example 类的作用:
UserExample example = new UserExample();example.createCriteria().andIdEqualTo(1).andNameLike("%test%");动态构建查询条件,避免手写 XML SQL。核心类:Criteria(内部类,构建 AND 条件)、Criterion(条件对象,封装字段、操作符、值)。 -
Example 使用场景:
- 动态查询:Agent 查询工具调用记录,按
session_id、tool_name、status等条件组合查询 - 复杂条件:
example.or().andCreateTimeBetween(startTime, endTime);支持 OR 条件 -
排序分页:
example.setOrderByClause("create_time DESC");排序,配合 PageHelper 分页 -
自定义 MBG 插件:
public class CommentPlugin extends PluginAdapter {
@Override
public boolean modelFieldGenerated(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn, PluginModel.PluginModelClass pluginModelClass, PluginModel.PluginModelField pluginModelField) {
field.addJavaDocLine("/** " + introspectedColumn.getRemarks() + " */");
return true;
}
}
配置插件:<plugin type="com.example.CommentPlugin"/>,为生成的字段添加数据库注释。
- 扩展生成逻辑:
- 自定义方法:继承
IntrospectedTableMyBatis3Impl,重写generateBaseRecordClass(),添加自定义方法 - 自定义模板:修改
mapperGenerator.ftl模板,生成自定义 Mapper 方法 - MyBatis-Plus 代码生成器:
AutoGenerator generator = new AutoGenerator();generator.setStrategy(new StrategyConfig().setInclude("tool_call"));支持生成 Service、Controller 等更多文件。
3️⃣ Key Differences
3、场景题:Agent 项目需要快速接入新的数据表,如何用 MBG 或 MyBatis-Plus 代码生成器提升开发效率?¶
难度级别:⭐⭐⭐(代码生成、开发效率、Agent 场景)
1️⃣ Common Answer 用 MBG 生成 Mapper 接口和实体类,然后手写业务逻辑。或者用 MyBatis-Plus 的代码生成器,生成 Service 和 Controller,直接用。
2️⃣ Impressive Answer
- MyBatis-Plus 代码生成器:
AutoGenerator generator = new AutoGenerator();
generator.setDataSource(new DataSourceConfig.Builder("jdbc:mysql://localhost:3306/agent_db", "root", "password").build());
generator.setGlobalConfig(new GlobalConfig.Builder().outputDir("src/main/java").author("agent").build());
generator.setPackageInfo(new PackageConfig.Builder().parent("com.example.agent").moduleName("tool").build());
generator.setStrategy(new StrategyConfig.Builder().addInclude("tool_call", "tool_param").entityBuilder().enableLombok().build());
generator.execute();
生成内容:Entity、Mapper、Mapper XML、Service、ServiceImpl、Controller,完整 CRUD 代码。
- Agent 场景快速接入:
- 新表接入:Agent 新增
agent_log表,运行代码生成器,5 分钟内生成完整 CRUD 代码 - 自定义模板:修改
controller.java.ftl,添加 Agent 特有的方法(如@PostMapping("/execute")工具执行接口) -
批量生成:一次生成多个表(
addInclude("tool_call", "tool_param", "agent_log")),统一代码风格 -
开发效率提升:
- 减少重复代码:自动生成基础 CRUD,专注业务逻辑开发
- 统一代码规范:通过模板统一代码风格,降低维护成本
-
快速迭代:Agent 功能快速扩展,新增数据表即时生成代码
-
最佳实践:
- 版本控制:生成代码纳入 Git,避免重复生成覆盖修改
- 增量生成:只生成新表,已生成的表手动维护
- 自定义扩展:在生成代码基础上添加业务逻辑,不修改生成模板
3️⃣ Key Differences