《Mybatis从入门到精通》读书笔记(一)

前言

最近在在看刘增辉兄台写的这本Mybatis的入门入籍,一来想系统复习下Mybatis相关的知识,把Mybatis的知识点串一遍,一来想学习下Mybatis的一些高级部分,比如自定义插件,结合springboot使用,源码分析等等。

整本书读下来感觉非常顺畅,该讲的地方也都讲到了,Mybatis本身也就这些东西,测试类什么的写的也都很全很细致,总之感觉还是很用心的一本书,有不小的收获。

为了方便以后复习Mybatis这块的知识,把书中的一些重点,难点的东西记录下来,以备后用。

好了,开始正题!

第一章. Mybatis 入门

Mybatis是一款优秀的支持自定义SQL查询、存储过程和高级映射的持久化框架,消除了几乎所有的jdbc代码和参数的手动设置以及结果集的检索。

Mybatis可以使用XML或注解进行配置和映射,Mybatis通过将参数映射到配置的SQL形成最终执行的SQL语句,最后将执行SQL结果映射成Java对象返回。

与其他的ORM(对象关系映射)框架不同,Mybatis并没有将Java对象与数据库表关联起来,而是将Java方法与SQL语句关联。

Mybatis允许用户充分利用数据库的各种功能,例如存储过程,视图,各种复杂的查询以及某数据库的专有特性。

如果要对遗留数据库、不规范的数据库进行操作,或者要完全控制SQL的执行,Mybatis将会是一个不错的选择。

Mybatis提供了一个映射引擎,声明式的将SQL语句的执行结果与对象树映射起来。通过使用一种内建的类xml表达式语言,SQL语句可以被动态生成。

Mybatis提供了默认情况下基于Java Hashmap的缓存实现,以及与OScache、Ehcache、Hazelcast和Memcached连接的默认连接器,同时还提供了API供其他缓存实现使用。 

第二章. Mybatis XML方式的基本用法

由于Java中的基本数据类型会有默认值,在动态SQL中和null进行比较时结果总为true,所以在实体类中不要使用基本数据类型。

以前使用SqlSession通过命名空间调用Mybatis方式时,首先需要用到命名空间和方法id组成的字符串来调用相应的方法。当参数多余1个的时候,需要将所有参数放到一个Map对象中。通过Map传递多个参数,使用起来很不方便,而且还无法避免很多重复的代码。使用接口调用方法就会方便很多,Mybatis使用Java的动态代理可以直接通过接口来调用相应的方法,不需要提供接口的实现类,更不需要在接口类中使用SqlSession以通过命名空间间接调用。另外,当有多个参数的时候,通过参数注解@Param设置参数的名字省去了手动构造Map参数的过程,尤其在Spring中使用的时候,可以配置为自动扫描所有的接口类,直接将接口注入到需要用到的地方。

接口可以配合xml方式使用,也可以配合注解方式来使用。xml方式可以单独使用,但是注解方式必须在接口中使用。

映射XML和接口的命名需要符合如下规则:

1. 当只使用XML而不使用接口的时候,namespace值可以设置为任意不重复的名称。

2. 标签的id属性值在任何时候都不能出现英文".",并且同一个命名空间下不能出现重复的id。

3. 因为接口方法是可以重载的,所以接口中可以出现多个同名但参数不同的方法,但是XML中id的值不能重复,因而接口中的所有同名方法会对应着XML中的同一个id的方法。最常见的用法就是,同名方法中其中一个方法增加一个RowBound类型的参数用于实现分页查询。

2.1. <select/> 标签用法

  • constructor:通过构造方法注入属性的结果值。构造方法中的idArg、arg参数分别对应着id、result标签,它们的含义相同,只是注入方式不同。

  • resultMap中的id和result标签包含的属性相同。不同的地方在于。id代表的是主键(或唯一值)的字段(可以有多个),他们的属性值是通过setter方法注入的。

  • property:映射到列结果的属性。可以映射简单的如“username”这样的属性,也可以映射一些复杂对象中的属性,例如“address.number”,这会通过“.”方式的属性嵌套赋值。

  • javaType:一个Java类的完全限定名,或一个类型别名(通过TypeAlias配置或者默认的类型)。如果映射到一个JavaBean,Mybatis通常可以自动判断属性的类型。如果映射到HashMap,则需要明确的指定javaType的类型(即resultMap的Type属性值设定)。

  • jdbcType:列对应的数据库类型。JDBC类型仅仅需要对插入,更新,删除操作可能为空的列进行处理。这是JDBC jdbcType的需要,而不是Mybatis的需要。

  • typeHandler:使用这个属性可以覆盖默认的类型处理器。这个属性值是类的完全限定名或类型别名。

接口中定义的返回值类型必须和XML中配置的resultType类型(或resultMap中的Type)一致,否则就会因为类型不一致而抛出异常。

当返回值最多只有1个结果的时候(可以0个),可以将接口返回值定义为T,而不是List<T>,当然,如果将返回值改为List<T>或T[],也没有问题,只是不建议这么做。刚执行的SQL返回多个结果时,必须使用List<T>或T[]作为返回值,如果使用T,就会抛出TooManyResultsException异常。

