Mybatis原理深度解析

1.What is Mybatis?

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。上面是官网的解释,简单的说就是如何把Java中的对象映射到数据库中,把数据库中的记录映射成为Java对象的一个过程,并且封装好jdbc相关的功能,从mybatis的运作机理中可以发现其实它只是一个半ORM框架,因为在实现增删改查操作的时候需要我们自己手动编写数据库语句。

2.Mybatis应用(整合SpringBoot)

SpringBoot作为现如今主流的开发框架,为你提供非常便捷简单的项目搭建流程,再加上丰富工具集成而且out-of-the-box(开箱即用),不需要繁琐的配置,大大的提升了开发效率,SpringBoot本文不多做叙述,有兴趣的可以自行了解,下面开始使用SpringBoot+Mybatis做一简单的demo,实现快速使用mybatis来构建你的Dao层。

2-1.创建SpringBoot空项目,引入相关依赖和项目结构

 

2-2.自定义数据源配置类,完成数据源的自定义配置

@Configuration
@MapperScan(basePackages = DataSourceConfig.PACKAGE, sqlSessionFactoryRef = "mybatisSqlSessionFactory")
public class DataSourceConfig {

    //项目中的mapper接口包路径
    static final String PACKAGE = "com.lzl.demo.mapper";
    //mapper接口对应的xml文件的所在路径
    private static final String MAPPER_LOCATION = "classpath:mapper/*.xml";

    @Value("${datasource.master.url}")
    private String url;

    @Value("${datasource.master.username}")
    private String user;

    @Value("${datasource.master.password}")
    private String password;

    @Value("${datasource.master.driverClassName}")
    private String driverClass;

    /**
     * 自定义数据源配置
     * @return
     */
    @Bean(name = "mybatisDataSource")
    public DataSource createDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClass);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        dataSource.setValidationQuery("select 1");
        dataSource.setTestOnBorrow(true);
        return dataSource;
    }

    /**
     * 事务管理
     * @param dataSource
     * @return
     */
    @Bean(name = "mybatisTransactionManager")
    public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mybatisDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * 初始化 mybatisSqlSessionFactory
     * @param mybatisDataSource
     * @return
     * @throws Exception
     */
    @Bean(name = "mybatisSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(mybatisDataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(DataSourceConfig.MAPPER_LOCATION));
        return sessionFactory.getObject();
    }
}

这里就已经完成了配置,因为这里我使用的是自定义配置,如果使用的是默认的配置会更简单。SpringBoot+Mybatis就配置好了,非常简单,这里的Mapper接口、**.xml和**PO文件都可以使用 mybatis-generator来生成,如果使用的是IDEA,会有一个插件better-mybatis-generator,安装之后直接可以连接数据库生成上述文件。

3.Mybatis初始化流程

看了上面简单的配置,估计会一脸懵,太简单导致你都看不懂为啥它就可以使用了,下面结合项目说下Mybatis是怎么初始化的。

3-1.启动SpringBoot

一般一个SpringBoot工程的启动类DemoApplication都是在package路径的根目录下,这样你的@SpringBootApplication注解就可以不用指定scanBasePackages了,因为@SpringBootApplication注解里的@ComponentScan默认扫描的就是DemoApplication所在的路径及其子路径下的所有class。
3-2.@MapperScan注解
当SpringBoot启动的时候,扫描到这个@MapperScan 类的时候,就开始了Mybatis的加载。首先看下@MapperScan的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
  .../* 省略 */...
      
  /** 
   * dao接口路径
   */
  String[] basePackages() default {};
    
  /**
   * SqlSessionFactory的bean名称
   */
  String sqlSessionFactoryRef() default "";

  /**
   * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
   *
   */
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
}
其中最主要的就是 @Import(MapperScannerRegistrar.class) 这句,导入MapperScannerRegistrar.class这个类并把它注册到容器中,看下这个类的关系图

 

