- Mybatis映射文件之 Select 元素使用及详细解析
- 一、Select元素的作用
- 二、Select元素的属性
- 二、Select使用示例
- 一、单表查询
- 二、一对多查询
- 三、多对多查询
- 四、MyBatis对Select元素的解析及使用
Select元素的作用很简单就是映射查询语句,当我们在MyBatis中写一个查询代码
相当于就是告诉MyBatis创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就类似下面的Java代码
String selectPerson = "SELECT * FROM PERSON WHERe ID=?"; PreparedStatement ps = conn.prepareStatement(selectPerson); ps.setInt(1,id);二、Select元素的属性
MyBatis 在查询和结果映射做了相当多的改进,为查询提供了很大的便利
| 属性 | 描述 |
|---|---|
| id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
| parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset) |
| resultType | 期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个 |
| resultMap | 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个 |
| flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false |
| useCache | 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true |
| timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动) |
| fetchSize | 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动) |
| statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED |
| resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动) |
| databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略 |
| resultOrdered | 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false |
| resultSets | 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔 |
数据准备,下面示例将以下面三张表作为基础数据举例:
Blog博客实体类:
@Data
public class Blog implements Serializable{
private Integer bid;
private String name;
private Integer authorId;
}
BlogAndComment博客评论实体类:
@Data
public class BlogAndComment implements Serializable {
private Integer bid;
private String name;
private Integer authorId;
private List comment;
}
AuthorAndBlog作者文章实体类:
@Data
public class AuthorAndBlog implements Serializable {
private Integer authorId;
private String authorName;
private List blog;
}
一、单表查询
查询bid=1的Blog信息
mapper映射文件:
测试类:
@Test
public void testQueryById() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
控制台输出:
查询文章bid = 1 的评论信息
mapper映射文件:
测试类:
@Test
public void testSelectBlogWithComment() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
BlogAndComment blog = mapper.selectBlogWithCommentById(1);
System.out.println(JSONObject.toJSONString(blog));
} finally {
session.close();
}
}
控制台输出:
查询作者的文章评论信息
mapper映射文件:
测试类:
@Test
public void testSelectAuthorWithBlog() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
List authors = mapper.selectAuthorWithBlog();
System.out.println(JSONObject.toJSONString(authors));
} finally {
session.close();
}
}
控制台输出:
从上面了解到了Select的基本使用,接下来我们看一看,MyBatis到底是如何将mapper映射文件中的Select解析为SQL执行的,接下来的分析已上述单表查询为例:
1. select元素的解析
当我们在使用SqlSessionFactoryBuilder的build方法 构建 SqlSessionFactory 的时候,会对Mybatis的核心配置文件进行解析
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
在SqlSessionFactoryBuilder的build方法中会使用 XMLConfigBuilder 的 parse()方法对配置文件进行解析
public class SqlSessionFactoryBuilder {
// ....
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
//.....
}
在 parse()方法中,parseConfiguration()方法会选取configuration根标签开始解析
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
mapperElement()方法对 mappers 标签进行解析
private void parseConfiguration(XNode root) {
try {
//....这里只看mappers的解析
mapperElement(root.evalNode("mappers"));
//....
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里我采用的是resource的配置方式,只看resource 方式的解析逻辑
到这里我们可以看到出现了XMLMapperBuilder 这个类,这个类就是专用于对Mapper.xml映射文件的解析,我们接着看他的parse()方法
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//.....
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//resource 的配置方式
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
//
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
}
//.....
}
}
}
public void parse() {
//判断是否重复加载
if (!configuration.isResourceLoaded(resource)) {
//从mapper标签开始解析Mapper.xml映射文件
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//绑定Mapper
bindMapperForNamespace();
}
//....
}
Mapper.xml映射文件解析,这里是不是又看到一个熟悉的错误,当我们在Mapper.xml映射文件中没有配置namespace属性的时候,会抛出BuilderException异常
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//省略其他元素解析.....
//解析select、insert、update、delete元素
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这里的一个XNode 就是对应的一个select 或insert 或update 或delete元素
private void buildStatementFromContext(Listlist) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); }
private void buildStatementFromContext(Listlist, String requiredDatabaseId) { //循环各个节点,将其构建成一个个MappedStatement对象 for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
解析元素中的各个属性,拿到其属性值,用于构建 MappedStatement 对象
public void parseStatementNode() {
//省略部分属性...
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
//省略部分属性...
builderAssistant.addMappedStatement(id, sqlSource, statementType,, resultSets);
}
public MappedStatement addMappedStatement(String id, SqlSource sqlSource,StatementType statementType,
,String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
//构建MappedStatement 的id,namespace + 方法名,后面会通过id来获取MappedStatement 对象
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
到这里Mapper.xml中的Select元素解析已经完成了,可以看到MyBatis最后将一个Select元素解析封装成了一个MappedStatement 对象保存在全局的configuration对象的mappedStatements 属性中,紧接着看bindMapperForNamespace() Mapper的绑定方法
```java
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
//创建Mapper接口实例
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
//注册Mapper
configuration.addMapper(boundType);
}
}
}
看到这里是不是很熟悉,当已经注册过的Mapper会抛出绑定异常,
publicvoid addMapper(Class type) { //不是接口直接忽略 if (type.isInterface()) { //判断knownMappers中是否已注册 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } //.... //添加映射 knownMappers.put(type, new MapperProxyFactory<>(type)); //..... } } }
到这里,mappers的解析已经完成了,最后MyBatis将所有mapper放在了MapperRegistry 的knownMappers (k,v) 中,其key就是mapper接口对应的Class类实例,value对应的是一个 MpperProxyFactory工厂对象。我们接着看查询的时候,是如何使用的:
//以下是部分上面测试类代码
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
//....
} finally {
session.close();
}
从代码中可以看到 getMapper()这个方法,传入了一个 BlogMapper.class又返回了一个 BlogMapper对象,这个操作有没有觉得有点奇怪,传入一个Mapper,又返回一个mapper,我们详细看一下:
在SqlSessionFactory的构建中,最后返回的是默认的DefaultSqlSessionFactory,所以这里的openSession() 会走DefaultSqlSessionFactory的openSession()方法,最后返回的SqlSession是DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
//.....
@Override
public T getMapper(Class type) {
return configuration.getMapper(type, this);
}
//.....
}
public class Configuration {
//.....
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//.....
}
传入Class接口的Class对象,获取到MapperProxyFactory工厂对象,调用了它的newInstance()方法
public class MapperRegistry {
//....
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//....
}
这里newInstance方法中使用了JDK的代理模式,这里的MapperProxy就是实现了InvocationHandler的触发管理类
public class MapperProxyFactory{ protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { //创建MapperInterface对应的MapperProxy 对象,比如mapperInterface是 BlogMapper.class,就创建MapperProxy final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
这里断点验证看到newInstance()方法执行完确实是返回的是一个Mapper代理对象
这里通过JDK的动态代理,返回了一个代理对象,也就明白了为什么传入一个Mapper后又返回了一个代理的Mapper对象,接下来该Mapper的所有方法执行,都会执行MapperProxy中invoke方法的逻辑
public class MapperProxyimplements InvocationHandler, Serializable { //..... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果是Object的方法就放行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { //像上面调用demo中的 selectBlogById 方法都会在这里开始执行 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } //..... }
到这里梳理一下,首先MyBatis通过XMLConfigBuilder对核心配置文件中的mappers标签进行解析,再通过XMLMapperBuilder对其子标签配置的 Mapper.xml映射文件进行解析,在解析Mapper.xml映射文件的过程中,通过XMLStatementBuilder将select、insert、update、delete元素解析后封装为一个个MappedStatement对象,保存在全局的configuration对象的mappedStatements中。之后又将Mapper的Class实例与MapperProxyFactory工厂对象进行绑定,Mapper.xml映射文件解析完成之后,在调用getMapper(Class type,SqlSession sqlSession)时,通过mapper的Class实例,获取到对应的MapperProxyFactory,调用其newInstance()方法,获取到Mapper的代理对象,最后Mapper的方法都会调用MapperProxy触发管理类的invoke()方法来执行。
以上就是对Select元素的简单解析了,如有错误欢迎指出,希望对你有点帮助!



