1 引言

MyBatis 是一个优秀的持久层框架,它简化了 JDBC 编程,使得数据库操作更加简单和灵活。MyBatis 支持定制化 SQL、存储过程以及高级映射,避免了几乎所有的 JDBC 代码和手动设置参数及获取结果集的过程。

配置文件如下


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 映射器(Mapper)文件的位置 -->
    <mappers>
        <mapper resource="com/example/mapper/UserMapper.xml"/>
    </mappers>

    <!-- 数据源配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 全局配置,如日志、事务等 -->
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>
</configuration>
```

代码如下

String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
Reader reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory   解析xml文件  1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

// 数据源 执行器  DefaultSqlSession 2
SqlSession session = sqlSessionFactory.openSession();
try {
    // 创建动态代理
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    session.commit();
} catch (Exception e) {
    e.printStackTrace();
    session.rollback();
} finally {
    session.close();
}

2 源码解析

入口方法:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);


public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    //解析配置的mybatis的配置文件
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      reader.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

public Configuration parse() {
  //1 若已经解析过了 就抛出异常
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  //2 设置解析标志位
  parsed = true;
  //3 解析我们的mybatis-config.xml的configuration节点
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}


private void parseConfiguration(XNode root) {
  try {
    //1 解析 properties节点
    propertiesElement(root.evalNode("properties"));
    //2 解析我们的mybatis-config.xml中的settings节点
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    //3 解析我们的别名
    typeAliasesElement(root.evalNode("typeAliases"));
    //4 解析我们的插件(比如分页插件)
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));

    //5 设置settings 和默认值到configuration
    settingsElement(settings);
    //6 解析数据源的信息 详情看3
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    //7 解析我们的类型处理器节点
    typeHandlerElement(root.evalNode("typeHandlers"));
    //8 解析mapper节点 扫描配置的mapper接口和绑定的xml文件 详情看4
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

3 数据源的解析

/**
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
*/

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
       //1 获取默认的额数据源
      environment = context.getStringAttribute("default");
    }
    //2 遍历解析配置的所有数据源
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
          //3 获取事务类型
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //4 解析数据源信息
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

4 mapper接口及xml的解析

4.1 分类

在 MyBatis 的配置文件中,mappers 元素用于指定映射文件(Mapper XML 文件)的位置。MyBatis 支持多种格式来配置映射文件的位置。以下是四种常见的配置方式:

1. 使用类路径资源

你可以直接指定映射文件的类路径资源路径。这种方式适用于映射文件位于类路径中的情况。

<mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>

2. 使用类名

你可以通过指定映射接口的全限定类名来加载映射文件。这种方式适用于映射文件与映射接口同名且位于同一包中。

<mappers>
    <mapper class="org.mybatis.example.BlogMapper"/>
</mappers>

3. 使用文件路径

你可以指定映射文件的绝对路径或相对于类路径的路径。这种方式适用于映射文件不在类路径中的情况。

<mappers>
    <mapper url="file:///var/mappers/BlogMapper.xml"/>
</mappers>

4. 使用包扫描

你可以指定一个包名,MyBatis 会自动扫描该包及其子包中的所有映射接口,并加载对应的映射文件。这种方式适用于有多个映射接口的情况。

<mappers>
    <package name="org.mybatis.example"/>
</mappers>

<mappers> <package name="org.mybatis.example"/> </mappers>

4.2 解析入口

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    //1 获取我们mappers节点下的一个一个的mapper节点 对应上面4种情况
    for (XNode child : parent.getChildren()) {
      //2 判断我们mapper是不是通过批量注册的 <package name="org.mybatis.example"/>
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        //3 判断从classpath下读取我们的mapper
        String resource = child.getStringAttribute("resource");
        //4 判断是不是从我们的网络资源读取(或者本地磁盘得)
        String url = child.getStringAttribute("url");
        //5 解析这种类型(要求接口和xml在同一个包下)
        String mapperClass = child.getStringAttribute("class");
        //6 <mapper resource="org/mybatis/example/BlogMapper.xml"/>
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
          //7 <mapper url="file:///var/mappers/BlogMapper.xml"/>
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
          //8 <mapper class="org.mybatis.example.BlogMapper"/>
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
4.3 package解析

我们常用的是通过package来配置扫描路径,以下源码的解析过程为package解析


public void addMappers(String packageName, Class<?> superType) {
  //1 根据包找到所有类
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  //2 循环所有的类
  for (Class<?> mapperClass : mapperSet) {
    addMapper(mapperClass);
  }
}

public <T> void addMapper(Class<T> type) {
  //1 判断我们传入进来的type类型是不是接口
  if (type.isInterface()) {
    //2 判断我们的缓存中有没有该类型
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      //3 创建一个MapperProxyFactory 把我们的Mapper接口保存到工厂类中, 该工厂用于创建 MapperProxy
      //后续的getmapper从这里取出
      knownMappers.put(type, new MapperProxyFactory<>(type));
      //4 mapper注解构造器
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      //5 进行解析, 将接口完整限定名作为xml文件地址去解析
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}


public void parse() {
  String resource = type.toString();
  //1 是否已经解析mapper接口对应的xml
  if (!configuration.isResourceLoaded(resource)) {
    //2 根据mapper接口名获取 xml文件并解析,  解析<mapper></mapper>里面所有东西放到configuration
    loadXmlResource();
    //3 添加已解析的标记
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    //4 获取所有方法 看是不是用了注解
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        if (!method.isBridge()) {
          //5 是不是用了注解  用了注解会将注解解析成MappedStatement
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}


private void loadXmlResource() {
  //1 获取xml的sql文件
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    //2 把mapper接口的全类名替换 查询对应的sql文件
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    //3 解析sql文件
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
      // Search XML mapper that is not in the module but in the classpath.
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e2) {
        // ignore, resource is not required
      }
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}


public void parse() {
   //1 判断当前的Mapper是否被加载过
  if (!configuration.isResourceLoaded(resource)) {
    //2 真正的解析我们的 <mapper namespace="com.mybatis.mapper.UserMapper">
    configurationElement(parser.evalNode("/mapper"));
     //3 把资源保存到我们Configuration中
    configuration.addLoadedResource(resource);
    //4 绑定mapper和namespace的关系
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}


private void configurationElement(XNode context) {
  try {
    //1 解析我们的namespace属性
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    //2 保存我们当前的namespace 
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    //3 解析我们的resultMap节点
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //4 解析我们通过sql片段
    sqlElement(context.evalNodes("/mapper/sql"));
    //5 解析我们的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);
  }
}


private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  //1 循环解析我们的select|delte|insert|update节点
  for (XNode context : list) {
    //2 创建一个xmlStatement的构建器对象
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}


public void parseStatementNode() {
  //1 解析方法名称
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  //2 获得节点名称:select|insert|update|delete
  String nodeName = context.getNode().getNodeName();
  //3 根据nodeName 获得 SqlCommandType枚举
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  //4 解析我们的sql公用片段
  // Include Fragments before parsing
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());
  //5 解析我们sql节点的参数类型
  String parameterType = context.getStringAttribute("parameterType");
  //6 把参数类型字符串转化为class
  Class<?> parameterTypeClass = resolveClass(parameterType);
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }
  // 解析sql主体
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String resultType = context.getStringAttribute("resultType");
  //7 解析我们查询结果集返回的类型
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");

  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }

  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");


  //8 为我们的insert|delete|update|select节点构建成我们的mappedStatment对象
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
4.4 动态、静态sql的解析

mybatis中的sql主体的解析分为两类,一类是动态sql解析,一类是静态sql解析

  1. 静态 SQL

定义:静态 SQL 是指在编写映射文件时已经确定的 SQL 语句,这些语句在编译时就已经固定,不会根据运行时的条件变化。

特点:

  • 固定不变:SQL 语句在编译时就已经确定,不会根据运行时的条件发生变化。

  • 简单直接:通常用于简单的查询、插入、更新和删除操作。

  • 性能较高:由于 SQL 语句固定,MyBatis 可以在编译时对其进行优化。

<mapper namespace="org.mybatis.example.BlogMapper">
    <select id="selectBlog" resultType="Blog">
        SELECT * FROM blog WHERE id = #{id}
    </select>
</mapper>

在这个例子中,SQL 语句 SELECT * FROM blog WHERE id = #{id} 是固定的,不会根据运行时的条件变化。

静态sql的解析非常简单,只需要把入参替换为?即可

  1. 动态 SQL

定义:动态 SQL 是指在运行时根据条件动态生成的 SQL 语句。MyBatis 提供了多种标签(如 ifchoosewhenotherwisewheresetforeach 等)来实现动态 SQL或者使用${}等占位符。

特点:

  • 条件生成:SQL 语句根据运行时的条件动态生成,可以包含或排除某些部分。

  • 灵活性高:适用于复杂的查询条件和多变的业务逻辑。

  • 性能较低:由于 SQL 语句在运行时生成,可能会导致性能开销增加。

示例:

<mapper namespace="org.mybatis.example.BlogMapper">
    <select id="selectBlogsDynamic" resultType="Blog">
        SELECT * FROM blog
        <where>
            <if test="title != null">
                AND title = #{title}
            </if>
            <if test="author != null">
                AND author = #{author}
            </if>
        </where>
    </select>
</mapper>

在这个例子中,SQL 语句 SELECT * FROM blog 是固定的,但 WHERE 子句中的条件是根据传入的参数动态生成的。如果 title 和 author 都不为 null,生成的 SQL 语句可能是:

SELECT * FROM blog WHERE title = 'My Blog' AND author = 'John Doe'

如果只有 title 不为 null,生成的 SQL 语句可能是:

SELECT * FROM blog WHERE title = 'My Blog'

动态sql的解析会复杂很多,根据不同的动态标签(实现同一个接口),会封装成不同的实现对象。以装饰器模式层层包装,解析的时候层层解析。

区分方法

  1. 查看 SQL 语句是否固定:

    1. 静态 SQL:SQL 语句在编写时已经确定,不会根据运行时的条件变化。

    2. 动态 SQL:SQL 语句在运行时根据条件动态生成。

  2. 检查是否使用动态标签:

    1. 静态 SQL:不使用任何动态标签(如 ifchoosewhenotherwisewheresetforeach 等)。

    2. 动态 SQL:使用一个或多个动态标签来生成 SQL 语句。

  3. 查看参数是否影响 SQL 语句:

    1. 静态 SQL:参数只用于替换占位符(如 #{id}),不会影响 SQL 语句的结构。

    2. 动态 SQL:参数不仅用于替换占位符,还会影响 SQL 语句的结构(如添加或删除条件)。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