Mybatis

JDBC

JDBC相关概念

JAVA程序都是通过JDBC连接数据库的,通过SQL对数据库编程,JDBC是由SUN公司提出的一些列规范,只定义了接口规范,具体实现由各个数据库厂商去实现,它是一种典型的桥接模式。

桥接模式是一种结构型设计模式,它的主要特点是把抽象与行为实现分离开来,分别定义接口,可以保持各部分的独立性以及应对他们的功能扩展。

JDBC规范

所谓规范,就是自己定义了标准接口,如:用Connection代表和数据库的连接,用Statement执行SQL,用ResultSet表示SQL返回的结果,提供了对数据的便利。从Connection可以创建Statement,Statement执行查询得到ResultSet。

上面说的Connection、Statement、ResultSet都应该是接口,具体实现由各个数据库提供商提供。有了规范,可以通过统一的接口,访问多种类型的数据库,可随便切换数据库。

数据库驱动

接口的实现由各个厂商提供,那么实现类的类名就会不统一,去创建Connection对象时,代码就会写死某个实现类,切换数据库时,就需要修改代码,这样不太好。为了解决这个问题,抽象了Driver驱动的概念。

每个数据库都需要实现Driver接口,通过Driver可获得数据库连接Connection,通过反射机制动态创建。

同一个程序可能访问不同的数据库,通过DriverManager来管理驱动,Driver在初始化的时候,需要注册到DriverManager中。

DriverManager提供了一个getConnection方法,用于建立数据库Connection:

如果有多个数据库驱动,DriverManager如何区分呢,需要在数据库连接url中指定,比如mysql需要添加jdbc:mysql前缀:

数据源

数据源DataSource包含连接池和连接池管理2个部分,习惯上称为连接池。在系统初始化的时候,将数据库连接作为对象存储在内存中,当需要访问数据库时,从连接池中取出一个已建立的空闲连接对象。

使用数据源,获取其DataSource对象,通过该对象动态的获取数据库连接。另外,DataSource对象可以注册到名字服务(JNDI)中,可以通过名字服务获得DataSource对象,无需硬性编码驱动。

DriverManager是JDBC1提供的,DataSource是JDBC2新增的功能,提供了更好的连接数据源的方法。

JDBC程序

