list解析 mybatis_mybatis-session.selectList源码分析

本文详细介绍了MyBatis的查询流程。首先构建SqlSessionFactory并获取sqlSession对象,接着创建执行器,默认返回SimpleExecutor。使用sqlSession的selectList方法查询时,会经过一系列步骤,如获取MappedStatement、创建缓存Key等,还涉及StatementHandler、ResultSetHandler等的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0.构建工厂:SqlSessionFactory 。 new SqlSessionFactoryBuilder.build(配置的xml文件)

获取sqlSession对象

//指定事务隔离级别

1. sqlMapper.openSession(TransactionIsolationLevel.SERIALIZABLE)

openSession->openSessionFromDataSource->DefaultSqlSession

openSession->getTransactionFactoryFromEnvironment

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null;

try {

final Environment environment = configuration.getEnvironment();

final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

//通过事务工厂来产生一个事务

tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

//生成一个执行器(事务包含在执行器里)

final Executor executor = configuration.newExecutor(tx, execType);

//然后产生一个DefaultSqlSession

return new DefaultSqlSession(configuration, executor, autoCommit);

} catch (Exception e) {

//如果打开事务出错,则关闭它

closeTransaction(tx); // may have fetched a connection so lets call close()

throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);

} finally {

//最后清空错误上下文

ErrorContext.instance().reset();

}

}

创建执行器 Executor (BatchExecutor,ReuseExecutor,SimpleExecutor,CachingExecutor)

默认是有缓存的,返回SimpleExecutor,并使用插件plugin,可以用来改变executor的行为

返回DefaultSqlSession;

使用sqlSession中对应的方法

2. session.selectList(指定的Mapper中的方法eg:selectAllAuthors)

selectList->executor.query->query

selectList-> configuration.getMappedStatement(statement)

public List selectList(String statement, Object parameter, RowBounds rowBounds) {

try {

//根据statement id找到对应的MappedStatement

MappedStatement ms = configuration.getMappedStatement(statement);

//转而用执行器来查询结果,注意这里传入的ResultHandler是null

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);

} finally {

ErrorContext.instance().reset();

}

}

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {

//先构建所有语句,再返回语句

if (validateIncompleteStatements) {

buildAllStatements();

}

return mappedStatements.get(id);

}

public MappedStatement getMappedStatement(String id) {

return this.getMappedStatement(id, true);

}

query->ms.getBoundSql->sqlSource.getBoundSql

query->createCacheKey

query->query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

BoundSql boundSql = ms.getBoundSql(parameterObject);

//query时传入一个cachekey参数

CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

public BoundSql getBoundSql(Object parameterObject) {

//其实就是调用sqlSource.getBoundSql

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

//剩下的可以暂时忽略

List parameterMappings = boundSql.getParameterMappings();

if (parameterMappings == null || parameterMappings.isEmpty()) {

boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);

}

// check for nested result maps in parameter mappings (issue #30)

for (ParameterMapping pm : boundSql.getParameterMappings()) {

String rmId = pm.getResultMapId();

if (rmId != null) {

ResultMap rm = configuration.getResultMap(rmId);

if (rm != null) {

hasNestedResultMaps |= rm.hasNestedResultMaps();

}

}

}

return boundSql;

}

public BoundSql getBoundSql(Object parameterObject) {

return new BoundSql(configuration, sql, parameterMappings, parameterObject);

}

//创建缓存Key

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {

if (closed) {

throw new ExecutorException("Executor was closed.");

}

CacheKey cacheKey = new CacheKey();

//MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码

cacheKey.update(ms.getId());

cacheKey.update(Integer.valueOf(rowBounds.getOffset()));

cacheKey.update(Integer.valueOf(rowBounds.getLimit()));

cacheKey.update(boundSql.getSql());

List parameterMappings = boundSql.getParameterMappings();

TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

// mimic DefaultParameterHandler logic

//模仿DefaultParameterHandler的逻辑,不再重复,请参考DefaultParameterHandler

for (int i = 0; i < parameterMappings.size(); i++) {

ParameterMapping parameterMapping = parameterMappings.get(i);

if (parameterMapping.getMode() != ParameterMode.OUT) {

Object value;

String propertyName = parameterMapping.getProperty();

if (boundSql.hasAdditionalParameter(propertyName)) {

value = boundSql.getAdditionalParameter(propertyName);

} else if (parameterObject == null) {

value = null;

} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

value = parameterObject;

} else {

MetaObject metaObject = configuration.newMetaObject(parameterObject);

value = metaObject.getValue(propertyName);

}

cacheKey.update(value);

}

}

