传统JDBC的不足
有些项目的sql语句会写在程序当中,修改语句时需要修改代码,违反了OCP原则;
使用PrepareStatement预编译sql语句时,需要逐个对参数赋值,过程比较繁琐;
将结果集封装成对象时,需要逐个对成员变量赋值,过程繁琐。
MyBatis的特点
MyBatis的底层是对JDBC的封装,通常被用于持久层的半自动化ORM框架。
将接口和Java的POJOs映射成数据库中的记录;
支持定制化sql,存储过程,基本映射和高级映射;
避免了JDBC中绝大多数需要手动设置参数以及对结果集进行手动封装;
支持XML开发和注解开发;
体积小,只需要2个jar包和2个xml配置文件;
提供丰富的映射标签,支持编写动态sql。
启动第一个MyBatis程序
在pom.xml文件中引入MyBatis和相对版本的Mysql驱动的依赖;
创建MyBatis的配置文件,文件名和存储位置可以自定义;
创建Mapper的配置文件,文件名和存储位置可以自定义;
通过字节流读取MyBatis的配置文件,创建SqlSessionFactoryBuilder;
通过SqlSessionFactoryBuilder获取SqlSessionFactory,再通过SqlSesssionFactorty获取SqlSession;
最终通过SqlSession执行定义在Mapper配置文件中的sql语句。
需要注意的地方是JDBC默认事务是不提交的,需要手动提交。
在MyBatis程序中使用logback日志框架
在配置文件中配置启用logback日志;
创建logback配置文件。
在MyBatis程序中使用#{}占位符
在Java程序中,将数据放到Map集合中;在sql语句中使用#{Map.key}
完成传值。
#{Map.key}
等同于JDBC中的?
,即#{Map.key}
就是占位符。
还可以使用POJO完成传参。sql语句中使用#{POJO.成员变量名}
。
如果采用Map集合传参的方式,#{}
中写的是Map集合的key,如果这个key不存在,程序不会报错,数据库中会插入NULL;
如果采用POJO传参的方式,#{}
中写的是POJO对象中getXxx()
方法名去掉get
之后剩下的单词首字母小写。所以当对应的getXxx()
方法不存在时,程序就会报错。
编写Mapper配置文件时,sql标签中有个parameterType
属性,显式配置这个属性,可以指定传参的数据类型(Map或者POJO)。这个属性是可以省略的。
当占位符只有一个时,#{}
的内容可以是任意字符。
在MyBatis程序中查询数据
对于select
语句,需要指定这个sql的结果类型,否则会报错。
当查询结果的字段名和POJO对象的属性名对应不上时,那么对应不上的字段就无法正确地赋值给结果对象对应的属性。
解决这个问题的办法之一是使用as
关键字指定别名。
Mapper配置文件中的namespace
namespace属性可以防止sqlid冲突。
Mybatis配置文件的相关标签
configuration:根标签
environments:可以为MyBatis配置多个环境
environment:具体的环境配置,包括数据源配置和事务管理器的设置
transactionManager:事务管理器。可以指定JDBC
或者MANAGED
。JDBC
使用原生的事务管理机制,底层原理是先通过conn.setAutoCommit(false);
开启事务,之后进行业务处理,最终通过conn.commit();
提交事务。
dataSource:指定数据源。通过type
属性指定连接池策略。
mappers:配置多个sql映射文件,也就是Mapper配置文件
mapper:配置映射文件的路径。使用resource
属性指定相对于类路径的配置文件的路径;使用url
属性则需要完全限定资源定位符(URL)。
通过实现简易的MyBatis框架理解底层逻辑
根据使用MyBatis框架的流程,设计出以下几个部分。
1.SqlSessionFactoryBuilder
通过dom4j解析配置文件中的配置,包括数据源,事务管理器,和所有sql映射对象(xxxMapper接口配置文件)的相关配置。
通过这个构造器创建SqlSessionFactory对象。
2.SqlSessionFactory
通过使用这个SqlSession工厂对象获取Session对象。
工厂对象要包含事务管理器和sql映射对象的集合,在获取Session对象后要把这些数据都交给Session处理。
3.SqlSession
SqlSession对象是具体执行sql语句的对象。在这个类中有执行各种sql语句的具体实现,通过形参中的sqlId和参数对象实现对应的方法。
通过工厂类传递过来的事务管理器获取Connection对象和控制事务。
在执行DML语句的具体实现方法中,需要把定义在配置文件中的sql语句中的#{}
占位符通过正则表达式替换成?
,并遍历出所有在{}
里的成员变量的名称,通过反射调用目标对象的getXxx()
方法后,再将得到的参数正确赋给PrepareStatement对象。最终就可以完成sql的执行。
在执行DQL语句的具体实现方法中,同样需要把定义在配置文件中的sql语句中的#{}
占位符通过正则表达式替换成?
,遍历出所有在{}
里的成员变量的名称,通过反射调用目标对象的setXxx()
方法后,将sql执行后的结果集中的数据赋给返回值对象的成员变量。最终将返回值对象返回。
4.MappedStatement
sql映射对象实体类,封装了有关sql的所有信息。包括sqlId,返回值类型,参数类型,sql语句,sql语句类型。
通过参数类型可以判断使用Map集合传值或者POJO传值。
通过返回值类型可以反射出需要返回的对象。
所有的sql映射对象实体类在SqlSessionFactoryBuilder中就已经被解析出来,然后被统一放到一个Map集合中,Map的key就是namespace拼接sqlId,value就是MappedStatement对象。
5.实现了javax.sql.DataSource的DataSource类
用来在TransactionManager中获取Connection对象。
这个类也在SqlSessionFactoryBuilder中就已经被解析出所有数据并创建出来,并通过TransactionManager的构造方法传递到事务管理器当中。
6.TransactionManager
提供一个接口,指定不同事务处理的方法,提高拓展性。通过配置文件中指定的事务管理器类型创建不同的实现类。在创建类时,需要显示指定是否自动提交。
在MyBatis框架中,如果使用JDBC管理事务,那么是不会自动提交的。
通过以上6个类,就能实现简易的类似MyBatis框架的核心功能。由此可见,MyBatis框架的设计是相当精巧而强大的。
MyBatis核心对象的作用域
SqlSessionFactoryBuilder
这个类的实例的最佳作用域是方法作用域。当创建好SqlSessionFactory后就不需要它了。所以保证所有的XML解析资源释放给其他更重要的事情
SqlSessionFactory
这个类的实例被创建后应该在程序运行期间一直存在。最佳实践是在应用运行期间不要重复创建,最佳作用域就是应用作用域。
SqlSession
SqlSession实例不是线程安全的,所以每个线程都要有自己的SqlSession实例。最佳作用域是请求或者方法作用域。
SqlSession的事务问题
当SqlSession出现线程安全问题时,此时的事务管理是失效的。需要控制不同业务的SqlSession统一使用一个SqlSession对象。方法是使用ThreadLocal。
通过使用Javassist技术实现MyBatis中动态生成DaoImpl类代码理解底层原理
在代码中指定了Mapper配置文件的namespace属性和Dao接口的全限定名称一致,id属性和Dao接口的方法名称一致。
MyBatis中的几处细节
#{}
和${}
的区别
#{}
:先编译sql语句,再给占位符传值。底层实现是PrepareStatement。可以防止sql注入。比较常用。
${}
:先进行sql语句拼接,然后再编译。底层实现是Statement。只有在需要进行sql语句关键字拼接的情况下才会用到。
在配置文件中使用typeAlias配置对象类型的别名
//使用<typeAlias>
//alias属性不是必须的,且不区分大小写
<typeAliases>
<typeAlias type="com.example.exampleClass" alise="eClass" />
</typeAliases>
//使用<package>
//可以使用多个<package>标签
<typeAliases>
<package name="com.example.pojo" />
</typeAliases>
引入Mapper配置文件的几种方式
// 使用resource属性
<mappers>
<mapper resource="org/example/mapper/xxxMapper.xml" />
</mappers>
// 使用url属性
<mappers>
<mapper url="fiIe:///org/example/mapper/xxxMapper.xml" />
</mappers>
// 使用class属性
<mappers>
<mapper class="org.example.mapper.xxxMapper.xml" />
</mappers>
// 使用<package>标签
<mappers>
<package name="org.example.mapper" />
</mappers>
实现自增主键
MyBatis提供了insertUseGeneratedKeys()
方法
MyBatis对参数的处理
单个简单类型参数
<select id="selectByName" resultType="student" parameterType="java.Iang.String">
select * from t_student
where name = #{name, javaType=String, jdbcType=VARCHAR }
</select>
MyBatis具有强大的自动类型判断,所以不需要特别指定。
resultType
,javaType
,parameterType
都是可以省略的。
使用Map集合传递参数
// 准备的Map集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("name", "John");
paramMap.put("age", 12);
// 使用占位符接收参数
<select id="selectByNameAndAge" resultType="student">
select * from t_student
where name = #{name} and #{age}
</select>
通过#{Map.key}
获取参数
使用实体类参数
// 准备实体类参数
Student student = new Student();
student.setName("John");
student.setAge(12);
// sql映射配置
<insert id="insertStudent">
insert into t_student values(#{name}, #{age})
</insert>
// Mapper接口方法
int insert(Student student)
#{}
里写的是属性名称。本质是set()
/get()
方法去掉set
/get
后的名称。
多个参数的情况
// 调用接口
studentMapper.selectByNameAndAge("John", 12);
// sql映射文件
<select id="selectByNameAndAge" resultType="student">
select * from t_student
where name = #{arg0} and #{arg1}
</select>
使用arg0
,arg1
表示第一个,第二个参数,以此类推。
也可以使用param0
,param1
表示。
在MyBatis3.4.2之前的版本中,要使用#{0}
,#{1}
这种形式。
底层原理是创建一个Map集合用来保存参数,使用arg0
/param0
作为key,传递的参数作为value。
使用注解@Param
的情况
// Mapper接口中定义的方法
List<Student> selectByNameAndAge(@Param(value="name") String name, @Param("age") int age);
// sql映射文件
<select id="selectByNameAndAge" resultTyle="student">
select * from t_student
where name = #{name} and age = #{age}
</select>
使用注解可以增强可读性。
底层实现原理是,MyBatis会将Mapper接口调用的方法的参数放到一个参数数组中,将注解中的参数名按顺序保存在一个SortMap中。然后按顺序取出参数名作为key,参数作为value保存在一个HashMap中。同时还会将param
作为默认前缀拼接从0
开始的数字作为key,再次保存一遍参数。
Select语句的返回结果与结果映射
<select id="selectAll" resultType="student" />
当查询的结果有对应的实体类,并且查询结果只有一条时,可以使用一个对象实例接收,也可以使用一个List集合接收;
当查询到多条记录时,只能用集合接收。
<select id="selectAll" resultType="map" />
当返回的数据没有对应的实体类时,可以采用Map集合接收。此时字段名作为key,字段的值作为value;
当查询结果条数大于等于1时,可以返回一个存储Map集合的List集合,但是查询结果条数大于1时,必须使用List集合接收数据。
// Mapper接口
@MapKey("id)
Map<Long, Map<String, Object>> selectAllRetMap();
当使用Map集合接收数据时,可以使用Map<id,Map>
这样的嵌套Map集合。此时要使用@MapKey
注解在需要指定为key的字段上。
// 使用resultMap进行结果映射
<resultMap id="carResultMap" type="car">
<id property="id" colum="id" />
<result property="carNum" colum="car_num" />
<result property="brand" colum="brand" javaType="string" jdbcType="VARCHAR" />
</resultMap>
// sql
<select id="selectAllWithResultMap" resultMap="carResultMap">
select * from t_cat
</select>
使用结果映射可以将数据库中的字段名和POJO类的属性名一一对应。使用这个方法时,假如字段名和属性名是可以对应时,可以省略,javaType属性和jdbcType属性也可以省略。
// MyBatis配置文件
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
在MyBatis配置文件中开启驼峰命名自动匹配也可以将数据库中的字段名和POJO类的属性名一一对应。前提是属性名遵循Java的命名规范,数据库表的列名遵循sql的命名规范。
// Mapper配置文件
<select id="selectTotal" resultType"long">
select count(*) from t_car
</select>
使用long
关键字查询总记录的条数
动态标签
if标签
<mapper namespace="com.example.mapper.CarMapper">
<select id="selectByMultiCondition" resultType="car">
select * from t_car
where 1=1
<if test="brand != null and brand != ' '">
and brand like #{brand}"%"
</if>
<if test="carType != null and carType != ' '">
and carType = #{carType}
</if>
</select>
</mapper>
使用if
标签可以控制sql中的条件是否拼接。当条件不成立时不会拼接到sql中。使用恒成立的等式可以防止第一个条件不成立导致的语法问题。
where标签
<mapper namespace="com.example.mapper.CarMapper">
<select id="selectByMultiCondition" resultType="car">
select * from t_car
<where>
<if test="brand != null and brand != ' '">
and brand like #{brand}"%"
</if>
<if test="carType != null and carType != ' '">
and carType = #{carType}
</if>
</select>
</mapper>
使用where
标签可以保证当所有的条件都不成立时不会生成where子句,同时自动清除条件前面不合理的and
和or
。注意,条件后面的and
和or
是不会被清除的。
trim标签
<mapper namespace="com.example.mapper.CarMapper">
<select id="selectByMultiCondition" resultType="car">
select * from t_car
<trim prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ' '">
brand like #{brand}"%" and
</if>
<if test="carType != null and carType != ' '">
carType = #{carType}
</if>
</trim>
</select>
</mapper>
使用trim
标签可以更灵活的动态拼接sql语句,又4个属性。
prefix
:在trim
标签中的语句前添加内容
suffix
:在trim
标签中的语句后添加内容
prefixOverrides
:需要覆盖掉的前缀中的内容
suffixOverrides
:需要覆盖掉的后缀中的内容
需要注意的是只有当trim
标签存在sql语句时,以上约束才会生效。
set标签
<mapper namespace="com.example.mapper.CarMapper">
<update id="updateWithSet">
update t_car
<set>
<if test="carNum!=null and carNum!=' '">
car_num = #{carNum},
</if>
<if test="brand!=null and brand!=' '">
brand = #{brand}
</if>
</set>
where id = #{id}
</update>
</mapper>
用在update语句中,生成set
关键字,同时去掉字段末多余的,
。
choose when otherwise标签
<mapper namespace="com.example.mapper.CarMapper">
<select id="selectWithChoose" resultType="car">
<where>
<choose>
<when test="brand!=null and brand!=' '">
brand like #{brand}"%"
</when>
<otherwise>
produce_time >= #{produceTime}
</otherwise>
</when>
</select>
</mapper>
这三个标签在一起使用,只有一个分支被选择。
foreach标签
<mapper namespace="com.example.mapper.CarMapper">
<insert id="insertBatchByForeach">
insert into t_car values
<foreach collection="cars" item="car" separator=",">
(null, #{car.carNum}, #{car.brand})
</foreach>
</insert>
<delect id="delectBatchByForeach">
delect from t_car where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delect>
</mapper>
遍历传来的参数,使用separator
分隔,使用open
和close
包围。
sql标签
<mapper namespace="com.example.mapper.CarMapper">
<sql id="carCols">id, car_num, brand, car_type</sql>
<select id="selectAll" result="car">
select <include refid="carCols" /> from t_car
</select>
<select id="selectCarById" result="map">
select <include refid="carCols" /> from t_car where id=#{id}
</select>
</mapper>
使用sql
标签声明sql片段;
使用include
标签将声明过的片段包含到某个sql语句中。
高级映射以及延迟加载
多对一的实现方式
<mapper namespace="com.example.mapper.StudentMapper">
<resultMap id="studentResultMap" type="Student">
<id property="sid" colum="sid" />
<result property="sname" colum="sname" />
<result property="clazz.cid" colum="cid" />
<result property="clazz.cname colum="cname" />
</resultMap>
<select id="selectById" resultMap="studentResultMap">
select s.*, c.* from t_student s join t_clazz c
on s.cid = c.cid where s.sid = #{sid}
</select>
</mapper>
使用result
标签将所有字段进行级联属性映射。
<mapper namespace="com.example.mapper.StudentMapper">
<resultMap id="studentResultMap" type="Student">
<id property="sid" colum="id" />
<result property="sname" colum="sname" />
<association property="clazz" javaType="clazz>
<id property="cid" colum="cid" />
<result property="cname" colum="cname" />
</association>
</resultMap>
<select id="selectById" resultMap="studentResultMap">
select s.*, c.* from t_student s join t_clazz c
on s.cid = c.cid where s.sid = #{sid}
</select>
</mapper>
使用association
标签将两个对象进行关联。可读性更好,代码复用性更强。
// Mybatis-config.xml
<settings>
<setting name="lazyloadingEnabled" value="true" />
</settings>
// StudentMapper.xml
<mapper namespace="com.example.mapper.StudentMapper">
<resultMap id="studentResultMap" type="Student">
<id property="sid" colum="sid" />
<result property="sname" colum="sname" />
<association property="clazz"
select="com.example.mapper.ClazzMapper.selectById"
colum="cid"
fetchType="lazy"
</resultMap>
<select id="selectById" resultMap="studentResultMap">
select s.*, c.* from t_student s join t_clazz c
on s.cid = c.cid where s.sid = #{sid}
</select>
</mapper>
// ClazzMapper.xml
<mapper namespace="com.example.mapper.ClazzMapper">
<select id="selectById" resultType="Clazz">
select * from t_clazz where cid = #{cid}
</select>
</mapper>
在association
标签中将另一个sql的sqlID
写在select
属性的位置,就可以实现分步查询。分步查询的优点是可以支持延迟加载,需要在MyBatis配置文件中使用lazyLoadingEnabled
启用全局延迟加载。或者在sql映射文件中使用fetchType
启用局部延迟加载。
一对多的实现方式
<mapper namespace="com.example.mapper.ClazzMapper">
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" colum="cid" />
<result property="cname" colum="cname" />
<collection property="students" ofType="Student">
<id property="sid" colum="sid"/>
<result property="sname" colum="sname" />
</collection>
</resultMap>
<select id="selectClazzAndStudentsByCid" resultMap="clazzResultMap">
select * from t_clazz c join t_student s
on c.cid = s.cid where c.cid = #{cid}
</select>
</mapper>
使用collection
标签实现一对多映射。
// MyBatis-config.xml
<settings>
<setting name="lazyLoadingEnabled" value="true">
</settings>
// ClazzMapper.xml
<mapper namespace="com.example.mapper.ClazzMapper">
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" colum="cid" />
<result property="cname" colum="cname" />
<collection property="students" /
select="com.example.mapper.StudentMapper.selectByCId"
column="cid"
fetch="lazy" />
</resultMap>
<select id="selectClazzAndStudentsByCid" resultMap="clazzResultMap">
select * from t_clazz c join t_student s
on c.cid = s.cid where c.cid = #{cid}
</select>
</mapper>
// StudentMapper.xml
<mapper namespace="com.example.mapper.StudentMapper">
<select id="selectByCid" result="Student">
select * from t_student where cid = #{cid}
</select>
</mapper>
在collection
标签中将另一个sql的sqlId
写在select
属性的位置,实现分步查询。一对多的延迟加载和多对一的开启方式相同。
缓存
一级缓存
MyBatis的一级缓存默认开启,只要用一个SqlSession
对象执行同一个sql语句就可以使用缓存。
使用clearCache()
方法可以手动清空缓存,此时缓存会失效;
两次查询之间出现增删改操作,一级缓存和二级缓存都会失效。
二级缓存
使用二级缓存需要具备以下条件:
1.在配置文件中配置<setting name="cacheEnabled" value="true" />
开启缓存,默认是开启。
2.在需要使用二级缓存的sql映射文件中配置<cache />
3.使用二级缓存的实体类必须实现java.io.Serializable
接口
4.SqlSession
对象关闭或者提交后,一级缓存中的数据才会被写入二级缓存中。此时二级缓存才会生效。
二级缓存的淘汰算法
可以使用eviction
属性指定淘汰算法,默认是LRU
策略。
LRU
:Least Recently Used,最近最少使用策略
FIFO
:First In First Out,先进先出策略
SOFT
:淘汰软引用指向的对象
WEAK
:淘汰弱引用指向的对象
分页插件
PageHelper.startPage(startPage, pageSize);
使用MyBatis框架的PageHelper
插件能快速使用分页功能
使用注解开发
只推荐使用@Select
,@Delete
,@Update
,@Insert
注解开发简单的sql语句,否则只会使让复杂的sql语句更加混乱不堪。给出一个例子。
//StudentMapper接口
@Update("<script> update t_student set grade=’三年级‘ " +
" <if test=\"name != null\">, name = #{name} </if> " +
" <if test=\"sex!= null\">, name = #{sex} </if> " +
" where num = #{num} </script> ")
void update(Student student);