Public static void main(String[] args) {
           Connection connection = null;
           PreparedStatement preparedStatement = null;
           ResultSet resultSet = null;
           try {
              //1.加载数据库驱动
              Class.forName("com.mysql.jdbc.Driver");             
              //2.通过驱动管理类获取数据库链接
              connection =  DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "mysql");
              //3.定义sql语句 ?表示占位符
           String sql = "select * from user where username = ?";
              //4.获取预处理statement
              preparedStatement = connection.prepareStatement(sql);
              //5.设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
              preparedStatement.setString(1, "王五");
              //6.向数据库发出sql执行查询,查询出结果集
              resultSet =  preparedStatement.executeQuery();
              //7.遍历查询结果集
              while(resultSet.next()){
                  System.out.println(resultSet.getString("id")+"  "+resultSet.getString("username"));
              }
           } catch (Exception e) {
              e.printStackTrace();
           }finally{
              //8.释放资源
              if(resultSet!=null){
                  try {
                     resultSet.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                      e.printStackTrace();
                  }
              }
              if(preparedStatement!=null){

                  try {
                     preparedStatement.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
              }
              if(connection!=null){
                  try {
                     connection.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
              }
           }
       }

JDBC编程步骤

1、加载数据库驱动

2、创建并获取数据库链接

3、创建JDBC statement对象

4、设置SQL语句

5、设置SQL语句中的参数(使用preparedStatement)

6、通过statement执行SQL并获取结果

7、对SQL执行结果进行解析处理

8、释放资源(resultSet、preparedstatement、connection)

JDBC存在的问题

1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。

2、SQL语句在代码中硬编码,造成代码不易维护,实际应用SQL变化的可能较大,SQL变动需要改变JAVA代码。

3、使用PreparedStatement向占有位符号传参数存在硬编码,因为SQL语句的where条件不一定,可能多也可能少,修改SQL还要修改代码,系统不易维护。

4、对结果集解析存在硬编码(查询列名),SQL变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成POJO对象解析比较方便。

MyBatis介绍

MyBatis 本是Apache的一个开源项目iBatis, 2010年这个项目由Apache Software Foundation 迁移到了Google Code,并且改名为MyBatis 。2013年11月迁移到Github。

MyBatis是一个优秀的持久层框架,它对JDBC的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建Connection、创建Statement、手动设置参数、结果集检索等JDBC繁杂的过程代码。

Mybatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过java对象和statement中的SQL进行映射生成最终执行的sql语句,最后由MyBatis框架执行SQL并将结果映射成java对象并返回。

对比Hibernate和MyBatis

传统的JDBC编程给我们带来了连接数据库的功能,但其工作量相对较大,首先连接,然后处理JDBC底层事务,处理数据类型,还要对可能产生的异常进行捕捉处理并正确的关闭资源。

实际工作中,很少使用JDBC进行编程,提出了ORM模型,主要解决数据库数据和POJO对象的相互映射。

Hibernate和Mybatis都是ORM模型,Hibernate提供的是一种全表映射的模型,对JDBC的封装程度比较高。但Hibernate也有不少缺点,列举如下:

  1. 全表映射带来的不便,比如更新时需要发送所有的字段;
  2. 无法根据不同的条件组装不同的SQL;
  3. 对多表关联和复杂SQL查询支持较差,需要自己写SQL,返回后,需要自己将数据组装为POJO;
  4. 不能有效支持存储过程;
  5. 虽然有HQL,但性能较差,大型互联网系统往往需要优化SQL,而Hibernate做不到。

大型互联网环境中,灵活、SQL优化,减少数据的传递是最基本的优化方法,Hibernate无法满足要求,而MyBatis提哦给你了灵活、方便的方式,是一个半自动映射的框架。

MyBatis需要手工匹配提供POJO、SQL和映射关系,而全表映射的Hibernate只需要提供POJO和映射关系。

MyBatis可以配置动态SQL,可以解决Hibernate的表名根据时间变化,不同的条件下列明不一样的问题。可以优化SQL,通过配置决定SQL映射规则,也能支持存储过程,对于一些复杂和需要优化性能的SQL的查询它更加方便。

核心组件

核心组件主要包括以下几个:

  1. l SqlSessionFactoryBuilder:会根据配置信息或代码来生成SqlSessionFactory;
  2. l SqlSessionFactory:依靠工厂来生成SqlSession;
  3. l SqlSession:是一个既可以发送SQL去执行并返回结果,也可以获取Mapper的接口;
  4. l SQL Mapper:是MyBatis新设计的组件,由一个Java接口和XML文件构成,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果。

组件生命周期

SqlSessionFactory在MyBatis应用的整个生命周期中,每个数据库只对应一个SqlSessionFactory,可以实现一个工具类,以单例模式获取该对象。

SqlSession的生命周期在请求数据库处理事务的过程中,它是一个线程不安全的对象,在涉及多线程的时候要特别当心。它存活于一个应用的请求和操作,可以执行多条SQL,保证事务的一致性。

Mapper的作用是发送SQL,然后返回需要的结果,或者执行SQL修改数据库的数据,所以它应该在一个SqlSession事务方法之内,如同JDBC中一条SQL语句的执行,它最大的范围和SqlSession是相同的。

MyBatis配置

<?xml version="1.0" encoding="UTF-8"?>  
<configuration>  
    <properties/>  
    <settings/>  
    <typeAliases/>  
    <typeHandles/>  
    <objectFactory/>  
    <plugins/>  
    <environments>  
        <environment>  
            <transanctionManager/> <!-- 配置事务管理器 -->  
            <dataSource/> <!-- 配置数据源 -->  
        </environment>  
    </environments>  
  
    <databaseIdProvider/> <!-- 数据库厂商标识 -->  
    <mappers/> <!-- 映射器 -->  
</configuration>

properties元素

将一些公用、经常变更的值单独声明,能在配置文件的上下文中使用它,MyBatis提供了3种配置方式:

  1. l property子元素
  2. l properties配置文件
  3. 程序参数传递

property子元素

<properties>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mi-user"/>
        <property name="username" value="root"/>
        <property name="pwd" value="123456"/>
    </properties>

properties配置文件

创建一个配置文件 jdbc.properties

driver = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/mi-user
username = root
password = 123456

设置properties配置文件

<properties resource='jdbc.properties' />

程序参数传递

实际工作中,会遇到这种场景:系统由运维人员配置,生成数据库的密码对开发者是保密的,对用户名和密码进行了加密。可以通过程序参数传递的方式,先解密,再设置property。

//读入配置文件流
InputStream cfgStream = Resources.getResourceAsStream("mybatis-config.xml");
Reader cfgReader = new InputStreamReader(cfgStream);

//读入属性文件流
InputStream proStream = Resources.getResourceAsStream("jdbc.properties");
Reader proReader = new InputStreamReader(proStream);

Properties properties = new Properties();
properties.load(proReader);
//转换为明文
properties.setProperty("username",decode(properties.getProperty("username")));
properties.setProperty("pwd",decode(properties.getProperty("pwd")));

//创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(cfgReader,properties);

如果3种配置同时出现,优先级为第3种 > 第2种 > 第1种,推荐使用第2种,有特殊需求时使用第3种。

常用配置项

只介绍几个常用的配置项,想了解更多请查看官方文档。

  1. cacheEnabled:全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存,默认为true;
  2. lazyLoadingEnabled:延迟加载的全局开关,当开启时,所有关联对象都会延迟加载,特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态,默认为false;
  3. aggressiveLazyLoading:当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载,3.4.1版本之前默认为true,3.4.1之后默认为false;
  4. autoMappingBehavior:指定MyBatis应如何自动映射列到字段或属性,NONE 表示取消自动映射,PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集,FULL 会自动映射任意复杂的结果集,默认为PARTIAL;
  5. autoMappingUnknownColumnBehavior:指定发现自动映射目标未知列(或者未知属性类型)的行为,NONE: 不做任何反应,WARNING: 输出提醒日志,FAILING: 映射失败,默认为NONE;
  6. defaultStatementTimeout:设置超时时间,它决定驱动等待数据库响应的秒数;
  7. mapUnderscoreToCamelCase:是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射,默认为false;
  8. defaultEnumTypeHandler:指定 Enum 使用的默认 TypeHandler,默认为org.apache.ibatis.type.EnumTypeHandler;
  9. returnInstanceForEmptyRow:当返回行的所有列都是空时,MyBatis默认返回null。 当开启这个设置时,MyBatis会返回一个空实例,默认为false;
  10. localCacheScope:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据,默认为SESSION;
  11. logImpl:指定日志的具体实现,比如SLF4J|LOG4J|COMMONS_LOGGING等;

别名

在配置映射文件中,需要指定类的全限定名,为了简化,可以声明一个简短的名称去指代它,可以在MyBatis上下文中使用。系统已经为我们定义了常用的类型,比如数值、字符串、日期、集合等。对于自定义的业务POJO,需要自定义别名。

<typeAliases>
     <typeAlias alias="role" type="com.learn.chapter2.po.Role"/>
</typeAliases>

也可以通过注解方式进行,首先配置一个扫描的包,然后在类定义时添加注解@Alias("role")。

<typeAliases>
     <package name="com.learn.chapter2.po" />
</typeAliases>
@Alias("role")
public class Role{
}

类型处理器

MyBatis在预处理语句中设置一个参数时,或者从结果集中取出一个值时,都会用注册了的typeHader进行处理。typeHander的作用就是将参数从javaType转化为jdbcType,或者从数据库取出结果时把jdbcType转化为javaType。

系统内部已经定义了常用的类型处理器,有些情况下,需要自定义。

MyBatis也提供了枚举类型的类型处理器,有2个转化枚举类型的typeHandler,EnumTypeHandler和EnumOrdinalTypeHandler,其中EnumTypeHandler是使用枚举字符串名称作为参数传递的,EnumOrdinalTypeHandler是使用整数下标作为参数传递的。

但这2个枚举类型应用不那么广泛,更多的时候,需要自定义typeHandler进行处理。

自定义类型处理器,首先要定义类型处理类,实现TypeHandler泛型接口:

public class SexEnumTypeHandler implements TypeHandler<Sex> {

    @Override
    public void setParameter(PreparedStatement ps, int i, Sex sex, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, sex.getId());
    }

    @Override
    public Sex getResult(ResultSet rs, String name) throws SQLException {
        return Sex.getSex(rs.getInt(name));
    }

    @Override
    public Sex getResult(ResultSet rs, int id) throws SQLException {
        return Sex.getSex(id);
    }

    @Override
    public Sex getResult(CallableStatement cs, int id) throws SQLException {
        return Sex.getSex(cs.getInt(id));
    }
}