if (configuration.getEnvironment() != null) {

// issue #176

cacheKey.update(configuration.getEnvironment().getId());

}

return cacheKey;

}

query-> delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

throws SQLException {

Cache cache = ms.getCache();

//默认情况下是没有开启缓存的(二级缓存).要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:

//简单的说,就是先查CacheKey,查不到再委托给实际的执行器去查

if (cache != null) {

flushCacheIfRequired(ms);

if (ms.isUseCache() && resultHandler == null) {

ensureNoOutParams(ms, parameterObject, boundSql);

@SuppressWarnings("unchecked")

List list = (List) tcm.getObject(cache, key);

if (list == null) {

list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

tcm.putObject(cache, key, list); // issue #578 and #116

}

return list;

}

}

return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

query-> queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql)

@SuppressWarnings("unchecked")

@Override

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

//如果已经关闭,报错

if (closed) {

throw new ExecutorException("Executor was closed.");

}

//先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用

if (queryStack == 0 && ms.isFlushCacheRequired()) {

clearLocalCache();

}

List list;

try {

//加一,这样递归调用到上面的时候就不会再清局部缓存了

queryStack++;

//先根据cachekey从localCache去查

list = resultHandler == null ? (List) localCache.getObject(key) : null;

if (list != null) {

//若查到localCache缓存,处理localOutputParameterCache

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

//从数据库查

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

//清空堆栈

queryStack--;

}

if (queryStack == 0) {

//延迟加载队列中所有元素

for (DeferredLoad deferredLoad : deferredLoads) {

deferredLoad.load();

}

// issue #601

//清空延迟加载队列

deferredLoads.clear();

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

// issue #482

//如果是STATEMENT,清本地缓存

clearLocalCache();

}

}

return list;

}

queryFromDatabase->doQuery

//从数据库查

private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

List list;

//先向缓存中放入占位符???

localCache.putObject(key, EXECUTION_PLACEHOLDER);

try {

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

} finally {

//最后删除占位符

localCache.removeObject(key);

}

//加入缓存

localCache.putObject(key, list);

//如果是存储过程,OUT参数也加入缓存

if (ms.getStatementType() == StatementType.CALLABLE) {

localOutputParameterCache.putObject(key, parameter);

}

return list;

}

StatementHandler,

ResultSetHandler,

RoutingStatementHandler,

BaseStatementHandler,

PreparedStatementHandler

@Override

public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {

Statement stmt = null;

try {

Configuration configuration = ms.getConfiguration();

//新建一个StatementHandler

//这里看到ResultHandler传入了

StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

//准备语句

stmt = prepareStatement(handler, ms.getStatementLog());

//StatementHandler.query

return handler.query(stmt, resultHandler);

} finally {

closeStatement(stmt);

}

}

Configuration

//创建结果集处理器

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,

ResultHandler resultHandler, BoundSql boundSql) {

//创建DefaultResultSetHandler(稍老一点的版本3.1是创建NestedResultSetHandler或者FastResultSetHandler)

ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);

//插件在这里插入

resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

return resultSetHandler;

}

路由选择语句

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

//根据语句类型,委派到不同的语句处理器(STATEMENT|PREPARED|CALLABLE)

switch (ms.getStatementType()) {

case STATEMENT:

delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case PREPARED:

delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case CALLABLE:

delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

default:

throw new ExecutorException("Unknown statement type: " + ms.getStatementType());

}

}

BaseStatementHandler 语句处理类

//准备语句

@Override

public Statement prepare(Connection connection) throws SQLException {

ErrorContext.instance().sql(boundSql.getSql());

Statement statement = null;

try {

//实例化Statement

statement = instantiateStatement(connection);

//设置超时

setStatementTimeout(statement);

//设置读取条数

setFetchSize(statement);

return statement;

} catch (SQLException e) {

closeStatement(statement);

throw e;

} catch (Exception e) {

closeStatement(statement);

throw new ExecutorException("Error preparing statement. Cause: " + e, e);

}

}

