全局配置 mybatis-config.xml

porperties属性

可以配置一些运行参数,可以放一些porperties或者xml文件,比如:

<properties>
	<property resource = "db.properties"></property>
</properties>
settings设置

能够配置的内容比较多,能够影响Mybatis底层的运行,一般只修改一些常用的规则,比如自动映射、驼峰命名规则、级联规则、缓存、Executor类型等,比如:

<settings>
    
 	<!--打印查询语句 -->      
	<setting name = "logImpl" value = "STDOUT_LOGGING"/>
    
 	<!--延迟加载开关,解决N+1问题,默认值为false-->   
 	<setting name = "lazyLoadingEnabled" value = "true"/>
    
    <!--层级加载开关,默认值3.4.1之后为false-->
 	<setting name = "aggresiveLazyLoading" value = "true" />
    
    <!--一级缓存开关,默认值session,也就是开启,修改值为STATEMENT则会关闭一级缓存-->
    <setting name = "localCacheScope" value = "SESSION"/>
    
    <!--二级缓存开关,默认开启,另外也可以在映射器中使用<cache/>标签进行开启,在特定的Sql标签修改useCache为false可以关闭该statement的二级缓存-->
    <setting name = "cacheEnabled" value = "true"/>
    
</settings>
typeAliases类型别名

定义类的别名,简化使用,通常使用扫描的方式,如果别名重复使用注解@Aliases进行区分,比如:

<typeAliases>
	<typeAlias alias = "use" type = "com.xxx.User"></typeAlias>
    <package name = "com.xxx"/>
</typeAliases>
typeHandlers类型转换器

将Java类型和Jdbc类型进行转换,有系统定义和自定义,自定义的需要实现BaseTypeHandler接口并且在这里进行注册,或者在映射器中指定,比如:

<!-- 配置文件中指定 -->
<typeHandlers>
	<typeHandler handler = "com.xxx.typehandler.SexTypeHandler" javaType = "com.xxx.pojo.SexEnum" jdbcType = "INTEGER"/>
    <package name = "com.xxx.typehandler"/>
</typeHandlers>

<!-- 映射器中指定-->
<resultMap id = "userResultMap" type = "user">
	<result property = "sex" column = "sex" typeHandler = "com.xxx.typehandler.SexTypeHandler"></result>
</resultMap>
objectFactory对象工厂

对象工厂是在创建结果集时通过反射来创建实例对象,可以通过实现ObjectFactory接口或者继承DefaultObjectFactory来自定义返回规则,通常不需要配置,

<ObjectFactory type = "com.xxx.objectfactory.MyObjectFactory">
	<property name = "" value = ""></property>
</ObjectFactory>
plugin插件

Mybatis中的插件拦截的四个对象Executor、ParameterHandler、ResultSetHandler、StatementHandler,自定义的插件需要实现Interceptor接口,并且在配置中注册。关于插件的运行原理和分页插件的使用在后续文章中在写。

enviroments运行环境

主要是配置数据库信息,可以配置多个数据库,一般而言只需要配置一个就行了。包括了事务管理器和数据源

事务管理器:Mybatis提供了两个工厂类,:JdbcTransactionFactory和ManagedTransactionFactory,JDBC以jdbc的方式对数据库的提交和回滚进行操作,而MANAGED把事务交给容器处理,提交和回滚方法不用任何操作,默认情况下会关闭连接。

数据源:提供了PooledDataSourceFactory、UnpooledDataSourceFactory、JndiDataSourceFactory三个工厂类,POOLED使用连接池的思想对Connection管理;UNPOOLED采用非数据库连接池的管理方式,每次请求都会打开一个新的数据库连接;JNDI是为了能在如EJB或者应用服务器这类容器中使用。

在与Spring集成之后数据库连接和事务都托管给了Spring。

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"></transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value=""/>
            <property name="url" value=""/>
            <property name="userName" value=""/>
            <property name="password" value=""/>
        </dataSource>
    </environment>
</environments>
  1. databaseIdProvider数据库厂商标识
    查询时可以支持多数据库,当SQL语句中的databaseID被配置了的时候,系统会优先取和数据库配置一致的SQL,如果没有,则取没有databaseID的SQL执行,可以把它当做默认值,value为数据库别名,可以通过这个别名标注那一条SQL语句适用于哪种数据库运行,比如:
<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="Oracle" value="oracle"/>
    <property name="DB2" value="db2"/>
</databaseIdProvider>

<select id = "getUserById" parameterType = "int" resultType = "user" databaseId = "mysql">
	select id uname as name note from t_user where 1 = 1 and id = #{id}
</select>
<select id = "getUserById" parameterType = "int" resultType = "user" databaseId = "oracle">
	select id uname as name note from t_user where id = #{id}
</select>
<!-- 这样当数据库切换时,就会自动选择对应的sql进行查询-->
  1. mapper映射器
    映射器namespace = 接口的权限名,SELECT|UPDATE|INSERT|DELETE标签的id则对应接口的方法名,映射器的引入通常有下面几种方法:
<mappesr>
    
	<!--使用类名注册-->
    <mapper class = "com.xxx.mapper.UserMapper"/>
   
	<!--使用包名注册,常用-->
    <package name = "com.xxx.mapper"/>
    
	<!--使用文件路径引入-->
    <mapper resource = "com/xxx/mapper/UserMapper.xml"/>

</mappers>

和spring集成之后映射器的扫描托管到spring的配置文件中,在springboot中则使用注解@MapperScan(“packagename”)进行扫描。

Mapper映射器

缓存cache

跟Hibernate一样,Mybatis也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

Mybatis跟缓存相关的类都在cache包中,定义了一个Cache接口,并且只有一个默认的实现类PerpetualCache底层使用HashMap实现,这个对象一定会创建,也叫基础缓存,除此之外,Mybatis还定义了很多缓存的装饰器,通过这些装饰器,可以对基础缓存扩展很多额外的功能,比如回收策略、日志记录、定时刷新等。总体上分三类:基本缓存,淘汰算法缓存、装饰器缓存。下面对这些缓存做个说明:

缓存实现类

描述

作用

装饰条件

基本缓存

缓存基本实现类

默认是PerpetualCache,也可以自定义比如RedisCache/EhCache等具备基本功能的缓存类


LruCache

LRU策略的缓存

删除最近最少使用的缓存

eviction=“LRU”(默认)

FifoCache

FIFO策略的缓存

删除最先入队的缓存

eviction=“FIFO”

SoftCache WeakCaceh

带清理策略的缓存

通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理这些缓存,基于SoftReference和WeakReference

eviction=“SOFT|WEAK”

LoggingCache

带日志功能的缓存

比如:输出缓存命中率

基本

SynchronizedCache

同步缓存

基于Synchronized关键字实现,解决并发问题

基本

BlockingCache

阻塞缓存

通过在get/put方式中加锁,保证只有一个线程操作缓存,基于JAVA重入锁实现

bloking=“true”

SerializedCache

支持序列化的缓存

将对象序列化以后存入缓存,取出时反序列化

readOnly=false(默认)

ScheduledCache

定时调度的缓存

在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是1小时),如果是则清空缓存,也就是每隔一段时间清空一次缓存

flushInterval不为空

TransactionalCache

事务缓存

在二级缓存中使用,可一次存入多个缓存,移除多个缓存

在TransactionalCacheManager中用Map维护对应关系

  • 一级缓存
    一级缓存也叫本地缓存Local Cache,是SqlSession层面的缓存,默认开启。既然是在SqlSession中进行缓存,DefaultSqlSession中只有两个对象属性:Configuration和Executor,而前者是全局性的,所以一级缓存是在Executor中维护的,三种执行器的父类BaseExecutor中的构造函数中就初始化了PerPetual对象。
  • 在同一个Session中共享,不同Session不能共享。
    一级缓存的存入:是在BaseExecutor的query()方法中的queryFromDataBase()方法中存入的。

    一级缓存的清空:同一个会话中,更新或者删除操作会导致缓存被清空,如果select标签的flushCache=true时,则查询也会清空缓存。在BaseEexcutor中的update()方法中调用了clearLocalCache()方法来清空。
  • 一级缓存存在的问题:如果其他会话更新数据,会导致本会话从缓存中读取到过时的数据。要解决这个问题只能用范围更大的二级缓存。
  • 二级缓存
    二级缓存是用来解决一级缓存不能跨会话共享的问题的,是namespace级别的,只要是同一个接口里面相同的方法,都可以共享,声明周期和应用同步。
    如果开启了二级缓存,那么使用到缓存的时候,肯定是先查询二级缓存,二级缓存没有的时候才去一级缓存中查,一级缓存没有的时候就到数据库去查。
    一级缓存是放在Executor中的,二级缓存的范围比它大,Mybatis中对Executor做了个装饰,使用CashingExecutor来维护,也就是如果使用二级缓存,则在创建Executor对象时会对Executor进行装饰,查询时判断CashingExecutor中是否有缓存结果,有就从该缓存中返回,没有就委派给真正的执行器执行查询,比如说SimpleExecutor,再到该执行器中的一级缓存中查。

    注意:全局配置文件中二级缓存的总开关默认开启,如果一个Mapper要使用二级缓存,还要单独打开它自己的开关。