然后注册自定义的TypeHandler

<typeHandlers>
   <typeHandler handler="com.qqdong.study.SexEnumTypeHandler" javaType="sex"/>
</typeHandlers>

最后,在定义映射器时,指定typeHandler即可

<select id="getUser" parameterType="long" resultType="userMap">
</select>

<resultMap id="userMap" type="user">
    <result column="sex" property="sex" typeHandler="com.qqdong.study.SexEnumTypeHandler">
</resultMap>

ObjectFactory

MyBatis在构建一个结果返回的时候,都会使用ObjectFactory去构建POJO,在MyBatis中可以定制自己的对象工厂。一般不用配置,使用默认的DefaultObjectFactory即可。

environments配置环境

配置环境可以注册多个数据源,每个数据源包括基本配置和数据库事务配置。

 

<environments default="development">
    <environment id="development">
    <!-- 采用jdbc事务管理 -->
        <transactionManager type="JDBC">
            <property name="autoCommit" value="false">
        </transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
 </environments>

其中,transactionManager指定数据库事务,有3种配置方法:

  1. l JDBC,采用JDBC方式管理事务,在独立编码中常常使用;
  2. l MANAGED,采用容器方式管理事务,在JNDI数据源中常用;
  3. 自定义,由使用者自定义数据库事务管理方法;
  4. DataSource标签,配置数据源连接信息,type配置对数据库连接方式,有以下几种:
  5. l UNPOOLED:非连接池数据库;
  6. l POOLED:连接池数据库;
  7. l JNDI:JNDI数据源;
  8. 自定义数据源;