实现了2个接口ImportBeanDefinitionRegistrar和ResourceLoaderAware,其中ImportBeanDefinitionRegistrar这个接口就是Spring留给开发者的扩展接口,通常可以通过实现这个接口来初始化配置项,在这里Mybatis就是使用这个方式来初始化的。当MapperScannerRegistrar这个bean被注册之后,Spring就会调用ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars)方法来对各个register进行BeanDefinitions的注册,然后就开始调用MapperScannerRegistrar这个实现类的方法进行各项属性的注册,源码如下

@Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    // this check is needed in Spring 3.1
    if (resourceLoader != null) {
      scanner.setResourceLoader(resourceLoader);
    }

    .../* 省略 */...

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
    }

    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

    .../* 省略 */...
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    scanner.doScan(StringUtils.toStringArray(basePackages));
  }


以上源码主要就是设置 basePackages 和 SqlSessionFactoryBeanName,并且扫描basePackages下的所有class注册为BeanDefinition,注册的是交给Spring处理的,注册完成之后Mybatis会进行一些操作来修改这个BeanDefinition的属性,如下:

 private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      //这里将这些bean的class全部设置为 mapperFactoryBean.class,
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }

      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }
 

这里对bean定义做了修改,主要就是将beanClass设置为MapperFactoryBean.class,并且将原className作为MapperFactoryBean的构造函数入参,这两步就为后面的初始化做好了准备。OK到此为止完成了所有mapper接口bean的注册。

3-3.Spring实例化bean

完成BeanDefinition的注册,IOC的过程就完成一半了,接下来根据BeanDefinition的信息来对bean进行实例化。这里由于这里将所有mapper接口bean的beanClass设置为MapperFactoryBean.class,所以Spring对于实现了FactoryBean接口的类实例化的时候会调用该类的getObject方法,源码如下:

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  private Class<T> mapperInterface;

  private boolean addToConfig = true;

  public MapperFactoryBean() {
    //intentionally empty 
  }
  
  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

 .../* 省略 */...

  /**
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }
  .../* 省略 */...
}


继续追踪代码,最后到MapperProxyFactory这个类里取初始化Mapper接口的实例。

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}


这里使用了jdk的动态代理生成了 Mapper接口的真正bean实例,这里的生成细节就不说了。到这里,整个Mapper接口实例化完成。

3-4.Mapper接口实例化时序图

 

 

3-5.Mybatis的核心配置的初始化

接下来就是比较明显的Mybatis的核心配置的初始化,在自定义的数据源配置类中,方法如下:

/**
     * 初始化 mybatisSqlSessionFactory
     * @param mybatisDataSource
     * @return
     * @throws Exception
     */
    @Bean(name = "mybatisSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        //设置数据源配置
        sessionFactory.setDataSource(mybatisDataSource);
        //设置Mybatis的mapper.xml的路径,由于初始化sql语句
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(DataSourceConfig.MAPPER_LOCATION));
        //factoryBean 通过getObject获取实例
        return sessionFactory.getObject();
    }

 

调用的时序大体如下:

 

这里其实大多数工作都是在解析xml的sql语句到内存,还有就是Configuration的初始化了,这个类是Mybatis里非常核心的一个类,也非常的重,里面包含了几乎所有Mybatis运行时需要的组件。经过上面的初始化之后,Mybatis的主要组件SqlSessionFactory就被Spring注册为Bean放到Spring的容器里,用来初始化前面的Mapper接口的代理类实例。

4.Mybatis的架构及各个组件作用

Mybatis分为三层架构,大体分为接口层,数据处理层和基础支撑层,每层负责的职责不一样,模块划分清晰

 

4-1.接口层

这里就是我们能够看到的框架最上层的地方,它定义了对于数据库的基本操作,比较简单,要注意Mapper接口和Mapper.xml对应(或者基于注解,更简单,0配置),实例如下

 

4-2.数据处理层