可以通过在resultMap中配置property属性和column列的映射,或者在SQL中设置别名这两种方式实现将查询列映射到对象属性的目的。

property属性或别名要和对象中属性的名字相同,但是实际匹配时,Mybatis会先将两者都转为大写形式,然后再判断是否相同。即property=“username”和property=“userName”都可以匹配到对象的userName属性上。判断是否相同的时候要使用USERNAME,因此在设置property属性或别名的时候,不需要考虑大小写是否一致。但是为了便于阅读,要尽可能按照统一的规则来设置。

在数据库中,由于大多数数据库设置不区分大小写,因此下划线方式的命名很常见,如user_name, user_email。在Java中,一般都使用驼峰命名,如userName,userEmail。因为数据库和Java中的这两种命名方式很常见,因此Mybatis还提供了一个全局属性mapUnderscoreToCamelCase, 通过配置这个属性为true可以自动将以下划线方式命名的数据库列映射到Java对象的驼峰式命名属性中。

2.2. <insert/> 标签用法

  • parameterType:参数的完全限定类名或别名,属性可选,因为Mybatis可以推断出传入语句的具体参数,因此不建议配置此参数。

  • flushCache:默认值为true,任何时候只要语句被调用,都会清空一级缓存和二级缓存

  • timeout:设置在抛出异常前,驱动程序等待数据库返回请求结果的秒数。

  • useGeneratedKeys:默认值为false。如果设置为true,Mybatis会使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键。

  • keyProperty:Mybatis通过getGeneratedKeys获取主键值后将要赋值的属性名。如果希望得到多个数据库自动生成的列,属性值也可以是以逗号分隔的属性名称列表。

  • keyColumn:仅对INSERT和UPDATE有用。通过生成的键值设置表中的列名,这个设置仅在某些数据库(如PostgreSQL)中是必须的,当主键列不是表中的第一列时需要设置。如果需要得到多个生成的列,也可以是逗号分隔的属性名称列表。

为了防止类型错误,对于一些特殊的数据类型,建议指定具体的jdbcType值。例如headImg指定BLOB类型,createTime指定TIMESTAMP类型。

由于数据库区分date,time,datetime类型,但是Java中一般都使用java.util.Date类型。因此为了保证数据类型的正确,需要手动指定日期类型。date,time,datetime对应的JDBC类型分别为DATE、TIME、TIMESTAMP。

javaType jdbcType 数据库类型 测试结果
Date Timestamp(不指定默认) datetime  正确
Date DATE datetime  正确
Date TIME datetime  错误

通过上面的测试,说明数据库的datetime类型可以存储DATE(时间部分默认为00:00:00)和TIMESTAMP这两种类型的时间,不能存储TIME类型的时间。将数据库的字段类型修改为time时,就可以正确映射过去了。

2.3. 使用JDBC方式返回主键自增的值

在使用主键自增(如MySQL、SQL Server数据库)时,插入数据库后可能需要得到自增的主键值,然后使用这个值进行一些其他的操作。

