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)

parameterMap

用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性

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使用示例

数据准备,下面示例将以下面三张表作为基础数据举例:

mybatis EntityWrapper查询如何强制使用索引_默认值


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();
        }
    }

控制台输出:

mybatis EntityWrapper查询如何强制使用索引_xml_02

二、一对多查询

查询文章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();
        }
    }

控制台输出:

mybatis EntityWrapper查询如何强制使用索引_mybatis_03

三、多对多查询

查询作者的文章评论信息

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 EntityWrapper查询如何强制使用索引_java_04

四、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代理对象

mybatis EntityWrapper查询如何强制使用索引_java_05

这里通过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元素的简单解析了,如有错误欢迎指出,希望对你有点帮助!