映射器

映射器是由Java接口和XML文件(或注解)共同组成的,作用如下:

  1. 定义参数类型
  2. 描述缓存
  3. 描述SQL语句
  4. 定义查询结果和POJO的映射关系

首先,定义Java接口:

public interface RoleMapper{
public Role getRole(Long id);
}

然后,定义映射XML文件,RoleMapper.xml

<? 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">

<mapper namespace ="com.learn.chapter2.mapper.RoleMapper">
<select id="getRole" paramterType="long" resultType="role" >
select id,role_name as roleName , note from t_role where id=#{id}
</select>
</mapper>

POJO对象Role的定义比较简单,就不列出了。#{id}为这条SQL的参数,SQL列的别名和POJO的属性名称保持一致,会把这条语句的查询结果自动映射到Role属性上,这就是自动映射。执行查询:

RoleMapper roleMapper=sqlSession.getMapper(RoleMapper.class);
Role role=roleMapper.getRole(1L);
String roleName=role.getRoleName();

通过映射器,可以很容易的进行数据的增删改查操作,我们抽象下进行这些操作的关键点:传递查询参数、组装各种场景下的查询条件、关联查询、将查询结果映射为Java Bean对象或集合等。另外,可以通过延迟加载、缓存提高数据查询的性能。

映射器的主要元素

映射器是由Java接口和XML文件(或注解)共同组成的,Java接口主要定义调用者接口,XML文件是配置映射器的核心文件,包括以下元素:

  1. l select 查询语句,可以自定义参数,返回结果集;
  2. l insert 插入语句,返回一个整数,表示插入的条数;
  3. l update 更新语句,返回一个整数,表示更新的条数;
  4. l delete 删除语句,返回一个整数,表示删除的条数;
  5. l sql 允许定义一部分SQL,然后再各个地方引用;
  6. l resultMap 用来描述从数据库结果集中来加载对象,还可以配置关联关系;
  7. l cache 给定命名空间的缓存配置;

增、删、改、查操作

查找

执行select语句前,需要定义参数,执行后,也提供了强大的映射规则或自动映射,将返回的结果集绑定到java bean中。

select元素有很多配置项,下面简单说明下:

  1. l paramterType:传入的参数类型,可以是基本类型、map、自定义的java bean;
  2. l resultType:返回的结果类型,可以是基本类型、自定义的java bean;
  3. l resultMap:它是最复杂的元素,可以配置映射规则、级联、typeHandler等,与ResultType不能同时存在;
  4. l flushCache:在调用SQL后,是否要求清空之前查询的本地缓存和二级缓存,主要用于更新缓存,默认为false;
  5. l useCache:启动二级缓存的开关,默认只会启动一级缓存;
  6. l timeout:设置超时参数,等超时的时候将抛出异常,单位为秒;
  7. l fetchSize:获取记录的总条数设定;

比如根据米聊号获取用户信息:

 

<select id="findByMiliao" parameterType="string" resultType="User">
        select
        u.*
        from mxt_user u
        where u.miliao=#{miliao}
</select>

 

设置项autoMappingBehavior,默认为自动映射没有定义嵌套结果集映射的结果集;还有设置项mapUnderscoreToCamelCase,设置为true时,会自动将以「下划线」命名的数据库字段名,自动映射为以「驼峰式」命名的POJO。

