映射器是Mybatis最强大的工具,也是我们使用最多的,映射器包括接口和映射文件。可以这样说Mybatis是针对映射器构造的SQL构建的轻量级框架,它将SQL将硬编码的方式中抽取出来,通过配置的方式生成JavaBean返回给调用者。我们还可以定制动态SQL来满足不同的场景,所以它很灵活。它还支持自动映射:我们只需要让SQL返回的字段名和JavaBean的属性名一致即可(或者采用驼峰式命名,在Mybatis配置文件的settings中开启驼峰命名)即可完成自动映射。
Select元素
sselect元素帮助我们从数据库中读出数据并封装数据。执行select语句前,我们需要定义参数,参数可以是一个简单的参数类型,例如int,float,String等,也可以是一个复杂的参数类型,例如List,JavaBean,Map等,这些都是Mybatis接受的查收参数类型。
select元素的部分配置如下:
属性 | 说明 | 备注 |
id(必须) | 它和Mapper的命名空间组合起来一定要是唯一的,提供给Mybatis调用 | 如与命名空间组合起来不唯一,Mybatis将抛出异常 |
parameterType(不必须) | 可以给类的全路径名,也可以给类的别名,但使用的话一定要是Mybatis内定的或自定义的别名(Mybatis的typeHandler会根据参数推断类型) | 我们可以选择JavaBean,Map等复杂类型作为参数 |
resultType(必须) | 定义类的全路径名或别名可以完成自动映射;或定义为int,double,boolean等类型;不能与resultMap一块使用 | 常用参数之一 |
resultMap(必须) | 它是映射集的引用,将执行我们自定义的映射规则,不能与resultType同时使用 | 它是Mybatis最复杂的元素,可以配置自定义映射规则,级联,typyHandler等 |
databaseId(不必须) | 设置数据库厂商标识 | 这也是Mybatis提供多数据库的支持 |
自动映射
在默认情况下,Mybatis会提供自动映射的功能,只要返回的SQL的列名和JavaBean的属性名一致。在实际的情况,大部分的数据规范都是要求每个单词用下划线分隔,而Java则是使用驼峰命名法,于是可以给SQL语句中给列取别名的方式完成自动映射,或者是直接在配置文件中开启驼峰命名:
<settings> <!-- 开启驼峰命名规范 --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
自动映射的开关还是可以在settings元素中配置,autoMappingBehavior属性来设置其策略,它包括3个值:
- NONE:取消自动映射
- PARTIAL:只会自动映射,没有定义嵌套结果集映射的结果集(默认)
- FULL:会自动映射任意复杂的结果集(无论是否嵌套)
默认值是PARTIAL。所以在默认的情况下,它可以做到当前对象的映射,使用FULL是嵌套映射,在性能上下降。
传递多个参数
如果只是传递一个参数,我们在SQL中#{}到的值其实可以是任意的名称,因为只有一个值所以无所谓,这个参数值可以是基本类型,对象类型,集合类型的值。这种情况 MyBatis可直接使用这个参数,不需要经过任何处理。
但是更多的时候我们需要传递多个参数给映射器,接下来看看传递多个参数情况:
1.Map
我们可以将要传递的参数封装为Map,并设置parameterType="map"
<select id="findRoleByMap" parameterType="map" resultMap="roleMap"> select id,role_name roleName from t_role where role_name like concat('%','# {roleName}','%') and note like concat('%','#{note}','%') </select>
接口
public List<Role> findRoleByMap(Map<String,String> params);
调用这个接口封装个map作为参数
Map<String,String> paramsMap=new HashMap<String,String>(); paramsMap.put("roleName","me"); paramsMap.put("note","te"); roleMapper.findRoleByMap(paramsMap);
这种方式简单应用,但是有一个弊端:这样设置的参数使用Map,而Map需要键值对应,由于业务性不强,需要深入到程序中看代码,造成可读性下降。
2.使用@Param注解方式传递参数
我们可以给参数注解@Param(org.apache.ibatis.annotation.Param)来实现,我们将将上面的接口改为:
public List<Role> findRoleByMap(@Param("roleName") String roleName,@Param("note") String note);
再把映射XML改为无需定义参数的类型:
<select id="findRoleByMap" resultMap="roleMap"> select id,role_name roleName from t_role where role_name like concat('%','#{roleName}','%') and note like concat('%','#{note}','%') </select>
我们在调用的时候也不用去封装map了。可读性大大提高了,但是这会带来一个新的问题,就是参数个数比较多时,我们都用这个注解,参数列表将变得很长,可读性又下降了
3.使用JavaBean传递参数
在参数过多的情况下,Mybatis允许注意一个JavaBean,通过简单的setter和getter方法设置参数,这样就提高可读性了。
package cn.lynu.model; public class RoleParam{ private String roleName; private String note; setter and getter }
映射文件
<select id="findRoleByMap" parameterType="cn.lynu.model.RoleParam" resultMap="roleMap"> select id,role_name roleName from t_role where role_name like concat('%','#{roleName}','%') and note like concat('%','#{note}','%') </select>
这样再将我们的接口改为
public List<Role> findRoleByMap(RoleParam params);
思考
1.如果我们多个参数没封装Map,没使用@Param,没封装JavaBean如何使用?
只能通过#{param1}~#{paramN} 或#{0}~#{N} 来取值,不可以使用直接使用参数名,如果一定要使用参数名,可以在settings中设置,但是需要JDK1.8支持
2.接口如 public Employee getEmp(@Param("id")Integer id,String lastName); 应该怎样使用?
取值:id==>#{id/param1} lastName==>#{param2} 第一个id很好理解因为使用@Param注解了,而且使用#{param1}也可以,但它后面的第二个参数没有使用注解,所以只能使用#{param2}
3.接口如 public Employee getEmp(Integer id,@Param("e")Employee emp); 应该怎样使用?
取值:id==>#{param1} lastName===>#{param2.lastName/e.lastName} 第二个参数是一个JavaBean,如果这个参数使用@Param注解,可以直接使用#{注解的value值.属性值} 的方式取值;如果没有使用注解,则只能使用#{param2.属性值}
4.接口如 public Employee getEmpById(List<Integer> ids); 应该怎样使用?
取值:取出第一个id的值: #{list[0]} 如果参数是list或数组,MyBatis会做特殊处理,list类型的参数使用时只能用#{list[N]} , 数组必须使用#{array[N]}
5.使用Map的方式我们应该尽量少用,因为会导致业务可读性下降
6.如果参数个数<=5,推荐使用@Param注解,参数个数>=5个时,建议使用JavaBean
insert元素
insert元素相对select来说要简单多了,Mybatis会在执行插入之后返回一个整数,以表示你进行操作后插入的记录数。
insert部分属性:
属性 | 描述 | 备注 |
id | 它和mapper的命名空间组合起来是唯一的,作为唯一标识提供给Mybatis调用 | 如不唯一,将抛出异常 |
parameterType | 可以给全类名或别名 | 可以选择JavaBean,Map等类型作为参数类型 |
keyProperty | 表示JavaBean中哪个属性作为主键,不能与keyColumn同时使用 | Mybatis将使用getGeneratedKeys获得主键值封装进设置的属性中。如果是联合主键可以使用逗号分隔 |
keyColumn | 表示以哪个列作为主键,不能与keyProperty同时使用 | 联合主键使用逗号分隔 |
useGeneratedKeys | Mybatis使用JDBC的getGeneratedKeys获得有数据库内部生成的主键,例如:MySQL和SQL server自动增长字段,Oracle的序列等, 但是使用它就必须设置keyProperty或keyColumn | 取值为布尔值 true/false 默认值为false |
主键回填
如果是有主键自增的数据库,例如:MySQL或SQL server我们一般都是使用数据库内部的规则生成主键,在插入之后往往需要再获得这个主键值,以便其他操作。MyBatis提供了实现的方法:
首先使用keyProperty属性指定JavaBean中哪个属性对应表中的主键,同时使用userGeneratedKeys属性告诉这个主键是否是使用数据库内置策略生成。
<insert id="insertRole" parameterType="role" useGeneratedKeys="true" keyProperty="id"> insert into t_role(role_name,note) values(#{roleName},#{note}) </insert>
只需这样做,在插入数据的时候,Mybatis就会自动回填JavaBean的id值。
SqlSession sqlSession =SqlSessionFactoryUtil。openSQLSession(); RoleMapper roleMapper=sqlSession.getMapper(RoleMapper.class); Role role=new Role(); role.setRoleName("text"); role.setNote("testnote"); roleMapper.insertRole(role); System.out.print(role.getId()); sqlsession.commit();
但是如果是使用的Oracle(使用序列实现主键自增)或者对于主键的生成有一些特殊需求(例如:没有主键则设置主键为1,否则就取最大主键加2),如果有这种需求,就没那么简单了。我们还需要使用selectKey元素进行处理:
<insert id="insertRole" parameterType="role" 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 </selectLey> insert into t_role(id,role_name,note) values(#{id},#{roleName},#{note}); </insert>
order=“BEFORE” :这条sql会在插入sql之前执行 “AFTER” :这条sql会在插入sql之后执行 我们实现Oracle主键回填的时候会用到
Oracle主键回填
利用selectKey这个元素实现Oracle主键回填,会有order=“BEFORE”/“AFTER” 这两种方式
1. order=“BEFORE”方式
思路就是再插入语句之前,我们来从这张表的序列中查询出最大的主键值,取它的下一位赋值给JavaBean的主键属性,这样插入的时候id其实已经有值了
EMPLOYEES_SEO就是employees表的序列,使用序列名.nextval 就可以获得下一位数
2. order="AFTER"
思路就是在insert语句中嵌套查询序列的语句,然后在插入之后再查询序列得到主键值
序列名.currval 获得就是当前序列的主键值
注意:使用AFTER的方式有一些问题:如果插入一条数据,BEFORE和AFTER都没有问题;如果有多条插入语句,最后获得只是最后一条插入数据的主键,所以还是使用BEFORE方式吧
update和delete元素
update和delete元素执行后都会返回一个整数,标出执行后返回的记录数,我们唯一需要做的就是将对应的接口返回值设置为整型;insert,update和delete元素对应的接口返回值都可以设置为布尔类型,Mybatis都可以支持而无需任何配置。
参数配置
指定类型
有时候我们需要处理一些特殊的情况,我们指定特定的类型,以确实使用哪个typeHandler处理它们,以便我们进行特殊处理
#{age,javaType=int,jdbcType=NUMERIC}
但如我们可以直接指定typeHandler去处理数据(或使用我们自定义的typeHandler):
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
数字精度
如果我们使用的数字,我们还可以设置要保存数字的精度
#{price,javaType=double,jdbcType=NUMERIC,numericSscale=2}
null数据的保存
在大部分情况下,Mybatis都会推断数据的类型,所以大多数不需要我们去指定javaType,jdbcType或typeHandler。要我们设置的往往只是返回为空的字段类型而已,因为为null,Mybatis无法推断其类型
#{roleName}, #{note,jdbcType=VARCHER}
note用jdbcType=VARCHER明确告知Mybatis使用StringTypeHandler处理
如果不加设置,这个为null的属性保存在Oracle数据库中,会出现异常。因为Mybatis默认情况下处理null值使用的是OTHER类型,Oracle数据库不支持这中类型,所以需要设置为VARCHER或NULL,设置为NULL,Oracle也是支持的
其实这个Mybatis默认情况说的就是jdbcTypeForNull这个属性,这个属性默认就是OTHER,它可以取得值有NULL,OTHER,VARCHER。所以我们也可以直接在配置文件的settings中设置,而无需在设置参数时指定:
<settings> <setting name="jdbcTypeForNull" value="NULL"/> </settings>
#{}与${}的区别
在Mybatis中,我们使用#{}设置的参数Mybatis会用预编译的语句(就是preparedStatement JDBC中用?的方式),而有的时候我们需要传递SQL语句本身,而不是SQL所需的参数。例如。在一些动态表格(根据不同条件生成不同的动态列)中,我们传递的时列名;或者我们传递的是表名。这个时候就不能使用#{},因为设置设置参数的地方只能是where后面,而SQL其他的地方是不能使用?的,这些地方就要使用${}, 这样Mybatis就不会帮我们转义,会直接将参数拼到SQL上。
select ${columns} from t_tablename
所以#{}和${}的区别说就是Statement与PreparedStatement的区别是一样的。我们就知道了使用${}(Statement)的问题:这样的方式是不安全的,会有SQL注入的问题,Mybatis为我们提供灵活性的同时,也需要我们自己保证SQL的正确性和安全性。
返回值
我们知道Mybatis有自动封装的功能,当我们按照一定规则(列名和属性名一致时)就可以完成自动封装,这是返回值为JavaBean的情况。如果是返回一个List,我们又该如何设置呢?
返回List,我们不能使用resultType=“list” 这是错误的。我们应该使用的是集合的元素类型作为resultType值,例如:接口需要返回一个List<User>
/** * 结果返回集合 */ public List<User> findUserList();
我们的映射文件就需要将resultType设置为User
<select id="findUserList" resultType="cn.lynu.model.User"> select * from user </select>
需要牢记:resultType是告诉Mybatis将返回记录中的每一条数据封装成什么类型,而不是将所有的结果统一封装为什么类型
如果我们要求返回Map作为结果,又将如何封装呢?接口返回Map有两种情况:
1.将一条记录作为Map返回(列名为key 值为value) resultType=“map”
/** * 返回一个map(将一个User结果封装为一个map,key是列名) */ public Map<String, Object> findOneReturnMap(Integer id);
映射文件resultType设置为map即可,小map就是Map的别名了
<select id="findOneReturnMap" resultType="map" parameterType="int"> select * from user where user_id=#{id} </select>
2.将多条记录封装为一个Map(指定元素的某个属性作为key 每个元素作为value) resultType=“元素类型”
/** * 将所有结果封装为一个Map(key可以指定为User的任何属性,这里使用userId作为key) * MapKey指定key为哪个属性 */ @MapKey("userId") public Map<Integer, Object> finduserReturnMap();
这里是将每个User对象的userId作为key,而每个User对象作为value 。如需将某个属性作为key,只需要使用@MapKey指定属性名即可
映射文件
<select id="finduserReturnMap" resultType="cn.lynu.model.User"> select * from user </select>
映射文件的resultType值为User类,而不是Map,这是因为结果集中每条记录需要封装的是一个User,而不是一个Map。这一点需要好好领悟。
resultMap设置自定义结果
resultMap元素构成:
<resultMap type="" id=""> <constructor> <idArg/> <arg/> </constructor> <id/> <result/> <association property=""></association> <collection property=""></collection> <discriminator javaType=""> <case value=""></case> </discriminator> </resultMap>
之前我们一直使用的是resultType来封装结果,Mybatis允许我们自定义结果的封装规则,使用正是resultMap元素
resultMap元素的内部使用id和result元素来自定义封装规则。虽然该JavaBean中不指定封装规则的属性也可能会完成封装,但是建议都写上映射规则
resultMap的type属性指定需要完成自定义规则的JavaBean,可以说还是围绕着JavaBean来完成封装的,只不过我们可以完成一些特殊的需求,使用Mybatis提供的一些高级特性。可以认为resultMap是Mybatis中最复杂也最为重要的元素
id做为唯一标识供select元素的resultMap属性调用:
<!-- resultMap:自定义结果集映射规则; --> <!-- public Employee getEmpById(Integer id); --> <select id="getEmpById" resultMap="MySimpleEmp"> select * from tbl_employee where id=#{id} </select>
如同类一样,resultMap也是可以进行继承的,再加入自己的属性,使用的是extends属性:
<resultMap id="studentMap" type="cn.lynu.model.StudentBean"> ... </resultMap> <resultMap id="maleStudentMap" type="cn.lynu.model.MaleStudentBean" extends="studentMap"> ... </resultMap>
级联
在级联中存在三种对应关系。一对一,一对多和多对多。在实际中,多对多的关系应用不多,因为它比较复杂,会增加理解和级联的复杂度。推荐的方式是,将多对多关系分解为两个双向的一对多,以降低关系的复杂度,简化程序。
所以Mybatis中的级联有:association(代表一对一关系)和collection(代表一对多关系)
这里使用我们最为常见的员工(Employee)和部门(Department)作为例子,一个员工对应一个部门(一对一) 一个部门对应多个员工(一对多)
Employee:员工类中添加一个部门的引用来对应一对一关系
Department:部门中添加一个Employee类型的List集合来对应一对多关系
association
我们想要查询员工的同时将其部门信息带出来,这是一个一对一关系,所以需要使用association
一次查出:我们可以使用两张表的关联查询一次将所有信息查出,缺点就是要写一条很复杂的SQL
接口
public Employee getEmpAndDept(Integer id);
SQL
<select id="getEmpAndDept" resultMap="MyDifEmp"> SELECT e.id id,e.last_name last_name,e.gender gender,e.d_id d_id, d.id did,d.dept_name dept_name FROM tbl_employee e,tbl_dept d WHERE e.d_id=d.id AND e.id=#{id} </select>
就在我们自定义的结果集MyDirEmp中使用association封装结果
<resultMap type="cn.lynu.modelEmployee" id="MyDifEmp"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="gender" property="gender"/> <!-- association可以指定联合的javaBean对象 property="dept":指定哪个属性是联合的对象 javaType:指定这个属性对象的类型[不能省略] --> <association property="dept" javaType="cn.lynu.model.Department"> <id column="did" property="id"/> <result column="dept_name" property="departmentName"/> </association> </resultMap>
其实这种级联一个对象的情况,我们不使用association也是可以的,直接使用 result , Mybatis允许我们使用属性的属性这种关联关系
<resultMap type="cn.lynu.model.Employee" id="MyDifEmp"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="gender" property="gender"/> <result column="did" property="dept.id"/> <result column="dept_name" property="dept.departmentName"/> </resultMap>
分步查询:因为级联关系一般都涉及到两张表或多张表,所以我们可以分步进行查询。那么将原来的一条复杂SQL拆分为两条或多条简单的SQL,有什么好处呢?
首先就是简化SQL,关于复杂的SQL难以维护,性能也不是很好。再一个就是可以重用SQL,拆分成的简单SQL可能就是之前已经写好的SQL,我们直接重用即可。最后一点就是可以实现按需加载(懒加载),需要时再加载,不需要就不加载,这是符合现实需求的,也提高了性能。只有分步查询才可以玩懒加载。
接口
public Employee getEmpByIdStep(Integer id);
现在我们开始将这条复杂的SQL拆分为两条简单的SQL
<select id="getEmpByIdStep" resultMap="MyEmpByStep"> select * from tbl_employee where id=#{id} </select>
这是一条根据id查员工的sql,比较简单啦
<select id="getDeptById" resultType="cn.lynu.model.Department"> select id,dept_name departmentName from tbl_dept where id=#{id} </select>
根据id查询部门的SQL,也比较简单
名为 MyEmpByStep 的结果集
<resultMap type="con.lynu.model.Employee" id="MyEmpByStep"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="email" property="email"/> <result column="gender" property="gender"/> <!-- association定义关联对象的封装规则 select:表明当前属性是调用select指定的方法查出的结果 column:指定将哪一列的值传给这个方法 流程:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property指定的属性 --> <association property="dept" select="cn.lynu.dao.DepartmentMapper.getDeptById" column="d_id"> </association> </resultMap>
结果集MyEmpByStep中使用association,重点在于select属性,这个属性直接可以调用其他Mapper映射文件中SQL(namespace.元素id),column属性表明哪个字段作为参数传递给另一条SQL供调用。
在association中自己也不再需要封装规则了,因为 cn.lynu.dao.DepartmentMapper.getDeptById 这条SQL已经使用resultType封装结果了,所以我们不需要再封装。
这样就完成了分步查询,但是延迟加载还没有设置,我们之后再说
collection
如果我们查询部门需要将该部门下的所有员工得到,这就是一个一对多关系
一次查出:同样的可以使用一条复杂的SQL查询出所需信息
接口
public Department getDeptByIdStep(Integer id);
复杂的SQL
<select id="getDeptByIdPlus" resultMap="MyDept"> SELECT d.id did,d.dept_name dept_name, e.id eid,e.last_name last_name,e.email email,e.gender gender FROM tbl_dept d LEFT JOIN tbl_employee e ON d.id=e.d_id WHERE d.id=#{id} </select>
使用collection封装多的一方的数据
<resultMap type="cn.lynu.model.Department" id="MyDept"> <id column="did" property="id"/> <result column="dept_name" property="departmentName"/> <!-- collection定义关联集合类型的属性的封装规则 ofType:指定集合里面元素的类型[不能省略] --> <collection property="emps" ofType="cn.lynu.model.Employee"> <!-- 定义这个集合中元素的封装规则 --> <id column="eid" property="id"/> <result column="last_name" property="lastName"/> <result column="email" property="email"/> <result column="gender" property="gender"/> </collection> </resultMap>
我们需要使用collection中的ofType属性指定集合中元素的类型
分步查询:同样的我们也可以使用分步查询得到一对多的结果
接口
public List<Employee> getEmpsByDeptId(Integer deptId);
两条SQL语句
<select id="getEmpsByDeptId" resultType="cn.lynu.model.Employee"> select * from tbl_employee where d_id=#{deptId} </select>
这是条根据部门编号查询员工的SQL
<select id="getDeptByIdStep" resultMap="MyDeptStep"> select id,dept_name from tbl_dept where id=#{id} </select>
这是条根据id查询部门的SQL
名为 MyDeptStep 的自定义结果集
<resultMap type="cn.lynu.model.Department" id="MyDeptStep"> <id column="id" property="id"/> <id column="dept_name" property="departmentName"/> <collection property="emps" select="cn.lynu.dao.EmployeeMapperPlus.getEmpsByDeptId" column="id"></collection> </resultMap>
同样是select和column的属性起了关键作用 collection中的这两个属性与association是一样的
discriminator 鉴别器
鉴别器是在特定的条件下去使用不同的POJO,Mybatis中我们使用discriminator来处理需要鉴别的场景,它相当于Java中的switch语句。
我们在resultMap中使用鉴别器区分性别,男生和女生使用不同的结果集
<discriminator javaType="int" column="sex"> <case value="1" resultMap="maleStudentMap"/> <case value="0" resultMap="femaleStudentMap"/> </discriminator>
discriminator的column属性对应列名,我们就是根据sex这一列进行鉴别的,对应的javaType是int。 类似于switch ,我们使用case对不同的鉴别结果进行不同处理
级联性能分析和N+1问题
级联的优势就是能够方便的获取数据,但是使用多层级联时,建议超过三层关联时尽量少用级联,因为不仅用处不大而且会造成复杂度的增加,不利于维护和理解。同时级联也有一些问题,有时候我们并不需要所有的数据,会造成数据的浪费。而且会多执行几条SQL,造成性能下降,这就是N+1问题,为了解决这个问题应该考虑使用延迟加载。
延迟加载的意义在于一开始并不取出级联数据,只有当使用它了才发送SQL取数据。之前我们说过了只有分步查询才可以使用延迟加载,所以需要将association和collection改为分布查询的方式,这是第一步也是较为重要的关键的一步。
然后我们可以直接在配置文件的settings中设置lazyLoadingEnabled和aggressiveLazyLoading
lazyLoadingEnabled的含义是是否开启延迟加载功能。aggressiveLazyLoading的含义是对任意延迟加载属性调用都会完整加载;反之,每种属性将按需加载。
aggressiveLazyLoading还有一些有趣的特点:在默认情况下,Mybatis是按照层级延迟加载的,例如学生类的下面有课程成绩和学生证两个属性,这两个属性分别对应各自的类,这两个属性相当于是同一层级的,如果对这两个属性使用延迟加载,我们在调用课程成绩的同时,学生证信息也会被带出来,就因为它们是同一层级的。将aggressiveLazyLoading置为false可以解决这个问题,使Mybatis不在按照层级加载策略进行按需加载,而是使用什么属性才加载什么。
上面的是全局的设置,但是不太灵活,为什么呢?
因为我们不能指定哪些属性立即加载,哪些属性可以延迟加载。当一个功能的两个对象经常一起使用,我们采用立即加载更好,因为立即加载可以将多条SQL一次性发送,性能高。
Mybatis可以很容易的解决这种问题,因为它可以设置局部延迟加载功能。我们在association和collection元素加上属性值fetchType就可以了,它有两个取值eager和lazy。它们的默认值取决于配置文件settings的配置,如果我们没有配置默认使用的eager。一旦我们在局部配置了这个属性,那么全局的属性就会被局部所覆盖,这样我们可以灵活的进行指定。
<association property="" column="" select="" fetcherType="lazy"/> <collection property="" column="" select="" fetcherType="eager" />
无论是association还是controller,我们都可以使用两种方式进行级联(一次查出或分步查询),如果使用的是一次查出的方式是不能使用延迟加载的,而且SQL复杂,一次查出那么多数据,这会不会出现资源浪费?这都是问题,但是一次查出的方式没有N+1的问题,所以使用一次查出的方式还是分步查询的方式需要我们在实际工作中去考量。
缓存cache
缓存是经常使用到的优化持久层的技术,其特定是将数据保存在内存中。目前流行的缓存服务有MongoDB,Redis,Ehcache等。因为数据存在内存中,而无需在从硬盘读入,因此具备快速读取和使用的特点,如果缓存的命中率高,那么可以极大地提高系统的性能。如果缓存的命中率很低,那么缓存就没有使用的价值了,所以使用缓存的关键在于存储空间的命中率。
一级缓存和二级缓存
Mybatis对缓存的支持,在配有配置的默认情况下,它只开启一级缓存(一级缓存只是相对于同一个SqlSession而言)。在所有参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只执行一次SQL,因为使用SqlSession第一次查询之后,Mybatis会将其放在缓存中,以后再查询的时候,如果没有刷新,并且缓存灭有超时的情况下。SqlSession都只会读取当前缓存的数据,而不会再发SQL到数据库。
但是如果使用的是不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper,参数和方法,它还是会再次发送SQL到数据库去执行
虽然Mybatis默认是开启一级缓存的,但是还是建议在settings中显式声明:
<settings> <!-- 应用缓存,默认开启--> <setting name="cacheEnabled" value="true"/> </settings>
只要我们使用同一个Sqlsession既可体验到一级缓存:
/** * 一级缓存 */ @Test public void testOneLeven() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user1 = userMapper.findById(3); System.out.println(user1); User user2 = userMapper.findById(3); System.out.println(user2); System.out.println(user1==user2); }
上面的例子只会在第一次查询的时候发送SQL,第二次就不再查数据库了(只要这两次查询中间没有增删改操作,也没有手动刷新缓存),直接从缓存中拿
一级缓存在各个SqlSession是相互隔离的,为了解决资格问题,我们往往需要配置二级缓存,使得缓存在SqlSessionFactory层面(或是namespace层面)上能够为各个SqlSession对象所共享。开启二级缓存之后,程序每一次查询会先在二级缓存中查找,再到一级缓存中查找,都没有找到发SQL查数据库。但是需要注意的是:只有当我们调用SqlSession的commit方法关闭后,才会将数据放入到二级缓存中。
二级缓存开启的方法:
首先将POJO类实现序列化(Serializable)接口
然后直接在Mapper映射XML文件中使用:
<cache/>
很简单就开启了Mybatis的二级缓存,只有配置了这个元素的映射XML文件才会开启二级缓存,没有配置的就不会使用二级缓存。当然这是一个默认的二级缓存配置,它意味着:
- 映射文件中的所有select语句都会被缓存
- 映射文件中的insert,update,delete元素会刷新缓存
- 缓存会使用默认使用LRU(最近最少使用)算法收回
- 二级缓存中默认存储的集合或对象(无论查询方法返回什么)的1024个引用
- 缓存是read/write(可读/可写)的缓存
/** * 二级缓存(namespace级别的缓存,跨sqlSession) */ @Test public void testSecondLeven() { SqlSession session1 = SQLSessionFactoryUtil.openSqlSession(); UserMapper userMapper = session1.getMapper(UserMapper.class); User user1 = userMapper.findById(3); System.out.println(user1); session1.close(); SqlSession session2 = SQLSessionFactoryUtil.openSqlSession(); UserMapper userMapper2 = session2.getMapper(UserMapper.class); User user2 = userMapper2.findById(3); System.out.println(user2); System.out.println(user1==user2); }
注意:只有SqlSession调用commit或close之后,才会将数据放入二级缓存中,不然还是存在于一级缓存中
cache元素的设置
我们可以修改cache元素的默认配置
<cache eviction="LRU" flushInterval="100000" size="1024" readOnly="false" type="自定义缓存实现类全路径"/>
- eviction:代表缓存回收策略,目前Mybatis提供一下策略:
- LRU:最近最少使用(默认)
- FIFO:先进先出
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,移除基于垃圾回收器状态和弱引用规则的对象
- flushInterval:刷新时间间隔,单位是毫秒。如果不配置它,那么当SQL被执行的时候才会去刷新缓存
- size:引用数目,一个正整数,代表缓存中可以存放的多少个对象。不宜设置过大,设置过大会导致内存溢出。
- readOnly:是否只读,设置为true意味着只能读取而不能修改,这样设置可以快速的读取缓存中的数据,但是我们没法修改缓存。默认是false
自定义缓存
我们可以自定义缓存产品,只是需要实现oeg.apache.ibatis.cache.Cache接口,然后我们将这个实现类的全路径作为cache元素的type属性值即可。
例如我们使用Ehcache作为缓存实现产品。其实Mybatis已经为我们将常用缓存产品的这个实现类写好了,我们只需要在github上Mybatis的顶级项目中寻找,或直接访问https://github.com/mybatis/ehcache-cache 这个网址即可。
我们首先在项目中导入这个jar
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.1.0</version> </dependency>
然后将ehcache.xml配置文件放入我们的项目中
最后配置cache元素的type值:
<!--使用ehcache --> <cache type="org.mybatis.caches.ehcache.EhcacheCache" ></cache>
这样我们就使用Ehcache作为二级缓存的实现产品,而不是使用Mybatis默认的二级缓存(默认Mybatis的二级缓存就是一个Map,太过简单)
缓存局部设置
在上面我们配置的缓存(无论一级还是二级缓存)都是使用全局的设置,当然我们也可以使用SQL层面(局部)的缓存规则,来决定是否使用缓存或者刷新缓存。我们往往使用这两个属性:userCache和flushCache来完成。其中useCache表示是否使用二级缓存,而flushCache表示执行SQL后是否刷新缓存(一,二级缓存都会刷新)。
<select ... flushCache="false" usecache="true"/> <insert ... flushCache="true" /> <update... flushCache="true" /> <delete... flushCache="true" />
我们之前说再执行insert,update,delete之后一,二级缓存都会被清空,这还是为什么呢?
正是因为这三个元素的flushCache属性默认就是true,而select元素的flushCache值默认是false。