Mybatis映射文件之 Select 元素使用及详细解析
文章目录
- Mybatis映射文件之 Select 元素使用及详细解析
- 一、Select元素的作用
- 二、Select元素的属性
- 二、Select使用示例
- 一、单表查询
- 二、一对多查询
- 三、多对多查询
- 四、MyBatis对Select元素的解析及使用
一、Select元素的作用
Select元素的作用很简单就是映射查询语句,当我们在MyBatis中写一个查询代码
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
相当于就是告诉MyBatis创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就类似下面的Java代码
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
二、Select元素的属性
MyBatis 在查询和结果映射做了相当多的改进,为查询提供了很大的便利
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
属性 | 描述 |
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 | 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔 |
二、Select使用示例
数据准备,下面示例将以下面三张表作为基础数据举例:
Blog博客实体类:
@Data
public class Blog implements Serializable{
/**
* 文章ID
*/
private Integer bid;
/**
* 文章标题
*/
private String name;
/**
* 文章作者ID
*/
private Integer authorId;
}
BlogAndComment博客评论实体类:
@Data
public class BlogAndComment implements Serializable {
/**
* 文章ID
*/
private Integer bid;
/**
* 文章标题
*/
private String name;
/**
* 文章作者ID
*/
private Integer authorId;
/**
* 文章评论
*/
private List<Comment> comment;
}
AuthorAndBlog作者文章实体类:
@Data
public class AuthorAndBlog implements Serializable {
/**
* 作者ID
*/
private Integer authorId;
/**
* 作者名称
*/
private String authorName;
/**
* 文章和评论列表
*/
private List<BlogAndComment> blog;
}
一、单表查询
查询bid=1的Blog信息
mapper映射文件:
<resultMap id="BaseResultMap" type="blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!--根据id查询-->
<select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" useCache="false">
select * from blog where bid = #{bid}
</select>
测试类:
@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映射文件:
<!-- 查询文章带评论的结果(一对多) -->
<resultMap id="BlogWithCommentMap" type="com.zdp.entity.author.BlogAndComment" extends="BaseResultMap" >
<collection property="comment" ofType="com.zdp.entity.Comment">
<id column="comment_id" property="commentId" />
<result column="content" property="content" />
</collection>
</resultMap>
<!-- 根据文章查询评论,一对多 -->
<select id="selectBlogWithCommentById" resultMap="BlogWithCommentMap" >
select
b.bid,
b.name,
b.author_id authorId,
c.comment_id commentId,
c.content
from
blog b, comment c
where b.bid = c.bid and b.bid = #{bid}
</select>
测试类:
@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映射文件:
<!-- 按作者查询文章评论的结果(多对多) -->
<resultMap id="AuthorWithBlogMap" type="com.zdp.entity.AuthorAndBlog" >
<id column="author_id" property="authorId" jdbcType="INTEGER"/>
<result column="author_name" property="authorName" jdbcType="VARCHAR"/>
<collection property="blog" ofType="com.zdp.entity.author.BlogAndComment">
<id column="bid" property="bid" />
<result column="name" property="name" />
<result column="author_id" property="authorId" />
<collection property="comment" ofType="com.zdp.entity.Comment">
<id column="comment_id" property="commentId" />
<result column="content" property="content" />
</collection>
</collection>
</resultMap>
<!-- 根据作者文章评论,多对多 -->
<select id="selectAuthorWithBlog" resultMap="AuthorWithBlogMap" >
select
b.bid,
b.name,
a.author_id authorId,
a.author_name authorName,
c.comment_id commentId,
c.content
from
blog b,
author a,
comment c
where
b.author_id = a.author_id
and b.bid = c.bid
</select>
测试类:
@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<AuthorAndBlog> authors = mapper.selectAuthorWithBlog();
System.out.println(JSONObject.toJSONString(authors));
} finally {
session.close();
}
}
控制台输出:
四、MyBatis对Select元素的解析及使用
从上面了解到了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 方式的解析逻辑
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
到这里我们可以看到出现了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(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, 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会抛出绑定异常,
<!--错误示例 重复注册-->
<mappers>
<mapper resource="com/zdp/mapper/BlogMapper.xml"/>
<package name="com.zdp.mapper"/>
</mappers>
public <T> void addMapper(Class<T> 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> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
//.....
}
public class Configuration {
//.....
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//.....
}
传入Class接口的Class对象,获取到MapperProxyFactory工厂对象,调用了它的newInstance()方法
public class MapperRegistry {
//....
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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<T> {
protected T newInstance(MapperProxy<T> mapperProxy) {
/**
* @Description: 使用JDK动态代理创建Mapper代理对象
* @param ClassLoader
* @param interfaces
* @param InvocationHandler 实现了InvocationHandler接口的触发管理类
* @return Mapper代理对象
* @Author zdp
* @Date 2022-01-04 11:18
*/
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
//创建MapperInterface对应的MapperProxy 对象,比如mapperInterface是 BlogMapper.class,就创建MapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
这里断点验证看到newInstance()方法执行完确实是返回的是一个Mapper代理对象
这里通过JDK的动态代理,返回了一个代理对象,也就明白了为什么传入一个Mapper后又返回了一个代理的Mapper对象,接下来该Mapper的所有方法执行,都会执行MapperProxy中invoke方法的逻辑
public class MapperProxy<T> implements 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元素的简单解析了,如有错误欢迎指出,希望对你有点帮助!