<!-- mapper中声明该namespace使用二级缓存-->
<cache type = "org.apache.ibatis.cache.impl.PerpetualCache" size = "1000" eviction = "LRU" flushInterval = "12000" readOnly = "false"/>
<!-- size表示最多缓存的对象个数,eviction表示回收策略,flushInterval表示自动刷新时间-->
select

首先来看一个简单的使用:

  1. select emp_id as id name mobile note from t_emp where id = #{id} ```
  • 简单说一下各个属性的含义:namespace为接口的全限定名,id为接口中对应的方法,parameterType为传入参数的类型,resultType为结果映射的类型…
  • 多个参数的传递方式:使用map,使用注解,使用javabean。
    使用map:代码可读性降低,用的比较少
List<Employee> selectByMap(Map<String,Object> map);
  • select emp_id as id name mobile note from t_emp where name like concat('%',#{name},"%") and mobile like concat('%',#{lastNumber},'%') ```
使用注解:一般参数少于5个时,简单使用,可读性高,参数过多调用起来不方便,显得繁琐复杂

 ```java
List selectByAnnotation(@Param(“name”)String name,@Param(“lastNumber”)int lastNumber);
 ``````xml
 <select id = "selectByMap" resultType = "emploee">
 	select emp_id as id name mobile note from t_emp 
     where name like concat('%',#{name},"%") 
     and mobile like concat('%',#{lastNumber},'%')
 </select>
 ```

 使用javabean:参数多于5个时使用比较方便

 ```java
 public class EmploeeParam{
     private String name;
     private int lastNumber;
     ...}
 ``````xml
  1. select emp_id as id name mobile note from t_emp where name like concat('%',#{name},"%") and mobile like concat('%',#{lastNumber},'%') ```
  • 分页参数RowBounds

RowBounds是Mybatis内置的分页类,有两个参数,offset表示从哪一行开始,limit表示限制条数,在接口方法上加上这个参数直接使用,比如:

List<Employee> selectByAnnotation(@Param("name")String name,@Param("lastNumber")int lastNumber,RowBounds rb);

   @Autowired
   private EmployeeService service;
   @RequestMapping("/emps_like")
   @ResponseBody
   public ModelAndView getEmps(@RequestParam("name")String name,@RequstParam("lastNumber")int lastNumber) {
       ModelAndView mv = new ModelAndView();
       RowBounds rb = new RowBounds(0,10);
       List<Employee> list = service.selectByAnnotation(name,lastNumber,rb);
       mv.put("emps",list)
       return mv;
   }
   ```

3. #### insert

 - 简单的使用

```xml
   <mapper namespace = "com.xxx.dao.EmployeeMapper">
	<insert id = "insertEmp" parameterType = "employee">
   		insert into t_emp(emp_id,name, sex,mobile, note) 
           values(#{id},#{name},#{sex},#{mobile},#{note})
   	</insert>
</mapper>
   ```

 - 主键回填

   无论支持还是不支持主键自增的数据库都可以使用selectKey来实现主键回填,比如:

   ```xml
<!--mysql 注意order要设置为after,获取递增主键值用的是函数LAST_INSERT_ID()-->
   <insert id="insertUser" parameterType="user" >
    	<selectKey keyProperty="id" order="after " resultType="int" >
           		select LAST_INSERT_ID()
       	</selectKey>
       	insert into t_user(t_name,sex) values (#{name},#{sex})
   </insert>
   
   <!--oracle 注意order要设置为before,因为要先从序列中获取值,然后将值作为主键插入到数据库中-->
   <insert id="insertUser" parameterType="user" >
    	<selectKey keyProperty="id" order="before " resultType="int" >
           		select t_id.nextval as id from dual
    	</selectKey>
       	insert into t_user(t_id,t_name,sex) values (#{t_id},#{name},#{sex})
</insert>
   ```

   对于支持自增主键的数据库MySQL,也可以使用下面这种方式实现主键回填,设置useGeneratedKeys属性为true,keyProperty设置生成主键映射到那个属性上,keyColum是映射到那个字段上。

   ```java
   void insertUser(User user);
   ```

   ```xml
   <insert id="insertUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
        insert into t_user(t_name,sex) values (#{name},#{sex})
   </insert>
   ```

   对于不支持主键自增的数据库Oracle,就通过通用方式selectKey属性和标签来完成。

   这里对selectKey标签中的属性简单说明一下:

   | 属性          | 描述                                                         |
   | ------------- | ------------------------------------------------------------ |
   | keyProperty   | selectKey语句结果应该被设置的目标属性                        |
   | resultType    | 结果的类型                                                   |
   | keyColumn     | 匹配属性的返回结果集中的列名称                               |
   | order         | 如果为before,那么它会先执行selectKey语句设置keyProperty然后执行插入语句,如果为after则相反 |
   | statementType | 使用哪种语句类型,默认为PREPARED。                           |

4. #### update|delete

 这两个比较简单,对于批量更新和删除见后面的动态sql一节。

5. #### resultMap

 resultMap主要用途有两个:第一个就是当数据库字段名和Java对象属性名不一致是使用,第二个就是用于高级查询,也就是一对多,多对多。代码示例:

 ```xml
     <resultMap id="employee" type="employee">
         <id column="id" property="id"/> <!-- 主键列-->
         <result column="real_name" property="realName"/>
      <result column="sex" property="sex"/>
         <result column="birthday" property="birthday"/>
      <result column="mobile" property="mobile"/>
         <result column="email" property="email"/>
      <result column="POSITION" property="position"/>
         <result column="note" property="note"/>
         <!-- association完成一对一的映射-->
         <association property="workCard"  column="id"  select="com.dao.WorkCardDao.getWorkCardByEmpId"/>
         <!-- collection完成一对多的映射-->
         <collection property="employeeTaskList" column="id" fetchType="lazy" ofType="employeeTask" select="com.dao.EmployeeTaskDao.getEmployeeTaskByEmpId"/>
         <!--鉴别器-->
         <discriminator javaType="long" column="sex">
             <case value="1" resultMap="maleHealthFormMapper"/>
             <case value="0" resultMap="femaleHealthFormMapper"/>
         </discriminator>
     </resultMap>
 
  <resultMap id="maleHealthFormMapper" type="maleEmployee" extends="employee">
         <association property="maleHealthForm" column="id" select="com.dao.MaleHealthFormDao.getMaleHealthForm"/>
  </resultMap>
 
     <resultMap id="femaleHealthFormMapper" type="femaleEmployee" extends="employee">
         <association property="femaleHealthForm" column="id" select="com.dao.FemaleHealthFormDao.getFemaleHealthForm"/>
     </resultMap>
 
     <select id="getEmployee" parameterType="long" resultMap="employee">
         select id,real_name as realName,sex,birthday,moblie,email,POSITION,note from t_emploee
         where id = #{id}
     </select>
sql片段

通常是把一些反复使用的SQL片段比如列名,定义在sql标签中,在其他标签中就可以通过引入的方式使用,比如:

<sql id = "empCols">
    id,real_name,sex,birthday,moblie,email,POSITION,note 
   </sql>

   <select id="getEmployee" parameterType="long" resultMap="employee">
           select 
       	<include refid = "empCols"/> <!--引入-->
       	from t_emploee
           where id = #{id}
   </select>

另外sql标签还支持变量传递,比如:

<sql id = "empCols">									${alias}.id,${alias}.real_name,${alias}.sex,${alias}.birthday,${alias}.moblie,${alias}.email,${alias}.POSITION,${alias}.note 
</sql>
<!--定义的alias变量的值就是表的别名-->
<select id="getEmployee" parameterType="long" resultMap="employee">
     select 
    	<include refid = "empCols">
            <property name = "alias" value = "e"></property>
        </include>
    	from t_emploee e
        where id = #{id}
</select>
级联查询

Mybatis中的级联分三种:鉴别器、一对一和一对多,分别对应着resultMap中的:discriminator、association和collection属性,

  • 嵌套查询:会多执行一些SQL语句导致数据库资源的损耗和系统性能的下降,也就是N+1问题,解决方案就是打开延迟加载开关:
<resultMap id="employee" resultType = "employee">
   <association property="workCard"  			      column="id"select="com.xxx.dao.WorkCardDao.getWorkCardByEmpId"/>
   <collection property="employeeTaskList" column="id" fetchType="lazy" 			select="com.xxx.dao.EmployeeTaskDao.getEmployeeTaskByEmpId"/>
</resultMap>       
<!--就是在查询employee表的时候通过select属性值(statementId)定位到另外的一个查询上,进行了第二次查询,看上去只有一个查询雇员信息的操作,其实后台分别执行了对雇员表、工号表、雇员任务表的查询 -->

<!--解决方案:
	1.全局性配置:lazyLoadingEnabled是否开启延迟加载,aggressiveLazyLoading是否采用层级加载。
	2.语句配置:fetchType属性可以处理全局定义无法处理的问题,进行自定义。注意该选项只在级联元素association和collection中存在,fetchType=eager 获得当前POJO后立即加载对应的数据,fetchType=lazy 获得当前POJO后延迟加载对应的数据
 注意:如果配置了fetchType属性,那么它会忽略上述的两个全局配置项。
-->
  • 嵌套结果查询:基于表连接实现级联,会导致SQL变复杂,所需要的配置也比较复杂,一次性取出所有数据会造成内存浪费,给日后的维护工作带来不便,一般用于比较简单,关联不多的场景下。
<resultMap id="employee2" type="employee">
        <id column="id" property="id"/>
        <result column="real_name" property="realName"/>
        <result column="sex" property="sex"/>
        <result column="brithday" property="birthday"/>
     <result column="mobile" property="mobile"/>
        <result column="email"  property="email"/>
     <result column="position" property="position"/>
        <association property="workCard"  javaType="workCard" column="id">
         <id column="wc_id" property="id"/>
            <result column="id" property="empId"/>
         <result column="wc_real_name" property="realName"/>
            <result column="wc_department" property="department"/>
         <result column="wc_mobile" property="mobile"/>
            <result column="wc_position" property="position"/>
         <result column="wc_note" property="note"/>
        </association>
        <collection property="employeeTaskList" ofType="employeeTask" column="id" >
            <id column="et_id" property="id"/>
            <result column="id" property="empId"/>
            <result column="et_task_name" property="taskName"/>
            <result column="et_note" property="note"/>
            <association property="task" javaType="task" column="et_task_id">
                <id column="t_id" property="id"/>
                <result column="t_title" property="task.title"/>
                <result column="t_context" property="task.context"/>
                <result column="t_note" property="note"/>
            </association>
        </collection>
        <discriminator javaType="int" column="sex">
            <case value="1" resultMap="maleEmployeeMapper2"/>
            <case value="0" resultMap="femaleEmployeeMapper2"/>
        </discriminator>
</resultMap>

动态SQL

  1. 概念:动态Sql可以让我们在xml映射文件中以标签的形式编写动态sql,完成逻辑判断和动态拼接Sql的功能。
  2. 九中动态Sql标签:if|where|set|foreach|choose|when|otherwise|trim|bind
    具体使用方法后面文章在写
  3. 执行原理:使用OGNL从Sql参数对象中计算表达式的值,根据表达式的值动态拼接Sql。

```

动态SQL

  1. 概念:动态Sql可以让我们在xml映射文件中以标签的形式编写动态sql,完成逻辑判断和动态拼接Sql的功能。
  2. 九中动态Sql标签:if|where|set|foreach|choose|when|otherwise|trim|bind
    具体使用方法后面文章在写
  3. 执行原理:使用OGNL从Sql参数对象中计算表达式的值,根据表达式的值动态拼接Sql。