传递多个参数时,有3种方式:

  1. 使用Map参数;
  2. 使用注解方式传递;
  3. 使用java bean;

使用注解方式如下:

 

public List<Role> findRoleByNameAndNote(@Param("roleName") String rolename,
@Param("note") String note);

使用Map传递参数,会导致业务可读性丧失,导致以后扩展和维护不方便,不建议;如果参数个数<=5,建议使用注解的方式,因为过多参数将给调用者带来困难;如果参数个数>5,建议使用JavaBean方式;

insert

属性和select大部分都相同, 说下3个不同的属性:

  1. l keyProperty:指定哪个列是主键,如果是联合主键可以用逗号隔开;
  2. l keyColumn:指定第几列是主键,不能和keyProperty共用;
  3. l useGeneratedKeys:是否使用自动增长,默认为false;
  4. useGeneratedKeys设为true时,在插入的时候,会回填Java Bean的id值,通过返回的对象可获取主键值。

如果想根据一些特殊关系设置主键的值,可以在insert标签内使用selectKey标签,比如:如果t_role没有记录,则需要设置为1,否则取最大id加2:

<insert id="insertRole" useGeneratedKeys="true" keyProperty="id" >
    <selectKey keyProperty="id" resultType="int" order="before">
        select if(max(id) is null,1,max(id)+2) as newId from t_role
    </selectKey> 
</insert>

参数

上面已经介绍了参数传递,另外可以指定参数的类型去让对应的typeHandler处理它们。

#{age , javaType=int , jdbcType=NUMERIC }

还可以对一些数值型的参数设置其保存的精度

#{price, javaType=double , jdbcType=NUMERIC , numericScale=2 }

 

一般都是传递字符串,设置的参数#{name}大部分情况下,会创建预编译语句,但有时候传递的是SQL语句本身,不是需要的参数,可以通过$符号表示,比如传递参数columns为"col1,col2,col3",可以写成下面语句:

select ${columns} from t_tablename

但要注意sql的安全性,防止sql注入。

sql元素

定义:

 

<sql id="role_columns">
    id,role_name,note
</sql>

 

使用:

 

<include refid="role_columns">
    <property name="prefix" value="r" />
</include>

 

结果映射

元素介绍

resultMap是MyBatis里面最复杂的元素,它的作用是定义映射规则、级联的更新、定制类型转换器等。

由以下元素构成:

 

<resultMap>
    <constructor> <!-- 配置构造方法 -->
        <idArg/>
        <arg/>
    </constructor>
    <id/> <!--指明哪一列是主键-->
    <result/> <!--配置映射规则-->
    <association/> <!--一对一-->
    <collection/> <!--一对多-->
    <discriminator> <!--鉴别器级联-->
        <case/>
    </discriminator>
</resultMap>

 

有的实体不存在没有参数的构造方法,需要使用constructor配置有参数的构造方法:

<resultMap id="role" type="com.xiaomi.kfs.mcc.core.domain.Role">
    <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="role_name" javaType="string"/>
    </constructor>
</resultMap>

id指明主键列,result配置数据库字段和POJO属性的映射规则:

<resultMap id="role" type="com.xiaomi.kfs.mcc.core.domain.Role">
    <id property="id" column="id" />
    <result property="roleName" column="role_name" />
    <result property="note" column="note" />
</resultMap>

association、collection用于配置级联关系的,分别为一对一和一对多,实际中,多对多关系的应用不多,因为比较复杂,会用一对多的关系把它分解为双向关系。

discriminator用于这样一种场景:比如我们去体检,男和女的体检项目不同,如果让男生去检查妇科项目,是不合理的, 通过discriminator可以根据性别,返回不同的对象。

延迟加载

级联的优势是能够方便地获取数据,但有时不需要获取所有数据,这样会多执行几条SQL,性能下降,为了解决这个问题,需要使用延迟加载,只要使用相关级联数据时,才会发送SQL去取回数据。

MyBatis的配置中有2个全局的参数 lazyLoadingEnabled 和 aggressiveLazyLoading,第一个的含义是是否开启延迟加载功能,第二个的含义是对任意延迟加载属性的调用,会使延迟加载的对象完整加载,否则只会按需加载。

理解下aggressiveLazyLoading属性,比如学生对象的关联对象如下:

当访问学生信息的时候,会根据鉴别器把健康的情况也会查找出来;当访问课程成绩的时候,同时也会把学生证信息查找出来,因为在默认情况下,MyBatis是按层级延迟加载的。 但这不是我们需要的,并不希望在访问成绩的时候,去加载学生证的信息,可以设置aggressiveLazyLoading为false,按需进行延迟加载数据。

上面的2个属性都是全局设置,也可以在association和collection元素上加上属性值fetchType,它有两个取值eager和lazy。

缓存

在没有显示配置缓存时,只开启一级缓存,一级缓存是相对于同一个SqlSession而言的,在参数和SQL完全一样的情况下,使用同一个SqlSession对象调用同一个Mapper的方法,只会执行一次SQL。

如果是不同的SqlSession对象,因为不同SqlSession是相互隔离的,即使用相同的Mapper、参数和方法,还是会再次发送SQL到数据库去执行。

二级缓存是SqlSessionFactory层面上的,需要进行显示配置,实现二级缓存的时候,要求POJO必须是可序列化的,只需要简单配置即可:

<cache />

这样很多设置是默认的,有如下属性可以配置:

l eviction:代表缓存回收策略,可选值有LRU最少使用、FIFO先进先出、SOFT软引用,WEAK弱引用;

l flushInterval:刷新间隔时间,单位为毫秒,如果不配置,当SQL被执行时才会刷新缓存;

l size:引用数目,代表缓存最多可以存储多少对象,不宜设置过大,设置过大会导致内存溢出;

l readOnly:只读,意味着缓存数据只能读取不能修改;

在大型服务器上,可能会使用专用的缓存服务器,比如Redis缓存,可以通过实现org.apache.ibatis.cache.Cache接口很方便的实现:

public interface Cache {
    String getId(); //缓存编号
    void putObject(Object var1, Object var2); //保存对象
    Object getObject(Object var1); //获取对象
    Object removeObject(Object var1); //移除对象
    void clear(); //清空缓存
    int getSize(); //获取缓存对象大小
    ReadWriteLock getReadWriteLock(); //获取缓存的读写锁
}

动态SQL

很多时候,需要根据不同的场景组装查询条件,MyBatis提供对SQL语句动态的组装能力。

主要提供以下几种元素:

  1. l if:判断语句,但条件分支判断;
  2. l choose (when、otherwise):多条件分支判断;
  3. l trim (where、set):处理一些SQL拼装问题;
  4. l foreach:循环语句,在in语句等列举条件常用;
  5. l bind:通过OGNL表达式去自定义一个上下文变量,可以方便使用;

trim可以处理 and 和 逗号 拼接的问题,举例如下:

<select id="findRoles" parameterType="string" >
  select id,role_name,note from t_role
  <trim prefix="where" prefixOverrides="and">
      <if test="roleName!=null and roleName!=''">
        and role_name like concat('%',#{roleName},'%')
      </if>
  </trim>
</select>

另外,可以使用set元素设置更新的字段列表:

<update id="updateRole" parameterType="role">
    update t_role
    <set>
        <if test="roleName!=null and roleName!=''">
            role_name=#{roleName},
        </if>
        <if test="note!=null and note!=''">
            note=#{note}
        </if>
    </set>
    where id=#{id}
</update>

理解反射和动态代理

反射

官网对反射的定义:

可以通过java代码,获取当前加载类的字段、方法、构造函数等信息,并在安全限制内,使用反射字段、方法、构造函数进行操作。

简单来说,可以在运行时获得程序中每一个类型的成员信息。程序中定义的对象,其类型都是在编译期确定的,而反射可以动态地创建对象,并访问或调用其成员。

动态代理

所谓代理,是一个人或组织代替另一个人或组织做事,主要有3个角色:访问者、代理人、被代理人,访问者经由代理人,与被代理人交互,中间会加入一些自己的处理。

所谓的动态代理,是说在编译时不需要定义代理类,而是在运行时创建,这个是关键:在运行时创建代理类。

Class对象

Class类是一个实实在在的类,存在于java.lang包中,用来表示运行时类型信息。Class对象表示自定义类的类型信息,比如创建一个User类,JVM就会创建一个User对应的Class对象,保存User类相关的类型信息,该对象保存在jvm堆中,作为访问方法区中User类型信息的接口。

在使用自定义类时,会首先检查这个类的Class对象是否已经加载,如果没有加载,默认的类加载器就会先根据类名查找.class文件,Class对象就会被加载到内存。

可以通过下面3种方法获取Class对象:

  1. 使用Class类的forName静态方法;
  2. 直接获取某一个对象的class;
  3. 调用某个对象的getClass()方法;

Class对象是反射的基础,提供了获取类信息的方法,后面会介绍。

反射提供的功能

java反射框架主要提供以下内容:

  1. 在运行时判断对象所属的类;
  2. 在运行时创建对象;
  3. 在运行时获取类包含的成员变量、方法、父类、接口等信息;
  4. 在运行时调用一个对象的方法;

下面举例说明相关功能

创建实例:

 

//获取String所对应的Class对象
Class<?> c = User.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
User user = (User)constructor.newInstance("calm");

 

获取方法:

 

//返回类或接口声明的所有方法,包括私有的,但不包括继承的方法
public Method[] getDeclaredMethods() throws SecurityException

//所有public方法,包括继承的方法
public Method[] getMethods() throws SecurityException

//返回一个特定的方法,第一个参数为方法名称,后面的参数为方法参数对应Class的对象
public Method getMethod(String name, Class<?>... parameterTypes)

 

调用方法:

 

Class<?> userClass=User.class;
Object obj = userClass.newInstance();
Method method =klass.getMethod("addRole",String.class);
method.invoke(obj,"超级管理员");

 

JDK动态代理

JDK本身提供了动态代理的实现,要求被代理者必须实现接口。

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,InvocationHandler h)