PreparedStatementHandler

@Override

protected Statement instantiateStatement(Connection connection) throws SQLException {

//调用Connection.prepareStatement

String sql = boundSql.getSql();

if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {

String[] keyColumnNames = mappedStatement.getKeyColumns();

if (keyColumnNames == null) {

return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);

} else {

return connection.prepareStatement(sql, keyColumnNames);queryFromDatabase

}

} else if (mappedStatement.getResultSetType() != null) {

return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);

} else {

return connection.prepareStatement(sql);

}

}

转载至链接:https://my.oschina.net/iioschina/blog/1859699

<think>我们正在解决MyBatis中`selectOne`方法返回多个结果时抛出的`TooManyResultsException`问题。用户期望每次调用`selectOne`时只返回一个结果,但实际查询返回了两个或更多结果,导致异常。 ### 问题分析 1. **根本原因**:`selectOne`方法要求查询结果必须为0或1条记录。当SQL查询返回多于1条记录时,MyBatis会抛出`org.apache.ibatis.exceptions.TooManyResultsException`。 2. **常见场景**: - 查询条件不够严格(如未使用唯一索引字段) - 数据库数据重复(违反唯一约束) - SQL语句错误(如忘记写WHERE条件) ### 解决方案 #### 方法一:确保查询条件唯一性(推荐) 修改SQL语句,确保查询条件能唯一确定一条记录: ```xml <select id="selectUserById" resultType="User"> SELECT * FROM user WHERE id = #{id} <!-- id为主键,确保唯一 --> </select> ``` #### 方法二:使用LIMIT限制结果集 在无法修改数据唯一性的情况下,强制限制返回结果数量: ```xml <select id="selectOneUserByName" resultType="User"> SELECT * FROM user WHERE name = #{name} LIMIT 1 <!-- 仅返回第一条 --> </select> ``` > 注意:此方法可能返回非预期数据,需谨慎使用。 #### 方法三:修改DAO层方法签名 如果业务逻辑允许返回多个结果,应改用`selectList`方法: ```java // 接口方法修改 List<User> selectUsersByName(String name); ``` 对应XML: ```xml <select id="selectUsersByName" resultType="User"> SELECT * FROM user WHERE name = #{name} </select> ``` #### 方法四:数据库层面添加约束 若数据重复是设计缺陷,应通过数据库约束解决: ```sql ALTER TABLE user ADD UNIQUE (email); -- 为email字段添加唯一约束 ``` ### 错误示例分析 ```java User user = sqlSession.selectOne("selectUserByName", "Alice"); // 当有多个Alice时抛出异常 ``` 修正方案: 1. 精确查询条件(如同时使用姓名+手机号) 2. 捕获异常并处理: ```java try { return sqlSession.selectOne("selectUserByName", "Alice"); } catch (TooManyResultsException e) { logger.warn("找到多个同名用户,返回第一条"); return sqlSession.selectList("selectUserByName", "Alice").get(0); } ``` ### 预防措施 1. **单元测试验证**: ```java @Test public void testSelectOne() { User user = userDao.selectById(1); // 唯一查询 assertEquals(1, user.getId()); // 验证重复数据场景 assertThrows(TooManyResultsException.class, () -> { userDao.selectByName("Alice"); }); } ``` 2. **SQL审核**:使用MyBatis拦截器检查返回结果数量: ```java @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})) public class ResultSizeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 检查方法是否为selectOne,若是则验证结果数量 // ... } } ``` ### 高级技巧:使用ResultHandler自定义处理 ```java sqlSession.select("selectUsersByName", param, new ResultHandler<User>() { @Override public void handleResult(ResultContext<? extends User> context) { User user = context.getResultObject(); // 自定义处理逻辑,例如只取第一条 if (context.getResultCount() > 1) { context.stop(); // 停止继续获取结果 throw new TooManyResultsException("期望1条,实际多条"); } } }); ``` ### 总结处理流程 ```mermaid graph TD A[调用selectOne方法] --> B{结果数量检查} B -->|0条| C[返回null] B -->|1条| D[返回对象] B -->|多条| E[抛出TooManyResultsException] E --> F[解决方案] F --> G[修改SQL条件] F --> H[使用LIMIT 1] F --> I[改用selectList] ``` --- ### 相关问题 1. MyBatis中`selectList`和`selectOne`的性能差异是什么? 2. 如何全局拦截MyBatis异常进行统一处理? 3. MyBatis结果集映射有哪些最佳实践? 4. 在Spring Boot中如何配置MyBatis的日志输出? > 通过严格约束查询条件或显式限制结果数量,可有效解决`TooManyResultsException`问题。在业务设计阶段应优先考虑数据唯一性约束[^1]。</think>### 解决 MyBatis `selectOne()` 返回多个结果的问题 当使用 MyBatis 的 `selectOne()` 方法查询时,如果 SQL 返回了多个结果,会抛出 `TooManyResultsException` 异常。以下是完整的解决方案: --- #### 一、问题根本原因 `selectOne()` 方法要求查询结果**必须为 0 或 1 条记录**。当数据库中存在多条匹配记录时,MyBatis 会抛出: ```java org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2 ``` --- #### 二、解决方案 ##### 1. 确保查询条件唯一性(推荐) ```xml <!-- UserMapper.xml --> <select id="findUserByUniqueKey" resultType="User"> SELECT * FROM users WHERE unique_code = #{code} <!-- 使用唯一性字段 --> </select> ``` ```java // 调用方式 User user = userMapper.findUserByUniqueKey("A123"); ``` ##### 2. 添加 LIMIT 限制 ```xml <select id="findFirstUserByName" resultType="User"> SELECT * FROM users WHERE name = #{name} LIMIT 1 <!-- 强制只返回一条 --> </select> ``` ##### 3. 改用 `selectList()` 方法 ```java // Mapper 接口 List<User> findUsersByName(String name); // 调用方式 List<User> users = userMapper.findUsersByName("Alice"); User firstUser = users.isEmpty() ? null : users.get(0); ``` ##### 4. 数据库层去重 ```sql SELECT DISTINCT * FROM users WHERE department = 'IT' -- 去重查询 ``` ##### 5. 添加聚合函数 ```xml <select id="countUsers" resultType="int"> SELECT COUNT(*) FROM users WHERE role = #{role} </select> ``` --- #### 三、错误使用场景分析 ```java // ❌ 错误示例:非唯一字段查询 User user = sqlSession.selectOne("findUserByName", "Alice"); // ✅ 正确修改: // 方案1:改用 selectList List<User> users = sqlSession.selectList("findUserByName", "Alice"); // 方案2:增加唯一性条件 User user = sqlSession.selectOne("findUserByEmail", "alice@example.com"); ``` --- #### 四、预防措施 1. **数据库设计**: - 为主表添加主键约束 - 为业务唯一字段添加 `UNIQUE` 约束 ```sql ALTER TABLE users ADD UNIQUE (email); ``` 2. **MyBatis 拦截器监控**: ```java @Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })) public class ResultSizeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); if (result instanceof List && ((List<?>) result).size() > 1) { log.warn("⚠️ 查询返回多条结果: {}", invocation.getArgs()[0]); } return result; } } ``` --- #### 五、处理流程 ```mermaid graph TD A[调用 selectOne] --> B{结果数量检查} B -->|0条| C[返回null] B -->|1条| D[返回对象] B -->|多条| E[抛出TooManyResultsException] E --> F[解决方案] F --> G[修改查询条件] F --> H[使用LIMIT 1] F --> I[改用selectList] ``` --- ### 相关问题 1. MyBatis 中 `selectList` 和 `selectOne` 的性能差异是什么? 2. 如何配置 MyBatis 拦截器进行 SQL 监控? 3. MyBatis 结果集映射有哪些最佳实践? 4. 在 Spring Boot 中如何全局处理 MyBatis 异常? > 通过确保查询条件的唯一性或显式限制结果数量,可彻底解决此问题。在设计阶段优先考虑数据库约束是根本解决方案[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值