文章目录
1. 简介
ROM框架发展:JDBC -> DbUtils(QueryRunner) -> JdbcTemplate -> Hibernate -> MyBatis
前三个只能称为简单的工具,sql语句都写在java源代码中,硬编码高耦合。
Hibernate
: 全自动全映射ORM(Object Relation Mapping)框架,全部都封装好了,亦在消除sql
缺点:sql语句不能定制优化,由框架自动编写(学习HQL可以自己编写,但学习成本高)。查询一个字段把所有字段都映射了,性能不好。
MyBatis
:半自动轻量级框架,把sql编写,放到配置文件中,其他操作框架封装好,sql与java编码分离。sql语句交给程序员编写,不失去灵活性。
2. 第一个程序
-
创建数据库对应的Bean实体类
Emp.java
@Data public class Emp implements Serializable { private Integer id; private String lastName; // 注意这个和数据库字段名不一样 private String gender; // 也可用char private String email; }
-
创建全局MyBatis配置文件,数据源环境信息,事务管理器信息…(类路径下)
mybatis-config.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"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="9527"/> </dataSource> </environment> </environments> <!-- 将写好的sql映射文件一定要注册到全局配置文件中!!!--> <mappers> <mapper resource="empMapper.xml"/> <!-- 类路径下可以直接写--> </mappers> </configuration>
-
创建sql映射xml文件 ,配置每一个sql和结果的封装规则(类路径下)
empMapper.xml
<mapper namespace="com.sutong.dao.EmpMapping"> <!-- namespace命名空间, id是sql的唯一标识,resultType返回值类型, 中间是sql语句,数据库字段名和Bean不一样需要取别名, #{id}从传递过来的参数中取出id值 --> <select id="selectEmp" resultType="com.sutong.bean.Emp"> select `id`, `last_name` as lastName, `gender`, `email` from t_emp where id = #{id} </select> </mapper>
-
获取执行sql的对象,去执行sql语句( SqlSession实例 - 这一个对象就代表和数据库的一次会话,用完关闭,和Connection一样都是线程不安全的,每次使用都应该去获取新的对象)
public class HelloMyBatis01 { @Test public void test01() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); try { // 第一个参数是sql语句的唯一标识符!!! // (就是select标签里面的id,但多个文件id可能冲突,所以需要加上命名空间),第二个的执行sql需要的参数 Emp emp = sqlSession.selectOne("com.sutong.dao.EmpMapping.selectEmp", 1); System.out.println(emp); } finally { sqlSession.close(); // 始终都要关闭 } } }
-
⭐接口式编程,我们Dao层写的接口可以直接和MyBatis映射文件绑定!MyBatis自动为接口创建代理对象,去执行增删改查
namespace
必须指定为接口的全类名select
标签的id
属性是要绑定接口方法的名
public interface EmpMapping { Emp getEmpById(Integer id); }
<mapper namespace="com.sutong.dao.EmpMapping"> <!-- namespace命名空间,接口的全类名!!!select标签的id需是接口方法的名称!!绑定 --> <select id="getEmpById" resultType="com.sutong.bean.Emp"> select `id`, `last_name` as lastName, `gender`, `email` from t_emp where id = #{id} </select> </mapper>
测试:
//先获取SqlSession实例 try { EmpMapping mapper = sqlSession.getMapper(EmpMapping.class); // 获取接口实现类, Emp emp = mapper.getEmpById(1); // 实际调用接口代理的方法,就不用写上面一大堆sql标识了,还有类型检查! System.out.println(emp); } finally { sqlSession.close(); }
3. 全局配置文件
-
configuration(配置)
-
properties(属性)
-
settings(设置)
-
typeAliases(类型别名)
-
typeHandlers(类型处理器)
-
objectFactory(对象工厂)
-
plugins(插件)
-
environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
-
databaseIdProvider(数据库厂商标识)
-
mappers(映射器)
-
注意:配置的时候要按照上面这个顺序配置!!
3.1 引入DTD约束
MyBatis的xml配置文件约束(语法),(IDEA自带)
<!-- 全局配置文件--> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!-- sql映射文件--> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3.2 properties标签
类路径下创建 dbconfig.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=9527
使用:
<!-- 因为外部properties配置文件内容,resource属性引入类路径下资源,url引入网络下或磁盘下-->
<properties resource="dbconfig.properties"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
3.3 settings标签 ☢
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。里面很多设置。!!
mapUnderscoreToCamelCase
:是否开启驼峰命名自动映射,即从经典数据库列名A_COLUMN
映射到经典Java
属性名aColumn
。这样就可以不用查询的时候在sql语句中用as
取别名了。默认值是false
jdbcTypeForNull
当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。默认是OTHER
lazyLoadingEnabled
延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType
属性来覆盖该项的开关状态。默认是fasle
aggressiveLazyLoading
开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。默认false
(3.4.1前默认是true)cacheEnabled
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认truelocalCacheScope
MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为SESSION
,会缓存一个会话中执行的所有查询。 若设置值为STATEMENT
,本地缓存将仅用于执行语句,对相同SqlSession
的不同查询将不会进行缓存。- …等多设置看MyBatis官网 ,官网:配置
原则:即使的默认的,我们确认开启的设置都要显示的配置出来!
<settings>
<!-- name设置项,value设置项取值-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
3.4 typeAliases标签 ☢
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
<!-- 别名处理器,给java类名取别名(别名不区分大小写)!!-->
<typeAliases>
<!-- type是要取别名的全类名,默认名就是类名小写!!也可以alias属性指定别名-->
<typeAlias type="com.sutong.bean.Emp"/>
<!-- 批量区别名,指定包名- 当前包和后代包都起个默认别名-->
<package name="com.bean"/>
</typeAliases>
使用:
<select id="getEmpById" resultType="emp">
select * from t_emp where id = #{id}
</select>
批量区别名问题:当多包下有相同的类时,都取默认别名,则MyBatis就会报错,这时可以在重名类上面使用
@Alias
注解,给个别类单独起个别名@Alias("emp01")
,就不冲突了。
MyBatis为我们起好了一些别名:
- 别名:
_int
<–> 映射的类型:int
(基本类型就是前面加下划线) integer
<–>Integer
(基本类型的包装类都是首字母小写)string
<–>String
3.5 typeHandlers标签
类型处理器:在设置预处理语句(PreparedStatement
)中的参数或从结果集中取出一个值时,都用类型处理器将获取到的值以合适的方式转换成 Java 类型。
Mybatis 3.4.5后添加Java8的新日期API支持
…
3.6 plugins标签 ☢
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
后面讲!!
3.7 environments标签
将 SQL 映射应用于多种数据库之中。记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
<!-- 环境配置,default指定使用那个环境!!-->
<environments default="development">
<!-- 每一个environment表示一个具体的环境信息,里面必须有transactionManager,dataSource标签-->
<environment id="development">
<!-- 事务管理器,type两种类型JDBC|MANAGED(两个别名),
可以自定义,实现TransactionFactory接口,type就是自定义事务管理器的全类名-->
<transactionManager type="JDBC"/>
<!-- 数据源,type三种UNPOOLED|POOLED|JNDI(也是别名),POOLED是使用连接池,MyBatis自带的
可以自定义实现DataSourceFactory接口,type就是自定义数据源的全类名-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
<environment id="test">
<transactionManager type="..."/>
<dataSource type="..."></dataSource>
</environment>
</environments>
3.8 databaseIdProvider标签
可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId
属性。
<!-- 支持多数据库厂商,type是DB_VENDOR固定的,是个别名
作用得到数据库厂商的标识(驱动里有方法能获取),mybatis根据标识来执行不同的sql-->
<databaseIdProvider type="DB_VENDOR">
<!-- 为不同的数据库厂商起别名-->
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle" />
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
</databaseIdProvider>
在sql映射文件select
标签上加上databaseId
属性,值为上面的别名
<!-- 则在MySQL环境下才发送(环境MyBatis自动识别,只需要上面写上支持导入依赖就行)-->
<select id="getEmpById" resultType="emp" databaseId="mysql">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
</select>
<!-- 则在Oracle环境下才发送-->
<select id="getEmpById" resultType="emp" databaseId="oracle">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
</select>
3.9 mappers标签 ☢
1.引用配置文件
将sql映射注册到全局配置中。
<mappers>
<!-- resource引用类路径下的资源,url引用网络路径下的/或者磁盘下的,这两种都是引用配置文件-->
<mapper resource="empMapper.xml"/>
</mappers>
2.引用(注册)接口
⭐mapper
还有一个属性class
:引用(注册)接口
<mappers>
<mapper class="com.sutong.dao.EmpMapping"/>
</mappers>
-
如果有sql映射文件,映射文件名必须和接口同名,并且放在一个目录下
IDEA中不会把java包下的.java以外的文件放到classes里面。可以在resources目录下,创建一个和java包下与dao层同名的包resources.com.sutong.dao.EmpMapper.xml,编译后会自动合并的
// 接口main.java.com.sutong.dao.EmpMapping.java public interface EmpMapping { Emp getEmpById(Integer id); }
<!-- sql映射文件:main.resources.com.sutong.dao.EmpMapper.xml--> <mapper namespace="com.sutong.dao.EmpMapping"> <select id="getEmpById" resultType="emp" databaseId="mysql"> select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id} </select> </mapper>
-
如果没有映射文件,所有的sql语句都是利用注解写在接口上(这种还不如xml,如果改还要动源码,而且sql长了就麻烦了)
public interface EmpMapping { // Update,Delete等注解都有... @Select("select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}") Emp getEmpById(Integer id); }
3.批量注册
<mappers>
<!-- 使用注解好理解,不用说,
使用配置文件则需要把sql映射文件放到dao包下,即在Idea中这种创建resources.com.sutong.dao-->
<package name="com.sutong.dao"/>
</mappers>
总结:比较重要的复杂的Dao接口来写sql映射文件,不重要的简单的Dao接口为了开发快速可以使用注解!
4. 映射文件
cache
– 该命名空间的缓存配置。cache-ref
– 引用其它命名空间的缓存配置。resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。sql
– 可被其它语句引用的可重用语句块。insert
– 映射插入语句。update
– 映射更新语句。delete
– 映射删除语句。select
– 映射查询语句。
4.1 增删改查
接口:
public interface EmpMapper {
Emp getEmpById(Integer id);
void addEmp(Emp emp);
void updateEmp(Emp emp);
void deleteEmp(Integer id);
}
映射文件:
<mapper namespace="com.sutong.dao.EmpMapper">
<select id="getEmpById" resultType="com.sutong.bean.Emp" databaseId="mysql">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
</select>
<!-- parameterType指定参数类型,可以省略的。#{lastName}就是直接取出对象中lastName属性的值-->
<insert id="addEmp" parameterType="com.sutong.bean.Emp">
insert t_emp(`last_name`, `gender`, `email`) values(#{lastName}, #{gender}, #{email})
</insert>
<!-- 参数类型这里省略了-->
<update id="updateEmp">
update t_emp
set `last_name` = #{lastName}, `gender` = #{gender}, `email` = #{email}
where `id` = #{id}
</update>
<delete id="deleteEmp">
delete from t_emp where `id` = #{id}
</delete>
</mapper>
测试:
// 先获取sqlSessionFactory...
// openSession()方法可以传个boolean值,代表是否自动提交,默认不自动提交!!
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 获取接口实现类
Emp emp = mapper.getEmpById(1); // 查
mapper.addEmp(new Emp(null, "sutong", "1", "sotong@qq.com")); // 增
mapper.updateEmp(new Emp(1, "Tom", "1", "Tom123@qq.com")); // 改
mapper.deleteEmp(4); // 删
sqlSession.commit(); // 手动提交!!
} finally {
sqlSession.close();
}
Mybati允许增删改直接定义 Integer,Long,Boolean,void 类型的返回值,会帮我们自动封装好!!
例如:
Long addEmp(Emp emp);
映射文件中不用写关于返回值的配置!
4.2 insert获取自增主键值
<!-- mysql支持自增主键,可以获取插入这条数据的自增后的主键值(底层是利用Statement对象里面的getGeneratedKeys()方法)
useGeneratedKeys="true"使用自增主键获取主键策略!!
keyProperty="id"指定获取到主键信息后,将这个值封装给JavaBean的哪个属性!!
-->
<insert id="addEmp" useGeneratedKeys="true" keyProperty="id">
insert t_emp(`last_name`, `gender`, `email`) values(#{lastName}, #{gender}, #{email})
</insert>
测试:
Emp e = new Emp(null, "haha", "0", "haha@qq.com");
mapper.addEmp(e); // 增
System.out.println(e.getId()); // id就有值了
Oracle不支持自增,Oracle使用序列来模拟自增,insert标签里面需要使用一个selectKey标签…
4.3 参数处理 ☢
1.单个参数,MyBatis不会做特殊处理
#{参数名}
取出(参数名可以随便写,因为就一个参数!!)
当一个参数是Collection类型(List,Set)或者是数组类型就会特殊处理了
Collection类型的key:collection
#{collection[0]}
,使用#{param1}
不行List类型的key:list
#{list[0]}
数组类型:array
#{array[0]}
单个参数加了
@Param
注解也会特殊处理
2.多个参数,特殊处理。
// 接口方法
Emp getEmpByIdAndLastName(Integer id, String lastName);
<select id="getEmpByIdAndLastName" resultType="emp">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id} and `last_name` = #{lastName}
</select>
// 测试
Emp emp = mapper.getEmpByIdAndLastName(1, "Tom"); // 报错
上面这样会报错!因为多个参数会被封装为一个Map,规则:key就是param1...paramN
(或者参数的索引也可以),value就是传入的参数值
<select id="getEmpByIdAndLastName" resultType="emp">
select `id`, `last_name`, `gender`, `email`
from t_emp where id = #{param1} and `last_name` = #{param2}
</select>
3.命名参数:明确封装时Map的key,接口方法参数使用注解@Param
Emp getEmpByIdAndLastName(@Param("id") Integer id, @Param("lastName") String lastName);
<select id="getEmpByIdAndLastName" resultType="emp">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id} and `last_name` = #{lastName}
</select>
<!-- 这样使用就不会报错的,使用param1..也行-->
4.如果多个参数正好是业务逻辑的数据模型,直接传入pojo
就行了
#{属性名}
就可以取出pojo的属性值
5.如果不是业务路径数据模型,我们可以直接传入Map
接口方法:Emp getEmpByMap(Map<String, Object> map);
#{key}
直接取map中的value就行
6.不是业务路径数据模型,而且经常使用,推荐编写一个TO
(Transfer Object)数据传输对象,例如Page对象等
4.4 参数值的获取
两者效果一样的,但有一点区别
#{}
是以预编译的形式,将参数设置到sql语句中,PreparedStatement
,?
占位符,可以防止SQL注入${}
取出是值直接拼装在sql语句中,Statement
,会有安全问题
大多数情况下我们取参数值都应该取使用#{}。原生Jdbc不支持占位符的地方,需要sql拼接是时候可以使用$(),
例如分表拆分 select * from ${year}_salary where xxx
,排序select * from t_user oder by ${f_name} ${order}
等。
#{}
的丰富用法:
规定参数的一些规则:
javaType,jdbcType,mode(存储过程),numericScale
resultMap,typeHandler,jdbcTypeName,expression(未来准备支持的)
jdbcType(数据库类型)通常在某种情况下需要被设置,我们在数据为null的时候有些数据库可能不能识别mybatis对null的默认处理,例如Oracle(报错),jdbcType OTHER无效的类型。
因为mybatis对所有的null都默认映射为原生的jdbc OTHER 类型,Oracle不能正确。
<insert id="addEmp" parameterType="com.sutong.bean.Emp"> insert t_emp(`last_name`, `gender`, `email`) values(#{lastName}, #{gender}, #{email, jdbcType=NULL}) </insert>
或者改全局设置
<setting name="jdbcTypeForNull" value="NULL"/>
这个值默认是OTHER
4.5 select返回集合 ☢
返回List
, Map
类型!!
public interface EmpMapper {
List<Emp> getEmpsByLastNameLike(String lastName);
// 返回一条记录的Map,key是列名,value就是该字段对应的值
// 例如:{gender=1, last_name=Tom, id=1, email=Tom123@qq.com}
Map<String, Object> getEmpByIdReturnMap(Integer id);
// 多条记录封装为一个Map,key是这条记录的主键(下面注解可指定),value是该记录封装后的JavaBean
// 例如:{2=Emp{id=2, lastName='Jack', gender='1', email='jack@163.com'},
// 5=Emp{id=5, lastName='haha', gender='0', email='haha@qq.com'}}
@MapKey("id") // 告诉mybatisMap的封装这个map的时候那个属性作为map的key
Map<Integer, Emp> getEmpsByLastNameReturnMap(String lastName);
}
<mapper namespace="com.sutong.dao.EmpMapper">
<!-- 如果返回的是一个集合,要写集合中元素的类型!!!-->
<select id="getEmpsByLastNameLike" resultType="com.sutong.bean.Emp">
select * from t_emp where `last_name` like #{lastNamr}
</select>
<!-- 一条记录返回Map类型,resultType就需要写map了(map是个别名,mybatis起好了)-->
<select id="getEmpByIdReturnMap" resultType="map">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
</select>
<!-- resultType还是map集合value元素的类型-->
<select id="getEmpsByLastNameReturnMap" resultType="com.sutong.bean.Emp" >
select * from t_emp where `last_name` like #{lastName}
</select>
</mapper>
4.6 自定义映射规则 ☢
⭐resultMap
可以自定义映射规则(1.as起别名 2.设置setting 3.使用resultMap)
public interface EmpMapperPlus {
Emp getEmpById(Integer id);
}
映射文件:
<mapper namespace="com.sutong.dao.EmpMapperPlus">
<!-- id是规则标识方便引用,type是自定义规则的Java类型-->
<resultMap id="MyEmp" type="com.sutong.bean.Emp">
<!-- id标签是指定主键映射,column是数据库的那一列,property是JavaBean的哪一个属性-->
<id column="id" property="id"/>
<!-- result标签定义普通列-->
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<!-- 如果不指定其他列,自动对应封装(推荐写了resultMap就把所有的映射规则都写上)-->
</resultMap>
<!-- resultMap自定义结果映射规则-->
<select id="getEmpById" resultMap="MyEmp">
select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
</select>
</mapper>
关联查询⭐
resultMap
更强大的功能:关联查询
例如:查询每个员工Emp
对应的部门Dept
信息:
public class Dept {
private Integer id;
private String deptName;
}
public class Emp {
private Integer id;
private String lastName;
private String gender;
private String email;
private Dept dept;
}
// Dao接口方法
public interface EmpMapperPlus {
Emp getEmpAndDept(Integer id);
}
映射文件:
<mapper namespace="com.sutong.dao.EmpMapperPlus">
<!-- id last_name gender email d_id dept_name 后两个列属于Dept对象, -->
<resultMap id="MyDifEmp" type="com.sutong.bean.Emp">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<result column="d_id" property="dept.id"/> <!-- 注意:使用级联属性封装,即属性.属性-->
<result column="dept_name" property="dept.deptName"/>
<!--映射规则的后两列还可以使用association标签:(相当于嵌套了) !!!!
指定联合的JavaBean对象,property指定那个属性是联合的对象,javaType指定这个属性的类型(不能省略)-->
<association property="dept" javaType="com.sutong.bean.Dept">
<!--这个property="id"和最上面那个不一样,这个是在Dept里面的id属性-->
<id column="d_id" property="id"/>
<result column="dept_name" property="deptName"/>
</association>
</resultMap>
<select id="getEmpAndDept" resultMap="MyDifEmp">
select
e.id, e.last_name, e.gender, e.email, e.d_id, d.dept_name
from
t_emp e
join
t_dept d
on
e.d_id = d.id
where
e.id = #{id}
</select>
</mapper>
分步查询⭐
association
标签还能分步查询:类似子查询!!组合已有的方法完成复杂功能
- 先根据员工id查询员工信息
- 根据员工信息中的d_id值取部门表查出部门信息
- 部门信息设置到员工信息中
<mapper>
<!-- id, last_name, gender, email, d_id -->
<resultMap id="MyEmpStep" type="com.sutong.bean.Emp">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<!-- 表示dept属性是根据select指定的方法查询得到的,column指定将那一列的值传给指定的方法
流程:使用select指定的方法查出对象封装给property指定的属性!!-->
<association property="dept" select="com.sutong.dao.DeptMapper.getDeptById" column="d_id"/>
</resultMap>
<select id="getEmpByIdStep" resultMap="MyEmpStep">
select `id`, `last_name`, `gender`, `email`, `d_id` from t_emp where id = #{id}
</select>
</mapper>
前提是必须Dept的Dao层得有getDeptById这个方法和对应的映射文件,这个在实际中基本都有的!!
在分步查询过程中,如果需要多列值传入过去,怎么办呢?则需要将多列的值封装到Map中传递,
column="{key1=column1, key2=column2}"
(=
换成:
也行)
延迟加载⭐
分布查询还能实现 延迟加载/按需加载/懒加载!!
每次查询Emp对象的时候,Dept都跟着一起查出来了
延迟加载:Dept信息在我们使用的时候再去查询!!只需要在分步查询的基础上加上两个配置就能完成
全局配置:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
collection标签
collection标签定义关联集合封装规则!!!
例如:查询部门的时候把部门下的所有员工都查出来!!
public class Dept {
private Integer id;
private String deptName;
private List<Emp> emps;
}
// 接口方法:
public interface DeptMapper {
Dept getDeptByIdPlus(Integer id);
Dept getDeptByIdStep(Integer id);
}
1.关联查询的映射文件(嵌套结果集的方式):
<mapper>
<!-- d_id dept_name | e_id last_name gender email-->
<resultMap id="MyDept" type="com.sutong.bean.Dept">
<id column="d_id" property="id"/>
<result column="dept_name" property="deptName"/>
<!-- collection定义关联集合类型封装规则!ofType指定集合里面元素的类型-->
<collection property="emps" ofType="com.sutong.bean.Emp">
<!-- 定义这个集合中元素的封装规则-->
<id column="e_id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
</collection>
</resultMap>
<select id="getDeptByIdPlus" resultMap="MyDept">
select
d.id as d_id, d.dept_name, e.id as e_id, e.last_name, e.gender, e.email
from
t_dept d
left join
t_emp e
on
d.id = e.d_id
where
d.id = #{id}
</select>
</mapper>
2.分布查询的映射文件:(只要上面那两个设置没关闭是有延迟查询的)
-
先在
EmpMapperPlus.xml
里面定义一个使用d_id
查询员工返回List<Emp>
的方法<select id="getEmpsByDeptId" resultType="com.sutong.bean.Emp"> select `id`, `last_name`, `gender`, `email`, `d_id` from t_emp where d_id = #{deptId} </select>
-
封装到Dept的List集合中
<mapper> <!-- id dept_name --> <resultMap id="MyDeptStep" type="com.sutong.bean.Dept"> <id column="id" property="id"/> <result column="dept_name" property="deptName"/> <!-- collection里面也有select属性和colume属性,和上面一样的意思!!--> <collection property="emps" select="com.sutong.dao.EmpMapperPlus.getEmpsByDeptId" column="id"/> </resultMap> <select id="getDeptByIdStep" resultMap="MyDeptStep"> select id, dept_name from t_dept where id = #{id} </select> </mapper>
collection
和association
标签里还有个属性fetchType="lazy"
表示延迟加载 ,如果这条查询不需要延迟则可设置为eager
discriminator标签
discriminator:鉴别器。可以根据鉴别器根据某列的值改变封装行为。(了解即可)
例如:查Emp,如果查出的是女生就把部分信息查询出来,否则不查询。
下面利用分步查询的基础上加了个鉴别器!!
<resultMap id="MyEmpDis" type="com.sutong.bean.Emp">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<!-- 鉴别器,column指定判定的列名,javaType是列值对应的java类型-->
<discriminator javaType="string" column="gender">
<!-- 女生,resultType指定封装的类型(不能省略)-->
<case value="0" resultType="com.sutong.bean.Emp">
<association property="dept" select="com.sutong.dao.DeptMapper.getDeptById" column="d_id"/>
</case>
<!-- 男生,不查Dept信息-->
<case value="1" resultType="com.sutong.bean.Emp"/>
</discriminator>
</resultMap>
5. 动态SQL
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
5.1 if/where
例如:查询员工,要求如果携带了id字段和lastName字段则按照这两个查询,两者携带那一个就按哪一个查询。
if其实就是根据条件拼接SQL语句!
<mapper namespace="com.sutong.dao.EmpMapperDynamicSQL">
<select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp
where 1 = 1
<!-- test:判断表达式(OGNL表达式,使用和EL类似),表达式里面的值的是从参数里面取的,
遇见特殊字符应该使用转义字符。-->
<if test="id != null">
and id = #{id}
</if>
<if test="lastName != null and lastName != ''">
and last_name = #{lastName}
</if>
</select>
</mapper>
id不传可能会导致语SQL法错误,解决方案:
1.where后面写个1=1,后面所有条件都加上and(看上面)
2.使用where标签将所有的查询条件包括(MyBatis推荐),MyBatis会帮我们第一个多出来的and或者or
<select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp
<where>
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null and lastName != ''">
and last_name = #{lastName}
</if>
</where>
</select>
还能这样写:(OGNL表达式甚至还能调用静态方法使用
@
,可看官方文档)<if test="email != null and email.trim() != ''"> and email = #{email} </if> /* gender是字符串可以直接和数组比较,会自动转换*/ <if test="gender == 0 or gender == 1"> and gender = #{gender} </if>
5.2 trim
可以用trim标签解决后面多出的and或者or!!(了解)
<select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp
<!-- trim标签体里的整个拼接后的字符串,
prefix加前缀,prefixOverrides前缀覆盖,去掉整个字符串多余的字符,
suffix加后缀,suffixOverrides去掉字符串后面多余的字符 -->
<trim prefix="where" suffixOverrides="and">
<if test="id != null">
id = #{id} and
</if>
<if test="lastName != null and lastName != ''">
last_name = #{lastName}
</if>
</trim>
</select>
5.3 choose
类似Java中的swtich-case,只会选一个!!
<select id="..." resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="lastName != null">
last_name = #{lastName}
</when>
<otherwise> <!-- 上面都不满足则查所有-->
1 = 1
</otherwise>
</choose>
</where>
</select>
5.4 set
带那一列的值就更新那一列(当然这个set也能用trim标签替换)
<update id="updateEmp">
update t_emp
<!-- 不用担心多出来的逗号了-->
<set>
<if test="lastName != null">
last_name = #{lastName},
</if>
<if test="gender == 0 or gender = 1">
gender = #{gender},
</if>
<if test="email != null">
email = #{email}
</if>
</set>
where id = #{id}
</update>
5.5 foreach
例如:查询指定id的员工,可多个!
public interface EmpMapperDynamicSQL {
// 批量查询
List<Emp> getEmpsByConditionForEach(@Param("ids") List<Integer> ids);
// 批量插入
void addEmps(@Param("emps") List<Emp> emps);
}
批量查询:
<select id="getEmpsByConditionForEach" resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp where `id` in
<!-- collection是要遍历的集合,如果是List类型参数会特殊处理Key默认是list,我们加了注解可以用注解写的名字!!
item是当前遍历的元素,separator表示每个元素之间的分隔符,
open是遍历的结果拼接一个开始的字符,close结束字符,
index是遍历List类型是元素的索引,Map类型表示map的Key,item就是map的Value -->
<foreach collection="ids" item="item_id" separator="," open="(" close=")">
#{item_id}
</foreach>
</select>
批量保存:
-
第一种(推荐)
<!-- MySQL下,支持values(),(),()语法 --> <insert id="addEmps"> insert into t_emp(`last_name`, `gender`, `email`, `d_id`) values <foreach collection="emps" item="emp" separator=","> (#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id}) </foreach> </insert>
-
第二种(这种还可以用于其他的批量操作!!删除修改都行)
jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true
<!-- 这样也行,但在MySQL需要开启,在一条语句中用';'来分割多条查询,即在配置文件中加上allowMultiQueries=true--> <insert id="addEmps"> <foreach collection="emps" item="emp" separator=";"> insert into t_emp(`last_name`, `gender`, `email`, `d_id`) values(#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id}); </foreach> </insert>
Oracle不支持values(),(),()语法。支持这种:
#1.多条insert放到begin end之间 begin insert into t_emp(`id`, `last_name`, `gender`, `email`) values(t_emp_seq.nextVal, 'test01', '1', 'test01@qq.com'); insert into t_emp(`id`, `last_name`, `gender`, `email`) values(t_emp_seq.nextVal, 'test02', '0', 'test02@qq.com'); end; #2.利用中间表 insert into t_emp(`id`, `last_name`, `gender`, `email`) select t_emp_seq.nextVal, `last_name`, `gender`, `email` from( select 'test01' as last_name , '1' as gender, 'test01@qq.com' as email from dual union select 'test02' as last_name, '0' as gender, 'test02@qq.com' as email from dual )
5.6 两个内置参数
_parameter
:代表整个参数。
如果单个参数则_parameter就代表这个参数。如果多个参数底层封装为Map,则_parameter就代表这个Map
_databaseId
:如果配置了DatabaseIdProvider标签(支持多厂商数据库的标签),那么就代表当前厂商数据库的别名。
<select id="getEmps" resultType="com.sutong.bean.Emp">
<if test="_databaseId == 'mysql'">
select * from t_emp
<if test="_parameter != null"> <!-- 传入参数才按照参数查-->
where id = #{id}
</if>
</if>
<if test="_databaseId == 'oracle'">
select * from emps
</if>
</select>
5.7 bind
在增删改查标签里面,还有个bind标签,可以OGNL表达式的值绑定到一个变量中,方便后来引用这个变量。
<select id="getEmp" resultType="com.sutong.bean.Emp">
<bind name="_lastName" value="'%' + lastName + '%'"/>
select * from e_emp where last_name like #{_lastName}
<!-- 模糊查询需要在我们传参的时候加上%。 这样是不行的:'%#{lastName}%'-->
</select>
但还是推荐在传参是时候加上模糊查询规则:
Emp emp = mapper.getEmp("%a%");
5.8 sql
sql可以抽取可重用的sql片段,方便后面引用。经常用于抽取查询插入时的很多字段。
<!-- sql标签里面也能写if等和上面的两个参数判断等-->
<sql id="insertColumn">
`last_name`, `gender`, `email`, `d_id`
</sql>
<insert id="addEmp">
<!-- include引入定义的sql,该标签里面还能给sql标签传参!${propName}取值-->
insert into t_emp( <include refid="insertColumn"></include> )
values (#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id})
</insert>
6. 缓存机制
MyBatis包含了一个非常强大的查询缓存特性,它可以非常方便的配置和定制。极大提升了查询效率。默认定义了两级缓存。
6.1 一级缓存
一级缓存(本地缓存):与数据库同一次会话期间查询到的数据会放到本地缓存中。以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库。
也叫sqlSession级别的一个Map(每个sqlSession都有一个缓存),是一直存在的,我们没办法关闭。
try {
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
Emp emp01 = mapper.getEmpById(1);
System.out.println(emp01);
Emp emp02 = mapper.getEmpById(1);
System.out.println(emp02);// 打印了两次,但只发送了一次sql
System.out.println(emp01 == emp02); // true
} finally {
sqlSession.close();
}
⭐一级缓存失效情况:
- sqlSession不同
- sqlSession相同,查询条件不同(原因:当前一级缓存中还没有这个数据)
- sqlSession相同,两次查询期间执行了增删改操作(增删改可能对当前数据有影响)
- sqlSession相同,手动清除了一级缓存,sqlSession.clearCache()
6.2 二级缓存
二级缓存(全局缓存):基于namespace级别的缓存,一个namespace对应一个二级缓存。
工作机制:
-
一个会话,查询一条数据,这个数据就会放到当前会话的一级缓存中
-
如果会话关闭,一级缓存中的数据会被保存到二级缓存中(注意会话关闭才会写入)
-
开启新的会话,查新信息的时候就会参考二级缓存中的内容
-
sqlSession --> EmpMapper -> Emp
--> DeptMapper -> Dept
不同的namespace查出的数据会放到自己对应的缓存中(Map中)
⭐使用:
1.开启全局二级缓存配置(默认是true)
<setting name="cacheEnabled" value="true"/>
2.取每个mapper.xml中配置使用二级缓存
<!-- eviction缓存的回收策略(LRU|FIFO|SOFT|WEAK)
flushInterval缓存刷新间隔毫秒为单位(多长时间情况一次,默认不清空)
readOnly缓存是否只读,
只读:mybatis认为所有从缓存中获取的数据都是只读操作,认为不会修改数据。所以为了加快速度,则直接把数据在缓存中的引用交给用户。
非只读:获得来的数据可能会被修改,mybatis会利用反序列化拷贝一份数据给用户(默认)
size缓存存放多少元素
type指定自定义缓存的全类名(实现Cache接口)-->
<cache eviction="LRU" flushInterval="60000" readOnly="false" size="1024"/>
属性可以不写都使用默认的 <cache></cache>
3.因为安全问题,底层使用序列化和反序列化技术,我们的pojo需要实现序列化接口Serializable
注意:查询的数据都会默认放在一级缓存中,只有会话提交或者关闭之后,一级缓存中的数据才会转移到二级缓存中。
和缓存相关的设置/属性:
cacheEnabled
如果设置为false,关闭的是二级缓存- 每个
select
标签都有一个useCache="true"
属性,默认true。设置为false则关闭的还是二级缓存 - 每个增删改标签都有一个
flushCache="true"
属性,默认true,代表增删改执行完之后都要清除缓存(一级二级缓存都会被清空),其实查询标签里面也要这个属性,但默认是false。 sqlSession.clearCache()
只会清除当前sqlSession的一级缓存。localCacheScope
本地缓存作用域(影响的是一级缓存),取值SESSION (默认) | STATEMENT (对相同 SqlSession 的不同查询将不会进行缓存。相当于可以禁用一级缓存)
图解(查询缓存的顺序:二级缓存 -> 一级缓存 -> 数据库):
6.3 第三方缓存
MyBatis缓存底层就是个简单是Map,有个Cache接口供第三方拓展(Redis,EHCache)
// EHCache依赖
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.9</version>
</dependency>
// EhCache和MyBatis整合包(即官网已经根据ehcache实现了CaChe接口,自定义缓存)
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
使用:
-
给每个映射文件里加入
cache
标签<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache> <!-- 别的映射文件下也可用 引用缓存!!namespace指定和那个名称空间下的缓存一样--> <cache-ref namespace="com.sutong.dao.EmpMapper"/>
-
需要在类路径下放一个
ehcache.xml
文件(这里暂不研究,代码里面或百度有解释)?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 磁盘保存路径 --> <diskStore path="D:\Learning\Java_Practice\Java Frame\ehcache"/> <defaultCache maxElementsInMemory="10000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
-
然后开启了二级缓存就可以直接使用了(pojo记得实现序列化接口)
@Test public void test02() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession1 = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); try { EmpMapper mapper1 = sqlSession1.getMapper(EmpMapper.class); EmpMapper mapper2 = sqlSession2.getMapper(EmpMapper.class); Emp e1 = mapper1.getEmpById(1); System.out.println(e1); sqlSession1.close(); // 关闭才能放到二级缓存中 Emp e2 = mapper2.getEmpById(1); System.out.println(e2); sqlSession2.close(); // 可以看到上面我们配置的文件夹里面有缓存的文件了 } finally { //sqlSession.close(); } }
7. MyBatis-Spring整合
MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession
并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException
。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。
依赖:
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
7.1 编写数据源配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value=".." />
<property name="username" value=".." />
<property name="password" value=".." />
<property name="driverClassName" value=".." />
</bean>
7.2 SqlSessionFactoryBean
在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder
来创建 SqlSessionFactory
的。而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean
来创建。
还有一个常用的属性是 configLocation
,它用来指定 MyBatis 的 XML 配置文件路径。
还有个mapperLocations指定sql映射文件的位置(在xml和类不在一个文件夹的时候使用)
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
7.3 SqlSessionTemplate
SqlSessionTemplate
是 MyBatis-Spring 的核心。作为 SqlSession
的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession
。 SqlSessionTemplate
是线程安全的,可以被多个 DAO 或映射器所共享使用。
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 构造器注入,里面没得set方法-->
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
7.4 加实现类
需要给接口加实现类,注入到Spring中。
里面获取sqlSession,获取对应的Mapper代理,调用对应的方法就行了。最后注入Spring,后面的Service层就能用了。
public class UserDaoImpl implements UserDao {
private SqlSession sqlSession;
public void setSqlSession(SqlSession sqlSession) { this.sqlSession = sqlSession; }
public User getUser(String userId) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.getUser(userId);
}
}
<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
<property name="sqlSession" ref="sqlSession" />
</bean>
或者使用 SqlSessionDaoSupport 抽象类:SqlSessionDaoSupport
用来为你提供 SqlSession
。调用 getSqlSession()
方法你会得到一个 SqlSessionTemplate
,之后可以用于执行 SQL 方法,
public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
// 这样我们的SqlSessionTemplate可以不注入到Spring中,我们可以直接getSqlSession();
// 但他的父类需要注入一个SqlSessionFactory
public User getUser(String userId) {
SqlSession sqlSession = getSqlSession();
return sqlSession.getMapper(UserMapper.class).getUser(userId);
}
}
<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
(这个了解,重要是SSM整合)
上面的我们手动写实现类,下面可以让Spring帮我们!!!⭐
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="com.sutong.dao"/>
</bean>
<!-- 和上面的MapperScannerConfigurer作用一样(上面的老项目用得多)-->
<!-- <mybatis-spring:scan base-package="com.sutong.dao"/> -->
8. 逆向工程(MBG)
MyBatis Generator(MBG):专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对应的映射文件,以及QBC风格的条件查询。但表连接存储过程等复杂的SQL的定义需要我们手工编写
依赖(当然还需要MySQL驱动和MyBatis依赖):
<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
8.1 配置 ☢
详细可以看官网
src/main/resources/generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- context 指定MyBatis的运行环境
MyBatis3Simple是生成简单版的CRUD,MyBatis3复杂一点带动态标签的查询添加等!!会多几个类-->
<context id="DB2Tables" targetRuntime="MyBatis3Simple">
<!-- 去掉自动生成的注释-->
<commentGenerator>
<property name="suppressDate" value="true"/> <!-- 去掉生成日期那行注释-->
<property name="suppressAllComments" value="true" /> <!-- 去掉所有的-->
</commentGenerator>
<!-- jdbcConnection 指定如何连接到目标数据库-->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis"
userId="root"
password="9527">
</jdbcConnection>
<!-- java类型解析器,不加使用默认的也行-->
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!-- 指定JavaBean生成策略,targetPackage生成的目标包名,targetProject指定目标工程, .\当前工程的src下-->
<javaModelGenerator targetPackage="com.sutong.bean" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- sql映射文件生成策略,(可以生成在dao包下,然后批量注册)-->
<sqlMapGenerator targetPackage="mybatis.mapper" targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<!-- 指定Mapper接口所在的位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.sutong.dao"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- 指定要逆向分析的那些表,根据表创建JavaBean,domainObjectName生成Bean的名字-->
<table tableName="t_emp" domainObjectName="Emp"></table>
<table tableName="t_dept" domainObjectName="Dept"></table>
</context>
</generatorConfiguration>
pom.xml
<build>
<plugins>
<plugin>
<!--Mybatis-generator插件,用于自动生成Mapper和POJO-->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<!--配置文件的位置-->
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite> <!-- 如果存在就覆盖-->
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
还要有全局配置文件mybatis-config.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">
<configuration>
<!-- 因为外部properties配置文件内容,resource属性引入类路径下资源,url引入网络下或磁盘下-->
<properties resource="dbconfig.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="cacheEnabled" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 将写好的sql映射文件一定要注册到全局配置文件中!!!-->
<mappers>
<mapper resource="mybatis/mapper/DeptMapper.xml"/>
<mapper resource="mybatis/mapper/EmpMapper.xml"/>
</mappers>
</configuration>
8.2 运行
public class RunMybatisGenerator {
@Test
public void run() throws InterruptedException, SQLException,
IOException, XMLParserException, InvalidConfigurationException {
List<String> warnings = new ArrayList<>();
boolean overwrite = true;
File configFile = new File("src/main/resources/generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
}
或者直接右上角Maven的点mybatis-generator命令就可以运行了!!!
它自动把数据库里面的下划线命名法换位Java的驼峰命名法!!
运行后:
8.3 测试
@Test
public void test01() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// MyBatis3Simple
try {
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
System.out.println(mapper.selectAll()); // 没有自动生成toString()
} finally {
sqlSession.close();
}
// MyBatis3
try {
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// xxExample就是封装查询条件的
List<Emp> allEmp = mapper.selectByExample(null); // 查询所有
// (查询员工字母带a的,而且性别的1的) 或者 (Email里面带e的)
EmpExample empExample = new EmpExample(); // 封装查询条件的Example
EmpExample.Criteria c1 = empExample.createCriteria(); // criteria就是拼装条件的
c1.andLastNameLike("%a%").andGenderEqualTo("1");
EmpExample.Criteria c2 = empExample.createCriteria();
c2.andEmailLike("%e%");
empExample.or(c2); // 把或条件拼装上去
List<Emp> empsByExample = mapper.selectByExample(empExample); // 条件查询
} finally {
sqlSession.close();
}
}
MyBatis3生产的是GBC风格的查询!!
9. MyBatis工作原理
⭐重要,对后面的插件开发有用
9.1 分层架构
9.2 运行流程
1.根据全局配置文件,创建SqlSessionFactory
2.获取SqlSession实例
3.获取接口代理对象 (MapperProxy对象)
4.执行增删改查
①SqlSessionFactory的获取:
其中的每个
MapperStatement
代表一个增删查改标签的详细信息
Configuration
包括 全局配置文件+所有mapper映射文件 的详细信息。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
最终得到的是一个
DefalutSqlSessionFactory
对象(里面包含了Configuration
对象)
总结:把所有配置文件信息解析保存到Configuration
对象中,然后返回包含了Configuration
的DefalutSqlSessionFactory
对象中
②sqlSession的获取:
上图的第七步很重要,是执行所有拦截器的拦截方法的。
SqlSession sqlSession = sqlSessionFactory.openSession();
即调用
DefalutSqlSessionFactory
方法创建返回一个DefalutSqlSession
对象,里面包含了Configuration
和Executor
对象!!!(Executor对象会在这一步创建)
总结:返回一个DefalutSqlSession
对象,包含了 Configuration
和Executor
。
③获取接口代理对象
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
返回接口代理对象,里面包含了
DefalutSqlSession
对象,所以可以用来执行增删查改。
总结:使用MapperProxyFactory
创建对应代理对象,代理对象包含了DefalutSqlSession
对象
④执行增删查改
代理对象 -> DefaultSqlSession -> Executor -> StatementHandler (处理sql语句预编译,设置参数等工作)
-> ParameterHandler (设置预编译参数用的)
-> ResultSetHandler (处理结果集)
(设置参数和处理结果都用到了TypeHandler来做的,处理数据库和Java类型的映射)
查询总结图解:
重要了解四大对象的作用和执行流程就行
9.3 总结 ☢
-
根据所有配置文件,初始化出
Configuration
对象 -
创建一个
DefaultSqlSession
对象,里面包含Configuration
和Executor
(根据配置文件中的
defaultExecutorType
设置创建对应的Executor
) -
getMapper() ,拿到Mapper接口对应的
MapperProxy
,里面包含DefaultSqlSession
-
执行增删改查
- 调用
DefaultSqlSession
中的Excutor
的增删改查 - 会创建一个
StatementHandler
对象(同时创建ParameterHandler
和RestSetHandler
) - 调用
StatementHandler
预编译以及利用ParameterHandler
设置sql的参数 - 调用
StatementHandler
的增删查改方法 RestSetHandler
封装结果
- 调用
四大对象创建的时候都会有:interceptorChain.plugAll(xxxx)
!!!
10. 插件开发
10.1 插件原理
-
四大对象创建不是直接返回的而是先调用
interceptorChain.plugAll(xxxx)
方法 -
而
plugAll()
方法里面是拿到所有的Interceptor
(即拦截器,我们需要实现的接口),然后调用每个拦截器的plugin()
方法,target = interceptor.plugin(target)
,执行完所有拦截器,后返回target
包装后的对象。 -
插件机制,我们我们可以使用插件为目标对象创建一个代理对象:AOP
我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行方法
10.2 插件实现
- 编写Interceptor实现类
- 使用Intercepts注解
- 注册到全局配置文件中
MyFirstPlugin.java
package com.sutong.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Statement;
import java.util.Properties;
// Intercepts注解完成插件的签名(告诉MyBatis当前插件拦截那个对象的那个方法),里面是Signature数组,
// Signature里面有type属性是要拦截对象(四大对象之一),method属性是要拦截该对象的那个方法,args属性是指定该方法是参数列表
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyFirstPlugin implements Interceptor {
/**
* intercept() - 拦截目标对象目标方法的执行
* @param invocation 目标方法
* @return 返回执行后的返回值
*/
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin - intercept -> " + invocation.getMethod());
Object result = invocation.proceed(); // 执行目标方法,前后可以做一些事
return result;
}
/**
* plugin() - 包装目标对象 (包装:为目标对象插件代理对象)
* 四大个对象的创建都会调用这个方法,只有是我们要拦截的对象才会创建代理对象!否则直接返回
* @param target 目标对象
* @return 返回当前target插件的动态代理
*/
public Object plugin(Object target) {
System.out.println("MyFirstPlugin - plugin -> 将要包装的对象" + target);
Object wrap = Plugin.wrap(target, this); // Mybatis为了我们使用方便,包装了Proxy生成代理对象
return wrap;
}
/**
* setProperties() - 将插件注册时的 property 属性设置进来
* @param properties 插件的配置信息
*/
public void setProperties(Properties properties) {
System.out.println("插件的配置信息:" + properties);
}
}
mybatis-config.xml
<!-- 注册插件-->
<plugins>
<plugin interceptor="com.sutong.interceptor.MyFirstPlugin">
<property name="username" value="sutong"/>
<property name="password" value="666"/>
</plugin>
</plugins>
10.3 多个插件 ☢
如果多个插件拦截同一个对象的同一个方法,则:
plugin()
则和注册插件的顺序有关!有几个插件包装几次!!多层代理… (正序)
intercept()
由于的多层代理,一进来是先进入最后一个包装的代理,执行其intercept方法,然后进入第二层代理执行方法…(反序)
创建插件动态代理的时候,是按照插件配置顺序创建代理对象,执行目标方法的时候按照逆向顺序执行的。
10.4 简单使用
动态改变一下sql运行的参数!!
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyFirstPlugin implements Interceptor {
// 偷梁换柱
public Object intercept(Invocation invocation) throws Throwable {
// 动态改变一下sql运行的参数:以前查1号员工,我们实际从数据库查2号员工!!偷梁换柱
// 拿到StatementHandler -> ParameterHandler -> parameterObject值
Object target = invocation.getTarget(); // 我们在拦截的对象
MetaObject metaObject = SystemMetaObject.forObject(target); // 拿到target的元数据
//metaObject.getValue("parameterHandler.parameterObject"); // 获取sql语句用的参数
metaObject.setValue("parameterHandler.parameterObject", 2); // 修改sql的参数
// 执行目标方法
Object result = invocation.proceed();
return result;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {}
}
测试:
@Test
public void test01() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
System.out.println(mapper.getEmpById(1)); // 则这里返回的是2号员工的信息!!!!
} finally {
sqlSession.close();
}
}
10.5 插件应用
- PageHelper插件进行分页
- 批量操作
- 存储过程
- typeHandler处理枚举
PageHelper ☢
PageHelper分页插件的使用(支持多种数据库)
1.依赖:
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.0</version>
</dependency>
2.配置拦截器
-
在MyBatis中配置
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 --> <property name="param1" value="value1"/> </plugin> </plugins>
-
或 在 Spring 配置文件中配置拦截器插件!!
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 注意其他配置dataSource,mapperLocations等.. --> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <!--使用下面的方式配置参数,一行配置一个 --> <value> params=value1 helperDialect=mysql </value> </property> </bean> </array> </property> </bean>
3.使用
接口:
// 接口
public interface EmpOther {
List<Emp> getEmps();
}
映射文件:
<mapper namespace="com.sutong.dao.EmpOther">
<select id="getEmps" resultType="com.sutong.bean.Emp">
select `id`, `last_name`, `gender`, `email` from t_emp
</select>
</mapper>
测试:
@Test
public void test01() throws IOException {
// ...
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 只调用这一个方法进行,第一个是第几页,第二个是每页记录数,返回一个分页的详细信息对象
Page<Object> pageHandler = PageHelper.startPage(2, 2);
EmpOther mapper = sqlSession.getMapper(EmpOther.class);
for (Emp emp : mapper.getEmps()) {
System.out.println(emp);
}
System.out.println("当前页码: " + pageHandler.getPageNum());
System.out.println("总页码: " + pageHandler.getPages());
System.out.println("总记录数: " + pageHandler.getTotal());
System.out.println("每页记录数: " + pageHandler.getPageSize()); // 还有...
} finally {
sqlSession.close();
}
}
// 或者使用PageInfo获得信息,更详细!!PageInfo<Emp> info = new PageInfo<>(list); 对结果进行包装
// 上面的那几个信息都有,还要 info.isIsFirstPage(),isIsLastPage()...等等!!
// 还可以这样new PageInfo<>(list, navigatePages); navigatePages是个int ,代表连续显示几页!!
public void test02() throws IOException {
// ...
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
PageHelper.startPage(2, 2); // 开始分页
EmpOther mapper = sqlSession.getMapper(EmpOther.class);
List<Emp> list = mapper.getEmps(); // 查询
PageInfo<Emp> info = new PageInfo<Emp>(list, 5); // 告诉插件我们要连续5页!!!
for (Emp emp : list) {
System.out.println(emp);
}
System.out.println("是否是第一页: " + info.isIsFirstPage());
int[] nums = info.getNavigatepageNums(); // 连续显示的页码,就不用我们处理了!!直接返回前端取出
System.out.println(Arrays.toString(nums));
} finally {
sqlSession.close();
}
}
批量处理
上面的动态SQL使用的foreach严格一样上不算批量处理,那上面只是拼一个很长的SQL一起发送服务器,如果太长了服务器就不能接受了。
有个设置defaultExecutorType
是配置默认的执行器。默认是SIMPLE
简单执行器, BATCH
执行器不仅能重用语句还会执行批量更新。
如果在全局配置文件改的话所有是SQL都会变为批量的。我们可以单独设置(在获取sqlSession传入一个参数)。
-
Mybatis使用
@Test public void testBatch() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 传入ExecutorType,就可以获得一个批量操作的sqlSession!!!! SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { EmpOther mapper = sqlSession.getMapper(EmpOther.class); List<Emp> list = new ArrayList<Emp>(); list.add(new Emp(null, UUID.randomUUID().toString(), "1", "test@qq.com")); list.add(new Emp(null, UUID.randomUUID().toString(), "0", "test@qq.com")); list.add(new Emp(null, UUID.randomUUID().toString(), "1", "test@qq.com")); // .... // 调用多次添加就行了,时间很短的。 // 非批量的每执行一个增删改查就发送一个SQL到数据库,预编译(n次) -> 设置参数(n次) -> 执行(n次) // 批量一起发送执行,预编译(1次) -> 设置参数(n次) -> 执行(1次) for (Emp emp : list) { mapper.addEmp(emp); } sqlSession.commit(); } finally { sqlSession.close(); } }
-
Spring中使用
配置
spring-dao.xml
里面:<!-- 配置一个可以批量执行的sqlSession--> <bean id="batchSqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg name="sqlSessionFactory" value="sqlSessionFactory"/> <constructor-arg name="executorType" value="BATCH"/> </bean>
使用:
public class EmpServiceImpl { private BookMapper bookMapper; @Autowired private SqlSession sqlSession; public void testBatch() { EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 然后调用里面的增删改查方法就是批量的了 } }
- 非批量:预编译(n次) -> 设置参数(n次) -> 执行(n次)
- 批量:预编译(1次) -> 设置参数(n次) -> 执行(1次)
存储过程
存储过程:很多sql语句是集合,编译一次,保存在数据库中的。MyBatis支持对存储过程的调用。
先学习一下存储MySQL中的存储过程:
-
入参存储过程(入参类似java方法中的形参)
# 参数写法:输入输出类型 参数名称 参数数据类型 # 入参存储过程,例如:根据传来的数字,动态赋值name create procedure add_name(in target int) #in代表形参,out代表返回值 begin # begin 和 end 之间写我们的逻辑 declare emp_name varchar(11); #定义一个变量 if target = 1 then set emp_name = '一号员工'; # 赋值需要加个set,否则的判断 else set emp_name = '其他员工'; end if; insert into t_emp(`last_name`, `gender`, `email`) values(emp_name, '1', 'test@qq.com'); end # 调用存储过程: call add_name(1); # 删除存储过程: drop procedure add_name;
-
出参存储过程(出参类似java方法中的返回值)
# 出参存储过程,例如:统计t_emp表总条数 create procedure count_emp(out count_num int) begin # 直接把查询出来的结果赋值给count_num select count(*) into count_num from t_emp; end; # 定义变量前面需要加上@,这样是不显示东西的,需要去查询这个变量 call count_emp(@count_num); select @count_num; drop procedure count_emp;
MyBatis中存储过程的调用:
接口:
public interface EmpOther {
void testProcedure(Integer num);
}
映射文件:
<!-- 调用存储过程(首先要得有!),要使用select标签,例如调用上面创建的add_name存储过程!!
statementType="CALLABLE"是表示要调用存储过程,默认的PREPARED。 mode=IN代表是入参-->
<select id="testProcedure" statementType="CALLABLE">
{call add_name( #{num, mode=IN, jdbcType=INTEGER} )}
</select>
测试:
@Test
public void test03() throws IOException {
// ...
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmpOther mapper = sqlSession.getMapper(EmpOther.class);
mapper.testProcedure(10); // 调用
} finally {
sqlSession.close();
}
}
typeHandler处理类型
学习写枚举的使用
@Test public void testEnumUse() { EmpStatus login = EmpStatus.LOGIN; System.out.println(login.ordinal()); // 枚举索引 System.out.println(login.name()); // 枚举名字 }
测试默认的枚举处理:
public enum EmpStatus {
LOGIN, LOGOUT, REMOVE
}
public class Emp {
private Integer id;
private String lastName;
private String gender;
private String email;
private EmpStatus empStatus; // 员工状态
}
<insert id="addEmp">
insert into t_emp(`last_name`, `gender`, `email`, `empStatus`)
values (#{lastName}, #{gender}, #{email}, #{empStatus})
</insert>
测试默认的枚举处理:
try {
EmpOther mapper = sqlSession.getMapper(EmpOther.class);
mapper.addEmp(new Emp(null, "enum", "1", "enum@qq.com", EmpStatus.LOGIN));
sqlSession.commit();
} finally {
sqlSession.close();
}
数据库默认保存的是枚举的名字:LOGIN
,即使用EnumTypeHandler
处理的!!
我们可以改变让数据库存储枚举索引,使用EnumOrdinalTypeHandler
,要设置全局配置文件
<typeHandlers>
<!-- handler指定要用的类型处理器,javaType是指定要处理的类型(不写对所有满足的都用这个)-->
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
javaType="com.sutong.bean.EmpStatus"/>
</typeHandlers>
上面这样使用的并不多,自定义处理才是使用最多的:
/**
* 希望数据库保存的是状态码,而不是默认的索引或者枚举名!
*/
public enum EmpStatus {
LOGIN(100, "用户登录"), LOGOUT(200, "用户登出"), REMOVE(300, "用户不存在");
private Integer code;
private String msg;
private EmpStatus(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
// 按照状态码返回枚举对象
public static EmpStatus getEmpStatusByCode(Integer code) {
switch (code) {
case 100:
return EmpStatus.LOGIN;
case 200:
return EmpStatus.LOGOUT;
case 300:
return EmpStatus.REMOVE;
default:
return EmpStatus.LOGOUT;
}
}
// 下面还有get/set
}
自定义类型处理器要TypeHandler
接口或者继承BaseTypeHandler
// 自定义的枚举类型处理器
public class MyEnumEmpStatusHandler implements TypeHandler<EmpStatus> {
// 定义当前数据如何保存到数据库中
public void setParameter(PreparedStatement preparedStatement, int i,
EmpStatus empStatus, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, empStatus.getCode().toString()); // 保存状态码
}
public EmpStatus getResult(ResultSet resultSet, String s) throws SQLException {
int code = resultSet.getInt(s); // s是数据库字段名
// 根据从数据库拿到的状态码,返回一个枚举对象
return EmpStatus.getEmpStatusByCode(code);
}
public EmpStatus getResult(ResultSet resultSet, int i) throws SQLException {
int code = resultSet.getInt(i); // i是数据库字段索引
return EmpStatus.getEmpStatusByCode(code);
}
public EmpStatus getResult(CallableStatement callableStatement, int i) throws SQLException {
int code = callableStatement.getInt(i); // i是数据库字段索引,是存储过程中拿!!
return EmpStatus.getEmpStatusByCode(code);
}
}
全局配置:
<typeHandlers>
<typeHandler handler="com.sutong.handler.MyEnumEmpStatusHandler"
javaType="com.sutong.bean.EmpStatus"/>
</typeHandlers>
<!-- 也可以在处理每个字段的时候告诉MyBatis用那个类型处理器-->
保存时:#{empStatus,typeHandler=com.sutong.handler.MyEnumEmpStatusHandler}
查询时:就要使用ResultMap了,自定义,<result column="empStatus" property="empStatus" typeHandler=".."/>
我们应该保证在查询和保存使用的的类型处理器是一样的!!!这样才不会出错
测试:
try {
EmpOther mapper = sqlSession.getMapper(EmpOther.class);
mapper.addEmp(new Emp(null, "myEnum", "0", "myEnum@qq.com", EmpStatus.REMOVE));
sqlSession.commit();
} finally {
sqlSession.close();
}
Emp emp = mapper.getEmpById(14);
System.out.println(emp.getEmpStatus()); // 查询出来的时候会封装为EmpStatus对象!!