第一个参数为类加载器,第二个参数是被代理者实现的接口列表,第三个参数是实现了InvocationHandler接口的对象。

InvocationHandler是一个接口,用于规范执行被代理者的方法,可在执行方法前后,添加公共的处理代码。生成的动态代理类包含一个InvocationHandler属性,调用对应方法时,会触发invoke方法的调用。

public class JDKProxy implements InvocationHandler {    
    
    private Object targetObject;//被代理对象   
    
    public Object newProxy(Object targetObject) {        
        this.targetObject = targetObject;     
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(), this);
}    
    
    public Object invoke(Object proxy, Method method, Object[] args)//invoke方法    
            throws Throwable {    
        Object ret = null;         
        ret  = method.invoke(targetObject, args);      
        return ret;    
    }    
}

测试代码:

 

JDKProxy jdkProxy=new JDKProxy();
UserService userService = (UserService) 
jdkProxy.newProxy(new UserServiceImp());    
userService.addRole("超级管理员");    

 

JDK动态代理的基本原理是根据定义好的规则,用传入的接口创建一个新类。

CGLIB动态代理

JDK动态代理要求必须有接口,CGLIB(Code Generate Library)动态代理没有这个要求,它是通过创建一个被代理类的子类,然后使用ASM字节码库修改代码来实现的。

public class CGLibProxy implements MethodInterceptor {    
    private Object targetObject; //被代理对象

    public Object createProxyObject(Object obj) {    
        this.targetObject = obj;    
        Enhancer enhancer = new Enhancer();    
        enhancer.setSuperclass(obj.getClass());    
        enhancer.setCallback(this);    
        Object proxyObj = enhancer.create();    
        return proxyObj;
    }    
    
    public Object intercept(Object proxy, Method method, Object[] args,    
            MethodProxy methodProxy) throws Throwable {    
        Object obj = null;       
        obj = method.invoke(targetObject, args);    
        return obj;    
    }       
}

测试代码:

 

CGLibProxy cgLibProxy=new CGLibProxy();
UserService userService = (UserService) 
cgLibProxy.newProxy(new UserServiceImp());    
userService.addRole("超级管理员");  

 

MyBatis解析和运行原理

构建SqlSessionFactory过程

构建主要分为2步:

通过XMLConfigBuilder解析配置的XML文件,读出配置参数,包括基础配置XML文件和映射器XML文件;

使用Configuration对象创建SqlSessionFactory,SqlSessionFactory是一个接口,提供了一个默认的实现类DefaultSqlSessionFactory。

说白了,就是将我们的所有配置解析为Configuration对象,在整个生命周期内,可以通过该对象获取需要的配置。

由于插件需要频繁访问映射器的内部组成,会重点这部分,了解这块配置抽象出来的对象:

MappedStatement

它保存映射器的一个节点(select|insert|delete|update),包括配置的SQL,SQL的id、缓存信息、resultMap、parameterType、resultType等重要配置内容。

它涉及的对象比较多,一般不去修改它。

SqlSource

它是MappedStatement的一个属性,主要作用是根据参数和其他规则组装SQL,也是很复杂的,一般也不用修改它。