这一层是Mybatis的核心层,包括了我们上面分析的加载和初始化都属于这个层,它包含了配置解析、参数解析、SQL解析、SQL执行、结果处理、插件等模块,其功能如下:

  • 配置解析:在Mybatis初始化过程中,会加载mybatis-config.xml配置文件(我们使用的是注解)、映射配置文件以及Mapper接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configration对象中。之后,根据该对象创建SqlSessionFactory对象。待Mybatis初始化完成后,可以通过SqlSessionFactory创建SqlSession对象并开始数据库操作。这一过程上面已经分析过了。
  • 参数解析:参数映射主要是将java类型和JDBC类型对应起来,Mybatis已经有默认的映射了,初始化的时候就已经默认赋值了,具体可以看看TypeHandlerRegistry类。对应之后就会根据映射将入参由java类型转换为JDBC类型的参数。
  • SQL解析:Mybatis实现的动态SQL语句,几乎可以编写出所有满足需要的SQL,Mybatis中scripting模块会根据用户传入的参数,解析映射文件中定义的动态SQL节点,形成数据库能执行的sql语句。
  • SQL执行:Executor主要维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给StatementHandler完成,这里一般情况下默认使用SimpleExecutor来执行sql。
  • 结果处理:这里和上面参数解析的过程反过来,由于JDBC返回的结果集是JDBC类型的参数,所以也需要对JDBC类型的参数转换为java类型,再转换为对应的ResultType,返回给调用者。
  • 插件:这里没使用到插件,但是一般分页都会使用到插件,参数解析的时候对ParameterHandler进行了增强。

 

4-3.基础支撑层

这一层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。其中比较重要有几个模块:

  • 反射模块:对java反射进行了很好的封装,提供给上层使用,并且对反射操作进行了一系列的优化,比如,缓存了类的元数据(MetaClass)和对象的元数据(MetaObject),提高了反射操作的性能。
  • 缓存模块:Mybatis对数据查询进行优化,所以引入了缓存机制,分为一级缓存和二级缓存,两种缓存的区别就是作用域和机制,一级缓存默认开启,作用域是同一SqlSession会话,即当会话被销毁则一级缓存也就没了,但是当SqlSession执行update操作(update()、delete()、insert())的时候,一级缓存同样被删掉。SpringBoot中同样默认帮我们全局开启了二级缓存,但是是需要在相应的mapper接口中加@CacheNamespace注解才有用(或者xml里配置 <cache/>,但是无论如何返回的java对象一定要可以序列化),而且他的作用域是mapper的namespace,多个SqlSession可以使用改缓存,但任何一个SqlSession执行了该Mapper里的update操作(update()、delete()、insert())的时候都会刷新二级缓存。这里就简单说下缓存模块,Mybatis的二级缓存使用不多。
  • 解析器模块:该模块有两个主要功能:一个是封装了XPath,为Mybatis初始化时解析mybatis-config.xml配置文件以及映射配置文件提供支持;另一个为处理动态SQL语句中的占位符提供支持。
  • 数据源和连接池模块:在数据源模块中,Mybatis自身提供了相应的数据源实现,也提供了与第三方数据源集成的接口。
  • 类型转换模块:类型转换模块的另一个功能是实现JDBC类型与Java类型间的转换。
  • 事务管理模块:Mybatis的事务管理分为实现JdbcTransaction 和 ManagedTransaction两种,一个自己管理,一个交给外部容器管理,感兴趣的话可以看看具体的原理。

5.Mybatis如何完成一条SQL的执行

5-1.首先看执行的时序图

 