<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
	insert into sys_user(
		user_name, user_password, user_email, 
		user_info, head_img, create_time)
	values(
		#{userName}, #{userPassword}, #{userEmail},
		#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
</insert>

useGeneratedKeys设置为true后,Mybatis会使用JDBC的getGeneratedKeys方法来取出数据库内部生成的主键。获取主键值后将其赋值给keyProperty设置的属性。

当需要设置多个属性时,使用逗号隔开,这种情况下通常还需要设置keyColumn属性,按顺序指定数据库的列,这里列的值会和keyProperty配置的属性一一对应。

2.4. 使用selectKey方式返回主键自增的值

JDBC方式只适用于支持主键自增的数据库。有些数据库(如Oracle)不提供主键自增的功能,而是使用序列得到一个值,然后将这个值赋给id,再将数据插入数据库。对于这种情况,可以采用另外一种方式:使用<selectKey>标签来获取主键的值,这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库。

<!--MySQL版本-->
<insert id="insert3">
	insert into sys_user(
		user_name, user_password, user_email, 
		user_info, head_img, create_time)
	values(
		#{userName}, #{userPassword}, #{userEmail}, 
		#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
	<selectKey keyColumn="id" resultType="long" keyProperty="id" order="AFTER">
		SELECT LAST_INSERT_ID()
	</selectKey>
</insert>

keyColumn、keyProperty:和useGeneratedKeys的用法含义相同

resultType:设置返回值类型

order:和使用的数据库有关。在MySQL中,order设置为AFTER,因为主键值在insert执行成功后才能获取到。而在oracle中,order设置为BEFORE,因为oracle需要先从序列获取值,然后再将值作为主键插入数据库

<!-- Oracle 的例子,查询多个列的时候需要 keyColumn -->
<insert id="insertOracle">
	<selectKey keyColumn="id" resultType="long" keyProperty="id" order="BEFORE" databaseId="ORACLE">
		SELECT SEQ_USER.nextval from dual
	</selectKey>
	insert into sys_user(
		id, user_name, user_password, user_email, 
		user_info, head_img, create_time)
	values(
		#{id}, #{userName}, #{userPassword}, #{userEmail}, 
		#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
</insert>

可以发现,selectKey元素放置的位置和之前的MySQL例子中的不同,其实这个元素放置的位置不会影响selectKey中的方法在insert前面或者后面执行的顺序,影响执行顺序的是order属性,这么写仅仅是为了符合实际的执行顺序,看起来更直观而已。

Oracle方式的insert语句中明确写出了id列和值#{id},因为执行selectKey中的语句后id就有值了,我们需要把这个序列值作为主键值插入到数据库中,所以必须指定id列,如果不指定这一列,数据库就会因为主键不能为空而抛出异常。

 

2.5. 接口方法中传入多个参数的用法

在实际应用中经常会遇到使用多个参数的情况。我们可以将多个参数合并到一个JavaBean中,并使用这个JavaBean作为接口方法的参数。这种方法用起来很方便,但是并不适用于全部的情况,因为不能只为了两三个参数去创建新的JavaBean类,因此对于参数比较少的情况,还有两种方式可以采用:使用Map类型作为参数或使用@Param注解。

使用Map类型作为参数的方法,就是在Map中通过key来映射XML中SQL使用的参数值名字,value用来存放参数值,需要多个参数时,通过Map的key-value方式传递参数值,由于这种方式还需要自己手动创建Map以及对参数进行赋值,其实并不简洁,所以着重掌握@Param注解这种方式。

先来看一个错误的栗子!

如果在接口中使用多个参数但不适用@Param注解会发生什么错误。

List<SysRole> selectRolesByUserIdAndRoleEnabled(Long userId, Integer enabled);
<select id="selectRolesByUserIdAndRoleEnabled" resultType="tk.mybatis.simple.model.SysRole">
    select 
		r.id, 
		r.role_name roleName, 
		r.enabled,
		r.create_by createBy,
		r.create_time createTime
	from sys_user u
	inner join sys_user_role ur on u.id = ur.user_id
	inner join sys_role r on ur.role_id = r.id
    where u.id = #{userId} and r.enabled = #{enabled}
</select>

测试结果报错:

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.binding.BindingException: Parameter 'userId' not found. Available parameters are [0, 1, param1, param2]

这个错误表示,XML可用的参数只有0、1、param1、param2,没有userId。

0和1,param1和param2都是Mybatis根据参数位置自定义的名字,这时如果将XML中的#{userId}改为#{0}或#{param1},将#{enabled}改为#{1}或#{param2},这个SQL就可以正常执行了,但是实际上并不建议这么做!

给参数配置@Param注解后,MyBatis就会自动将参数封装成Map类型,@Param注解值会作为Map中的key,因此在SQL部分就可以通过注解设置的值来使用参数。

当只有一个参数(基本类型或拥有TypeHandler配置的类型)的时候,可以不使用注解指定参数名在XML中就可以直接通过变量名来获取参数值,因为在这种情况下(集合和数组除外),Mybatis不关心这个参数叫什么名字就会直接把这个唯一的参数值拿来使用。

当参数是JavaBean类型时,使用@Param的情形:

List<SysRole> selectRolesByUserAndRole(@Param("user")SysUser user, @Param("role")SysRole role);
<select id="selectRolesByUserAndRole" resultType="tk.mybatis.simple.model.SysRole">
    select 
		r.id, 
		r.role_name roleName, 
		r.enabled,
		r.create_by createBy,
		r.create_time createTime
	from sys_user u
	inner join sys_user_role ur on u.id = ur.user_id
	inner join sys_role r on ur.role_id = r.id
    where u.id = #{user.id} and r.enabled = #{role.enabled}
</select>

这时,在XML中就不能直接使用#{userId}和#{enabled}了,而是要通过点取值方式使用#{user.userId}和#{role.enabled}从两个JavaBean中取出指定的属性值。

 

2.6. Mapper接口动态代理实现原理

package tk.mybatis.simple;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

public class MyMapperProxy<T> implements InvocationHandler {
    private Class<T> mapperInterface;
    private SqlSession sqlSession;

    public MyMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) {
        this.mapperInterface = mapperInterface;
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	//针对不同的 sql 类型,需要调用 sqlSession 不同的方法
    	//参数也有很多情况,这里只考虑一个参数的情况
        List<T> list = sqlSession.selectList(mapperInterface.getCanonicalName() + "." + method.getName());
        //返回值也有很多情况
        return list;
    }
}

这里代理方式和常规代理的不同之处在于,这里没有对某个具体类进行代理,而是通过代理转化成了对其他代码的调用。

同时提供了一种编程思路:可以通过动态代理这个桥梁将对接口方法的调用转换为对其他方法的调用

 

 

posted @ 2018-09-28 16:28  双子星世界  阅读(137)  评论(0编辑  收藏  举报