BoundSql

对于参数和SQL,主要反映在BoundSql类对象上,在插件中,通过它获取到当前运行的SQL和参数以及参数规则,作出适当的修改,满足特殊的要求。

BoundSql提供3个主要的属性:parameterObject、parameterMappings和sql,下面分别来介绍。

parameterObject为参数本身,可以传递简单对象、POJO、Map或@Param注解的参数:

  1. 传递简单对象(int、float、String等),会把参数转换为对应的类,比如int会转换为Integer;
  2. 如果传递的是POJO或Map,paramterObject就是传入的POJO或Map不变;
  3. 如果传递多个参数,没有@Param注解,parameterObject就是一个Map<String,Object>对象,类似这样的形式{"1":p1 , "2":p2 , "3":p3 ... "param1":p1 , "param2":p2 , "param3",p3 ...},所以在编写的时候可以使用#{param1}或#{1}去引用第一个参数;
  4. 如果传递多个参数,有@Param注解,与没有注解的类似,只是将序号的key替换为@Param指定的name;

parameterMappings,它是一个List,元素是ParameterMapping对象,这个对象会描绘sql中的参数引用,包括名称、表达式、javaType、jdbcType、typeHandler等信息。

sql,是写在映射器里面的一条sql。

有了Configuration对象,构建SqlSessionFactory就简单了:

sqlSessionFactory = new SqlSessionFactoryBuilder().bulid(inputStream);

SqlSession运行过程

映射器的动态代理

Mapper映射是通过动态代理来实现的,使用JDK动态代理返回一个代理对象,供调用者访问。

首先看看实现InvocationHandler接口的类,它是执行本代理方法的关键,可以看到,Mapper是一个接口,会生成MapperMethod对象,调用execute方法。

public class MapperProxy<T> implements InvocationHandler, Serializable {
  
  .....
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
}

看下面的代码,MapperMethod采用命令模式,根据不同的sql操作,做不同的处理。

public class MapperMethod {
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
        
        ......
        
      }
    }
  }

最后看下,生成代理类的方法,就是使用JDK动态代理Proxy来创建的。

public class MapperProxyFactory<T> {

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

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

}

总结下映射器的调用过程,返回的Mapper对象是代理对象,当调用它的某个方法时,其实是调用MapperProxy#invoke方法,而映射器的XML文件的命名空间对应的就是这个接口的全路径,会根据全路径和方法名,便能够绑定起来,定位到sql,最后会使用SqlSession接口的方法使它能够执行查询。

SqlSession下的四大对象

通过上面的分析,映射器就是一个动态代理对象,进入到了MapperMethod的execute方法,它经过简单的判断就进入了SqlSession的删除、更新、插入、选择等方法,这些方法如何执行是下面要介绍的内容。

Mapper执行的过程是通过Executor、StatementHandler、ParameterHandler和ResultHandler来完成数据库操作和结果返回的,理解他们是编写插件的关键:

  1. l Executor:执行器,由它统一调度其他三个对象来执行对应的SQL;
  2. l StatementHandler:使用数据库的Statement执行操作;
  3. l ParameterHandler:用于SQL对参数的处理;
  4. l ResultHandler:进行最后数据集的封装返回处理;

MyBatis中存在三种执行器:

  1. l SIMPLE:简易执行器,默认的执行器;
  2. l REUSE:执行重用预处理语句;
  3. l BATCH:执行重用语句和批量更新,针对批量专用的执行器;

SimpleExecutor为例,说明执行过程

public class SimpleExecutor extends BaseExecutor {

  /**
  * 执行查询操作
  */
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  
  /**
  * 初始化StatementHandler
  */
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
  }
  
  /**
  * 执行查询
  */
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    statement.execute(sql);
    return resultSetHandler.<E>handleResultSets(statement);
  }
}

可以看到最后会委托给StatementHandler会话器进行处理,它是一个接口,实际创建的是RoutingStatementHandler对象,但它不是真实的服务对象,它是通过适配器模式找到对应的StatementHandler执行的。在MyBatis中,StatementHandler和Executor一样分为三种:SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。

Executor会先调用StatementHandler的prepare方法预编译SQL语句,同时设置一些基本运行的参数。然后调用parameterize()方法启用ParameterHandler设置参数,完成预编译,跟着执行查询,用ResultHandler封装结果返回给调用者。

 

posted @ 2019-01-09 11:47  秋风伊人  阅读(285)  评论(0编辑  收藏  举报