这是一个比较漫长的流程,涉及到的类主要就是有以下几个:

  • SqlSession接口:Mybatis默认使用的是DefaultSession实现,这里主要就是Mybatis的操作的会话接口,里面定义了一系列的数据库操作方法,例如增删改查。持有2个重要的属性,配置类Configuration和Executor,前者可以获取各种加载的配置如Mapper.xml里的内容;后者则是sql执行器。
  • Executor类:执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。关系图如下,Executor总体来说大同小异,只是以不同场景需要有了4个实现类。

 

  • Configuration类:Mybatis整个架构的大总管,内部持有几乎所有Mybatis的配置。
  • MappedStatement类:这个类主要是用来存储Mapper.xml的节点信息的,如select、update等,是在Mybatis初始化Configuration的时候解析xml或者注解存到这个对象中的,这个对象是被Configuration对象持有的。
  • SqlSource类:这个接口第一个作用是在Mybatis初始化的时候存放解析出来的原始SQL语句的,根据不同类型的SQL初始化为DynamicSqlSource或StaticSqlSource实例,原始SQL语句存放于M,负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。在这里动态SQL会被分为很多节点,大体上是根据<if>、<set>这些节点来分的sql,并且通过不同的SqlNode接口实现循环解析sql,直到全部解析为StaticTextSqlNode为止,然append到sql结尾,组成jdbc可识别的sql语句。
  • SqlNode接口:在动态SQL的解析中至关重要。
  • BoundSql类:存放解析出来的jdbc识别的SQL语句和其参数和值。
  • TypeHandler类:jdbc类型和java类型的映射表和其处理类。

 

 

  • StatementHandler类:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  • ParameterHandler类:负责对用户传递的参数转换成JDBC Statement 所需要的参数。
  • ResultSetHandler类:对jdbc返回的结果集进行类型转换,使其变为java类型的对象。

5-2.Mybatis执行流程分析

 

上面的较为详细的描述了整个sql执行的流程,其中比较重要的步骤就是第三步SQL的构成BoundSql对象,这里涉及到了对动态sql的处理,而且这块设计也比较巧妙,这里对于动态SQL的组装是用递归解析的,我们分析下复杂的SQL的解析,首先会调用MappedStatement.getBoundSql(Object parameterObject)方法,由于本次执行的SQL是动态SQL,所以它最终调用的是DynamicSqlSource类的getBoundSql方法,源码如下

@Override
public BoundSql getBoundSql(Object parameterObject) {
    // 初始化上下文 主要是sqlBuilder和bindings模块初始化
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 这里就是递归调用,根据node的不同类型来进行apply解析,node的种类上面的类图已经画出,这里使用的是MixdSqlNode
    rootSqlNode.apply(context);
    // 从配置类中拿出 typeAliasRegister和TypeHandlerRegister
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 解析完整的SQL语句,找到#{},替换为? 输出预编译sql
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
}

这里首先初始化一个上下文,主要作用是初始化2个属性 sqlBuilder和bindings,前者为存放完整SQL的,后者则是存放参数的。然后进入MixedSqlNode的apply方法,源码如下:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

可以看到这里循环调用不同类型的SqlNode的qpply方法,最终都是解析为StaticTextSqlNode,并将SQL语句拼接到context的SqlBuilder中。这里构建之后就得到的完整的sql了,但是还不是预编译的sql,后面会在SqlSourceParser类的parse()方法中实例化真正的解析器GenericTokenParser对sql语句进行解析,相关代码为:

qlSourceBuilder类
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

GenericTokenParser类  对#{}的部分进行替换为 ?
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

从代码可以清楚的看到sql中的#{}被找到 然后替换为"?"的,完成了这个替换之后,sql这块基本就准备完成了,后面就是对参数的处理了,这里就不多说了。OK到此为止,Mybatis的运行流程和设计基本上介绍完成。

5-3.Mybatis使用注意事项

了解了Mybatis的大体运行原理,在实战过程中难免会遇到一些问题,我这里列举下注意事项:

  • 慎用 ${},多用#{},其中原因是Mybatis在解析#{}语句的时候,会将它解析为预编译语言 如 "update table set a=? where b=?",后续处理在对"?"进行替换填充,这种语句是会被缓存的,以后同样的语句就可以不用再解析了,但是${}不会,而且是直接替换值,可能会发生SQL注入攻击。
  • 同一个NameSpace下,节点的id不能相同
  • XML转义字符,如果直接写就会报错,所以需要换一种xml能识别的写法:
    <!--原符号 < <= > >= & ' "
     替换符号 &lt; &lt;= &gt; &gt;= &amp; &apos; &quot;-->
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值