1 概述
MyBatis从入门到精通,简要地把MyBatis的知识都说了。MyBatis其实就是一个方便在xml文件写SQL的库而已,并没有很好地做好ORM的工作,只能算是一个半自动的ORM。优点是简单,可控,上手快。
2 SqlSession与SqlSessionFactory
2.1 裸写MyBatis
代码在这里
<?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>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="cacheEnabled" value="false"/>
<settings>
</typeAliases>
<package name="mybatis_test.model"/>
<typeAliases>
</environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
<transactionManager>
</dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/Test"/>
<property name="username" value="root"/>
<property name="password" value="1"/>
<dataSource>
</environment>
</environments>
</mappers>
<mapper resource="spring_test/mapper/CountryMapper.xml"/>
<mappers>
</configuration> </
首先写一个mybatis的配置文件,typeAliases是为了简化resultType中不需要写命名空间,因为它指定了默认查询那些包下面的resultType。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper">
<select id="selectAll" resultType="Country">
<
select id,countryName,countryCode from t_countryselect>
</insert id="add" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode) values(#{countryName},#{countryCode})insert>
</delete id="del">
<
delete from t_country where id = #{id}delete>
</update id="mod">
<
update t_country set countryName=#{countryName},countryCode=#{countryCode} where id = #{id}update>
</mapper> </
然后我们写了CountryMapper.xml,相当于用xml的方式写SQL代码。
private SqlSessionFactory sqlSessionFactory;
private void init(){
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
.close();
reader}catch(Exception e){
.printStackTrace();
e}
}
然后我们创建一个SqlSessionFactory
private void printCountryList(List<Country> countryList){
for( Country country : countryList){
System.out.printf("id:%s,name:%s,code:%s\n",country.id,country.countryName,country.countryCode);
}
}
private void showCountryList(SqlSession sqlSession){
List<Country> countryList = sqlSession.selectList("spring_test.mapper.CountryMapper.selectAll");
printCountryList(countryList);
}
private void addCountry(SqlSession sqlSession){
= new Country();
Country country .countryCode = "67";
country.countryName = "我的国";
country
.insert("spring_test.mapper.CountryMapper.add",country);
sqlSession}
private void modCountry(SqlSession sqlSession,Long id){
= new Country();
Country country .id = id;
country.countryCode = "88";
country.countryName = "他的国";
country
.update("spring_test.mapper.CountryMapper.mod",country);
sqlSession}
private void delCountry(SqlSession sqlSession,Long id){
.delete("spring_test.mapper.CountryMapper.del",id);
sqlSession}
private void test1(){
System.out.println("---- test1 ----");
= this.sqlSessionFactory.openSession();
SqlSession sqlSession
try {
showCountryList(sqlSession);
addCountry(sqlSession);
delCountry(sqlSession, 3L);
modCountry(sqlSession, 2L);
showCountryList(sqlSession);
}finally {
.rollback();
sqlSession}
}
执行语句的时候,我们总是首先用sqlSessionFactory的openSession来获取sqlSession。最后执行sqlSession的select,update,delete,insert来执行具体的方法。这些方法的第一个参数都是xml中命名空间+id的位置,然后传入参数即可。
private void showCountryListNow(){
= this.sqlSessionFactory.openSession();
SqlSession sqlSession2 showCountryList(sqlSession2);
.close();
sqlSession2}
private void test2(){
System.out.println("---- test2 ----");
= this.sqlSessionFactory.openSession();
SqlSession sqlSession
try {
showCountryList(sqlSession);
addCountry(sqlSession);
//在sqlSession没有提交的时候,这个时候用另外一个sqlSession读取出来的数据依然是没有添加进去的
System.out.println("提交前");
showCountryListNow();
.commit();
sqlSession.close();
sqlSession
//在sqlSession提交以后,这个时候用另外一个sqlSession读取出来的数据才是有数据的
//注意,两次读取必须用不同的sqlSession,否则会因为一级缓存读取出来的数据都是相同,无刷新的
System.out.println("提交后");
showCountryListNow();
}finally {
}
}
---- test2 ----
DEBUG [main] -==> Preparing: select id,countryName,countryCode from t_country
DEBUG [main] -==> Parameters:
TRACE [main] -<== Columns: id, countryName, countryCode
TRACE [main] -<== Row: 1, 中国, CN
TRACE [main] -<== Row: 2, 美国, US
TRACE [main] -<== Row: 3, 俄罗斯, RU
TRACE [main] -<== Row: 4, 英国, GB
TRACE [main] -<== Row: 5, 法国, FR
TRACE [main] -<== Row: 6, MK世界, MK
TRACE [main] -<== Row: 7, MK世界, MK
TRACE [main] -<== Row: 8, MK世界, MK
DEBUG [main] -<== Total: 8
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
DEBUG [main] -==> Preparing: insert into t_country(countryName,countryCode) values(?,?)
DEBUG [main] -==> Parameters: 我的国(String), 67(String)
DEBUG [main] -<== Updates: 1
提交前
DEBUG [main] -==> Preparing: select id,countryName,countryCode from t_country
DEBUG [main] -==> Parameters:
TRACE [main] -<== Columns: id, countryName, countryCode
TRACE [main] -<== Row: 1, 中国, CN
TRACE [main] -<== Row: 2, 美国, US
TRACE [main] -<== Row: 3, 俄罗斯, RU
TRACE [main] -<== Row: 4, 英国, GB
TRACE [main] -<== Row: 5, 法国, FR
TRACE [main] -<== Row: 6, MK世界, MK
TRACE [main] -<== Row: 7, MK世界, MK
TRACE [main] -<== Row: 8, MK世界, MK
DEBUG [main] -<== Total: 8
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
提交后
DEBUG [main] -==> Preparing: select id,countryName,countryCode from t_country
DEBUG [main] -==> Parameters:
TRACE [main] -<== Columns: id, countryName, countryCode
TRACE [main] -<== Row: 1, 中国, CN
TRACE [main] -<== Row: 2, 美国, US
TRACE [main] -<== Row: 3, 俄罗斯, RU
TRACE [main] -<== Row: 4, 英国, GB
TRACE [main] -<== Row: 5, 法国, FR
TRACE [main] -<== Row: 6, MK世界, MK
TRACE [main] -<== Row: 7, MK世界, MK
TRACE [main] -<== Row: 8, MK世界, MK
TRACE [main] -<== Row: 10, 我的国, 67
DEBUG [main] -<== Total: 9
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
id:10,name:我的国,code:67
那么,sqlSession究竟是什么,我们在test2中可以看出,sqlSession可以看做是事务的边界,只有sqlSession的commit以后,其他的sql连接才能看到这个sqlSession所做成的修改。
2.2 MyBatis的接口映射
代码在这里
package mybatis_test.mapper;
import mybatis_test.model.Country;
import java.util.List;
/**
* Created by fish on 2021/3/24.
*/
public interface CountryMapper {
List<Country> selectAll();
void add(Country country);
void mod(Country country);
void del(Long id);
}
我们创建了一个CountryMapper的接口。其他的和2.1节的一样,建立SqlSessionFactory
private void printCountryList(List<Country> countryList){
for( Country country : countryList){
System.out.printf("id:%s,name:%s,code:%s\n",country.id,country.countryName,country.countryCode);
}
}
private void showCountryList(CountryMapper countryMapper){
List<Country> countryList = countryMapper.selectAll();
printCountryList(countryList);
}
private void addCountry(CountryMapper countryMapper){
= new Country();
Country country .countryCode = "67";
country.countryName = "我的国";
country
.add(country);
countryMapper}
private void modCountry(CountryMapper countryMapper,Long id){
= new Country();
Country country .id = id;
country.countryCode = "88";
country.countryName = "他的国";
country
.mod(country);
countryMapper}
private void delCountry(CountryMapper countryMapper,Long id){
.del(id);
countryMapper}
private void test1(){
System.out.println("---- test1 ----");
= this.sqlSessionFactory.openSession();
SqlSession sqlSession
= sqlSession.getMapper(CountryMapper.class);
CountryMapper countryMapper
try {
showCountryList(countryMapper);
addCountry(countryMapper);
delCountry(countryMapper, 3L);
modCountry(countryMapper, 2L);
showCountryList(countryMapper);
}finally {
.rollback();
sqlSession}
}
与2.1的test1函数一样,我们这个时候用sqlSessionFactory来获取sqlSession,然后用sqlSession来根据CountryMapper接口来生成对应的实现。根据接口生成实现,是通过对接口所在的包名和类名,找到对应的命名空间的xml文件,并且将xml文件的id与方法名一一对应来生成的。值得注意的是,CountryMapper依然与单个的指定的sqlSession绑定。
private void showCountryListNow(){
= this.sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSession2.getMapper(CountryMapper.class);
CountryMapper countryMapper showCountryList(countryMapper);
.close();
sqlSession2}
private void test2(){
System.out.println("---- test2 ----");
= this.sqlSessionFactory.openSession();
SqlSession sqlSession
= sqlSession.getMapper(CountryMapper.class);
CountryMapper countryMapper
try {
showCountryList(countryMapper);
addCountry(countryMapper);
//CountryMapp是与sqlSession绑定的,sqlSession未提交就,CountryMapper所做的修改也不会提交
System.out.println("提交前");
showCountryListNow();
.commit();
sqlSession.close();
sqlSession
//sqlSession提交以后,CountryMapper的修改才会提交
System.out.println("提交后");
showCountryListNow();
}finally {
}
}
因为CountryMapper依然与单个的指定的sqlSession绑定,所以,只有sqlSession执行commit以后,这个接口所做的sql修改才会提交。而且,当这个sqlSession关闭以后,再次调用CountryMapper是会报错的,因为该CountryMapper所依赖的sqlSession早就关闭了。
2.3 集成Spring Boot
代码在这里
/**
* Created by fish on 2021/3/15.
*/
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/Test
spring.datasource.username = root
spring.datasource.password = 1
mybatis.mapper-locations = classpath:mapper/*.xml
mybatis.type-aliases-package=mybatis_test.model
mybatis.config-location=classpath:mybatis-config.xml
logging.level.mybatis_test.mapper=DEBUG
我们直接引用mybatis的starter就可以了,同时配置一下外部的mybatis的外部配置文件。值得注意的是,spring boot默认的日志系统时slf4j的接口,logback的实现。
<?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>
<settings>
<setting name="logImpl" value="SLF4J"/>
<setting name="cacheEnabled" value="false"/>
<settings>
</configuration> </
因此,我们要指定mybatis的日志实现为SLF4J,才会让mybatis的日志输出也用到了spring boot的日志系统。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper">
<select id="selectAll" resultType="Country">
<
select id,countryName,countryCode from t_countryselect>
</insert id="add" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode) values(#{countryName},#{countryCode})insert>
</delete id="del">
<
delete from t_country where id = #{id}delete>
</update id="mod">
<
update t_country set countryName=#{countryName},countryCode=#{countryCode} where id = #{id}update>
</mapper> </
mapper文件依然不变,因为配置文件中指定了mybatis.mapper-locations = classpath:mapper/*.xml,所以它会在启动的时候,自动扫描注册resources文件夹下面的mapper的所有xml文件。
package mybatis_test.mapper;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Created by fish on 2021/3/24.
*/
@Mapper
public interface CountryMapper {
List<Country> selectAll();
void add(Country country);
void mod(Country country);
void del(Long id);
}
然后,我们再定义一个接口,注意,必须要用@Mapper注解。
@Autowired
private CountryMapper countryMapper;
private void printCountryList(List<Country> countryList){
for( Country country : countryList){
.info("id:{},name:{},code:{}",country.id,country.countryName,country.countryCode);
logger}
}
private void showCountryList(){
List<Country> countryList = countryMapper.selectAll();
printCountryList(countryList);
}
private void addCountry(){
= new Country();
Country country .countryCode = "67";
country.countryName = "我的国";
country
.add(country);
countryMapper}
private void modCountry(Long id){
= new Country();
Country country .id = id;
country.countryCode = "88";
country.countryName = "他的国";
country
.mod(country);
countryMapper}
private void delCountry(Long id){
.del(id);
countryMapper}
@Transactional
private void test1(){
System.out.println("---- test1 ----");
showCountryList();
addCountry();
delCountry(3L);
modCountry(2L);
showCountryList();
throw new RuntimeException("throw by me");
}
这个时候,直接注入CountryMapper的接口就能用了,不需要创建sqlSessionFactory,也不需要创建sqlSession,更不需要用sqlSession来创建对应的CountryMapper接口的实现。
@Autowired
;
SqlSessionFactory sqlSessionFactory
private void showCountryListNow(){
= this.sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSession2.getMapper(CountryMapper.class);
CountryMapper countryMapper List<Country> countryList = countryMapper.selectAll();
printCountryList(countryList);
.close();
sqlSession2}
private void test2(){
System.out.println("---- test2 ----");
showCountryList();
//这里的CountryMapper绑定的不是普通的sqlSession,它绑定的是sqlSessionTemplate类
//SqlSessionTemplate的特点是与事务绑定,当没有事务的时候,CountryMapper一个方法执行完毕后会自动commit
//当有事务的时候,就会在事务提交的时候进行commit
//具体看SqlSessionTemplate sqlSessionTemplate;的实现
//参考资料:
// * https://blog.csdn.net/yu_kang/article/details/88941908
// * https://blog.csdn.net/xlgen157387/article/details/79438676
addCountry();
//所以,这里不需要sqlSession.commit操作,在其他线程中就能看到执行了country添加的结果
showCountryListNow();
}
而且,更神奇的是,CountryMapper在2.2节我们探讨过,它是与具体的sqlSession绑定在一起的,只有sqlSession进行commit了,CountryMapper所做的修改才会提交。但是,在Spring Boot的集成,直接执行CountryMapper的方法就会自动commit,我们找不到CountryMapper对应的sqlSession,更不用说要用它来commit了。这是因为,这个CountryMapper绑定的不是DefaultSqlSession,而是SqlSessionTemplate。
SqlSessionTemplate的特点是:
- 无事务状态下(无@Transactional注解),每个方法会自动创建一个SqlSession,并且方法执行完毕后自动commit。因此,你依旧能在一个id下执行多个sql操作。
- 有事务状态(有@Transactional注解),每个方法会沿用一个固定的SqlSession,并且在事务提交的时候,commit对应的单个SqlSession。
这里的代码比较神奇,SqlSessionTemplate的主要实现是依赖TransactionSynchronizationManager的功能,在事务状态下:
- 将首次使用的SqlSession用TransactionSynchronizationManager.setResource方法写进去,下次当前事务的其他方法用sqlSession的时候,用TransactionSynchronizationManager.getResource获取出来的,这样达到了同一个事务下,总是固定用同一个sqlSession的目的。
- 使用TransactionSynchronizationManager.registerSynchronization方法,当事务提交之前的一刻(beforeCommit)回调自己的通知器,将TransactionSynchronizationManager.getResource的sqlSession拿出来,commit即可。
可以看到,TransactionSynchronizationManager可以轻松实现多个数据源的同步提交功能。
最后,谈谈SqlSessionManager与SqlSessionTemplate的不同。SqlSessionManager是线程级的,SqlSessionTemplate是事务级的。SqlSessionManager在同一个线程下总是使用同一个sqlSession。但是,在Web开发中,数据库写入读取操作之间总会穿插着外部第三方接口,耗时计算等业务代码,如果用SqlSessionManager就会导致挂起数据库连接,等待第三方接口返回,等待耗时计算执行完毕,才能释放数据库连接的问题。
3 SQL参数传入
代码在这里
3.1 函数参数名称
@Mapper
public interface CountryMapper2 {
List<Country> selectByCodeAndName(String countryName,String countryCode);
}
select id="selectByCodeAndName" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}select> </
注意,这种想当然地直接使用函数入参的名称是不行的。Java编译代码以后,参数名称都变为arg1,arg2的形式,原来的参数名称已经丢弃了。
3.2 @Param注解
@Mapper
public interface CountryMapper2 {
List<Country> selectByCodeAndNameWithParam(@Param("countryName") String countryName, @Param("countryCode") String countryCode);
}
select id="selectByCodeAndNameWithParam" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}select> </
用@Param注解来传入参数才是正确的。
3.3 参数为类类型
List<Country> selectByCodeAndNameWithClass(CountryCodeAndName param);
select id="selectByCodeAndNameWithClass" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}select> </
int add(Country country);
insert id="add" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryCode,countryName,createTime,modifyTime)VALUES
(#{countryCode},
#{countryName},
#{createTime,jdbcType=TIMESTAMP},
#{modifyTime,jdbcType=TIMESTAMP});insert> </
在添加操作的时候,我们依然可以直接类类型的属性。
3.4 参数为Map类型
当传入的是一个类的时候,我们可以轻松地直接引用类的属性。
int updateByMap(Map<String,Object> map);
update id="updateByMap">
<
update t_country setforeach collection="_parameter" item="val" index="key" separator=",">
<
${key} = #{val}foreach>
</
where id = #{id}update> </
当传入的是map类型的时候,我们可以用固定的参数名称_parameter来获取唯一的入参,这样能引用到map类型了。
int updateByMapAndId(@Param("map") Map<String,Object> map,@Param("id") Long id);
update id="updateByMapAndId">
<
update t_country setforeach collection="map" item="val" index="key" separator=",">
<
${key} = #{val}foreach>
</
where id = #{id}update> </
当入参不止一个,有map类型和基础类型的时候,就不能再通过_parameter来获取了,我们只能用@Param注解来表明参数的名称。
3.5 参数为List类型
List<Country> selectByCountryCodeList(List<String> countryCodeList);
select id="selectByCountryCodeList" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country
where countryCode inforeach collection="list" open="(" close=")" separator=","
< item="countryCode" index="i">
#{countryCode}foreach>
</select> </
我们可以用固定的参数名称list,来指定这个List类型的参数
4 动态SQL语句
代码在这里
4.1 if标签
insert id="insertCheckName" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode)values(if test="countryName != null and countryName.length() != 0">
<
#{countryName},if>
</if test="countryName == null or countryName.length() == 0">
<
"UNKNOWN",if>
</
#{countryCode}
)insert> </
if里面的test属性是特殊的OGNL语法,不好用的地方是没有else标签。
4.2 where标签
select id="selectByWhere" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_countrywhere>
<if test="id != 0">
<
and id = #{id}if>
</if test="countryName != null and countryName.length() != 0">
<
and countryName = #{countryName}if>
</if test="countryCode != null and countryCode.length() != 0">
<
and countryCode = #{countryCode}if>
</where>
</select> </
我们可以在where里面附加多个if标签,就可以做任意参数组合的模糊匹配查询。这个where标签的好处是,每个if里面都可以写一个and语句,即使只有一个匹配时生成的SQL也不会出错。
4.3 set标签
update id="updateByIdSelective">
<
update t_countryset>
<if test="countryName != null and countryName.length() != 0">
<
countryName = #{countryName},if>
</if test="countryCode != null and countryCode.length() != 0">
<
countryCode = #{countryCode},if>
</
id = #{id},set>
</
where id = #{id}update> </
同样地,set标签,相当于代替了select中的where标签。这样就能实现,任意非空属性的单独设值。但是,这个set标签与where标签不同的是,如果没有一个属性匹配的时候,生成的update语句就会缺少set部分内容而失败。因此,我们总是固定在set标签的尾部,加入id=#{id}的内容,而避免这个问题。
4.4 foreach标签
int insertList(List<Country> countryList);
insert id="insertList" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode) VALUESforeach collection="list" item="country" separator=",">
<
(#{country.countryName},#{country.countryCode})foreach>
</insert> </
批量插入的方法,可以遍历一个List来批量插入数据。注意我们使用了useGeneratedKeys=true的设置,因此插入的自增字段会自动赋值到输入的参数countryList的id属性下面。keyProperty指定的是要返回的自增ID要插入到实例的哪个字段上。
4.5 bind标签
package mybatis_test;
import mybatis_test.model.Country;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created by fish on 2021/3/28.
*/
public class StringUtil {
final static Logger logger= LoggerFactory.getLogger(StringUtil.class);
public static void print(Country country){
.info("MyBatis print countryCode:{},countryName:{}",country.countryCode,country.countryName);
logger}
}
insert id="insertListWithPrint" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode) VALUESforeach collection="list" item="country" separator=",">
<
(#{country.countryName},#{country.countryCode})bind name="print" value="@mybatis_test.StringUtil@print(country)"/>
<foreach>
</insert> </
bind语句可以使用ONGL来执行Java的方法,调试的时候特别好用。
5 SQL返回值映射
代码在这里
5.1 ResultType的直接映射
package mybatis_test.model;
import java.util.Date;
/**
* Created by fish on 2021/3/29.
*/
public class People {
public Long peopleId;
public String name;
public String homeAddress;
public String primaryEmail;
public Long countryId;
public Date createTime;
public Date modifyTime;
public String toString(){
return String.format("People{id:%s,name:%s,homeAddress:%s,primaryEmail:%s,countryId:%s}",peopleId,name,homeAddress,primaryEmail,countryId);
}
}
这是People类型,可以用setter,也可以直接用public字段。
package mybatis_test.mapper;
import mybatis_test.model.People;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Created by fish on 2021/3/29.
*/
@Mapper
public interface PeopleMapper {
List<People> selectAll();
}
这是PeopleMapper的接口
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.PeopleMapper">
<select id="selectAll" resultType="people">
<
select people_id,name,home_address,primary_email,countryId,createTime,modifyTime from t_people;select>
</select id="selectByCountryId" resultType="people">
<
select people_id,name,home_address,primary_email,countryId,createTime,modifyTime from t_people where countryId = #{countryId};select>
</mapper> </
这是PeopleMapper的xml实现,返回结果直接用resultType映射,相当于字段的一一映射。注意,sql字段都是下划线命名,但是属性字段是驼峰命名的,但是依然能匹配上。
<?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>
<settings>
<setting name="logImpl" value="SLF4J"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="cacheEnabled" value="false"/>
<settings>
</configuration> </
这是因为我们在mybatis-config.xml中配置了打开mapUnderscoreToCamelCase,这大大简化了开发的代码。
5.2 ResultMap的setter映射
package mybatis_test.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
/**
* Created by fish on 2021/3/23.
*/
/* resultMap也支持属性的直接赋值,绕过setter赋值
public class Country{
public Long countryId;
public String name;
public String code;
public Long continentId;
public Date createTime;
public Date modifyTime;
public String toString(){
return String.format("Country{id:%s,name:%s,code:%s,continentId:%s}",countryId,name,code,continentId);
}
}
*/
public class Country {
final Logger logger = LoggerFactory.getLogger(getClass());
private Long countryId;
public void setCountryId(Long countryId){
this.countryId = countryId;
}
public Long getCountryId(){
return this.countryId;
}
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
private String code;
public void setCode(String code){
this.code = code;
}
public String getCode(){
return this.code;
}
private Long continentId;
public void setContinentId(Long continentId){
.info("setContinentId setter call {}",continentId);
loggerthis.continentId = continentId;
}
public Long getContinentId(){
return this.continentId;
}
private Date createTime;
public void setCreateTime(Date createTime){
this.createTime = createTime;
}
public Date getCreateTime(){
return this.createTime;
}
private Date modifyTime;
public void setModifyTime(Date modifyTime){
this.modifyTime = modifyTime;
}
public Date getModifyTime(){
return this.modifyTime;
}
public String toString(){
return String.format("Country{id:%s,name:%s,code:%s,continentId:%s}",countryId,name,code,continentId);
}
}
一个Country的定义,注意有了setter和getter方法。
package mybatis_test.mapper;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* Created by fish on 2021/3/27.
*/
@Mapper
public interface CountryMapper {
List<Country> selectAll();
}
定义CountryMapper的接口
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper">
<resultMap id="countryMap" type="Country">
<id property="countryId" column="countryId"/>
<result property="name" column="name"/>
<result property="code" column="code"/>
<result property="continentId" column="continentId"/>
<result property="createTime" column="createTime"/>
<result property="modifyTime" column="modifyTime"/>
<resultMap>
</select id="selectAll" resultMap="countryMap">
<
select countryId,name,code,continentId,createTime,modifyTime from t_country;select>
</mapper> </
使用resultMap来指定如何将列映射到字段上面即可
5.3 ResultMap的contructor映射
package mybatis_test.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
/**
* Created by fish on 2021/3/29.
*/
public class Continent {
final Logger logger = LoggerFactory.getLogger(getClass());
private Long continentId;
private String name;
private Date createTime;
private Date modifyTime;
public Continent(Long continentId,String name,Date createTime,Date modifyTime){
.info("continent construct call");
loggerthis.continentId = continentId;
this.name = name;
this.createTime = createTime;
this.modifyTime = modifyTime;
}
public Long getContinentId(){
return this.continentId;
}
public String getName(){
return this.name;
}
public Date getCreateTime(){
return this.createTime;
}
public Date getModifyTime(){
return this.modifyTime;
}
public String toString(){
return String.format("Continent{id:%s,name:%s}",continentId,name);
}
}
定义一个Continent,注意只有getter,没有setter,所有字段通过Continent的构造函数传入,这意味着这个类无法被修改。
package mybatis_test.mapper;
import mybatis_test.model.Continent;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Created by fish on 2021/3/29.
*/
@Mapper
public interface ContinentMapper {
List<Continent> selectAll();
}
这是ContinentMapper的接口
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.ContinentMapper">
<resultMap id="continentMap" type="Continent">
<constructor>
<idArg javaType="Long" column="continentId"/>
<arg javaType="String" column="name"/>
<arg javaType="Date" column="createTime"/>
<arg javaType="Date" column="modifyTime"/>
<constructor>
</resultMap>
</select id="selectAll" resultMap="continentMap">
<
select continentId,name,createTime,modifyTime from t_continentselect>
</select id="selectByContinentId" resultMap="continentMap">
<
select continentId,name,createTime,modifyTime from t_continent where continentId = #{id}select>
</mapper> </
指定resultMap来赋值数值,但是用constructor的方式来传入。注意,这个方法,缺少对函数参数的指定,只能指定函数的类型,要保证参数的顺序,要与构造器的参数顺序一致。
5.4 一对一映射
package mybatis_test.model;
/**
* Created by fish on 2021/3/29.
*/
public class CountryAndContinent extends Country {
private Continent continent;
public void setContinent(Continent continent){
this.continent = continent;
}
public String toString(){
return String.format("CountryAndContinent{%s,continent:%s}",super.toString(),this.continent);
}
}
先定义一个CountryAndContinent类,它包含着一个关联类Continent。
resultMap id="countryAndContinentMap" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndContinent">
<association property="continent" columnPrefix="conti_" resultMap="mybatis_test.mapper.ContinentMapper.continentMap"/>
<resultMap>
</select id="selectAll" resultMap="countryAndContinentMap">
<
select
t_country.countryId ,
t_country.name ,
t_country.code ,
t_country.createTime ,
t_country.modifyTime ,
t_continent.continentId as conti_continentId,
t_continent.name as conti_name,
t_continent.createTime as conti_createTime,
t_continent.modifyTime as conti_modifyTime
from t_country
inner join t_continent on t_country.continentId = t_continent.continentId;select> </
第一种方法是,拉取的时候,直接join,将多个类的数据取出来。然后在resultMap里面,指定association,将conti_开头的列都赋值到continent属性上。
resultMap id="countryAndContinentMap2" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndContinent">
<association property="continent" column="{id=continentId}" select="mybatis_test.mapper.ContinentMapper.selectByContinentId"/>
<resultMap>
</select id="selectAllWithNest" resultMap="countryAndContinentMap2">
<
select countryId,name,code,createTime,modifyTime,continentId from t_country;select> </
第二种方法是,分两次select拉取,先用select拉取一次,然后对于每一个continentId,用selectByContinentId再拉取多一次,赋值到continent属性上。显然,这个方法有N+1的问题,每一个country都需要执行一次selectByContinentId操作
5.5 一对多映射
package mybatis_test.model;
import java.util.List;
/**
* Created by fish on 2021/3/30.
*/
public class CountryAndPeople extends Country{
private List<People> peopleList;
public void setPeopleList(List<People> peopleList){
this.peopleList = peopleList;
}
public String toString(){
return String.format("CountryAndPeople{%s,people:%s}",super.toString(),this.peopleList);
}
}
先定义一个CountryAndPeople,嵌套了一个peopleList的List。这是一个一对多的映射
resultMap id="peopleMap" type="People">
<id property="peopleId" column="people_id"/>
<result property="name" column="name"/>
<result property="homeAddress" column="home_address"/>
<result property="primaryEmail" column="primary_email"/>
<result property="countryId" column="countryId"/>
<result property="createTime" column="createTime"/>
<result property="modifyTime" column="modifyTime"/>
<resultMap>
</resultMap id="countryAndPeopleMap" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndPeople">
<association property="peopleList" columnPrefix="people_" resultMap="peopleMap"/>
<resultMap>
</select id="selectAll" resultMap="countryAndPeopleMap">
<
select
t_country.countryId ,
t_country.name ,
t_country.code ,
t_country.createTime ,
t_country.modifyTime ,
t_people.people_id as people_people_id,
t_people.name as people_name,
t_people.home_address as people_home_address,
t_people.primary_email as people_primary_email,
t_people.countryId as people_countryId,
t_people.createTime as people_createTime,
t_people.modifyTime as people_modifyTime
from t_country
left join t_people on t_people.countryId = t_country.countryId;select> </
第一种方法依然是使用join来一次性全部拉取,然后使用columnPrefix属性,将部分列写入到peopleList属性上。
resultMap id="countryAndContinentMap2" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndPeople">
<association property="peopleList" column="{countryId=countryId}" select="mybatis_test.mapper.PeopleMapper.selectByCountryId"/>
<resultMap>
</select id="selectAllWithNest" resultMap="countryAndContinentMap2">
<
select countryId,name,code,createTime,modifyTime,continentId from t_country;select> </
第二种方法是使用两次select来拉数据,第一次先拉t_country,第二次用selectByCountryId来拉每个country里面的people。显然,这个方法依然有N+1的问题。
6 缓存
代码在这里
6.1 一级缓存
package mybatis_test.model;
import java.io.Serializable;
import java.util.Date;
/**
* Created by fish on 2021/3/23.
*/
public class Country implements Serializable {
public Long id;
public String countryName;
public String countryCode;
public Date createTime;
public Date modifyTime;
}
先定义一个Country类
package mybatis_test.mapper;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* Created by fish on 2021/3/27.
*/
@Mapper
public interface CountryMapper {
List<Country> selectAll();
selectById(@Param("id")Long countryId);
Country selectById2(@Param("id")Long countryId);
Country int insertList(List<Country> countryList);
}
再定义一个CountryMapper
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper">
<select id="selectAll" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country;select>
</select id="selectById" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country where id = #{id};select>
</select id="selectById2" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country where id = #{id};select>
</insert id="insertList" useGeneratedKeys="true" keyProperty="id">
<
insert into t_country(countryName,countryCode) VALUESforeach collection="list" item="country" separator=",">
<
(#{country.countryName},#{country.countryCode})foreach>
</insert>
</mapper> </
注意,selectById与selectById2的方法实现是完全一致的,只是方法名称不同而已。
@Transactional
public void test1(){
.info("------ test1 ------");
logger//在同一个事务里面,同一个方法用同样的参数返回的总是同一个对象
= countryMapper.selectById(1L);
Country country
.countryName = "CC";
country
//这一步没有查询数据库,直接查一级缓存后返回了
= countryMapper.selectById(1L);
Country country2
.info("country pointer : {}, country2 pointer: {}",System.identityHashCode(country),System.identityHashCode(country2));
logger.info("country name: {}, country2 name: {}",country.countryName,country2.countryName);
logger
//这一步有查询数据库,因为selectById和selectById2是不同的方法
= countryMapper.selectById2(1L);
Country country2_2
.info("country pointer : {}, country2_2 pointer: {}",System.identityHashCode(country),System.identityHashCode(country2_2));
logger.info("country name: {}, country2_2 name: {}",country.countryName,country2_2.countryName);
logger
//但是,在同一个事务里面,用其他方法就不会返回这个对象.
//所以,一级缓存总是以方法和参数作为缓存的key
showCountryList();
//添加一个Country,这时候会清空mapper所在的缓存
= new Country();
Country countryNew .countryName = "MK世界";
countryNew.countryCode = "MK";
countryNewList<Country> countriesAdd = new ArrayList<Country>();
.add(countryNew);
countriesAdd.insertList(countriesAdd);
countryMapper
//因为一级缓存为空,所以这次查询会走数据库.
= countryMapper.selectById(1L);
Country country3 .info("country pointer : {}, country3 pointer: {}",System.identityHashCode(country),System.identityHashCode(country3));
logger.info("country name: {}, country3 name: {}",country.countryName,country3.countryName);
logger}
: ------ test1 ------
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?;
==> Parameters: 1(Long)
<== Total: 1
country pointer : 1789282489, country2 pointer: 1789282489
country name: CC, country2 name: CC
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?;
==> Parameters: 1(Long)
<== Total: 1
country pointer : 1789282489, country2_2 pointer: 1949126165
country name: CC, country2_2 name: 中国
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country;
==> Parameters:
<== Total: 10
id:1,name:中国,code:CN,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:2,name:美国,code:US,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:3,name:俄罗斯,code:RU,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:4,name:英国,code:GB,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:5,name:法国,code:FR,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:6,name:MK世界,code:MK,createTime:Tue Mar 30 22:18:21 CST 2021,modifyTime:Tue Mar 30 22:18:21 CST 2021
id:7,name:MK世界,code:MK,createTime:Wed Mar 31 13:32:07 CST 2021,modifyTime:Wed Mar 31 13:32:07 CST 2021
id:8,name:MK世界,code:MK,createTime:Wed Mar 31 13:32:40 CST 2021,modifyTime:Wed Mar 31 13:32:40 CST 2021
id:10,name:我的国,code:67,createTime:Wed Mar 31 14:53:47 CST 2021,modifyTime:Wed Mar 31 14:53:47 CST 2021
id:11,name:MK世界,code:MK,createTime:Mon Apr 12 12:50:40 CST 2021,modifyTime:Mon Apr 12 12:50:40 CST 2021
==> Preparing: insert into t_country(countryName,countryCode) VALUES (?,?)
==> Parameters: MK世界(String), MK(String)
<== Updates: 1
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?;
==> Parameters: 1(Long)
<== Total: 1
country pointer : 1789282489, country3 pointer: 1733947537
country name: CC, country3 name: 中国
这是输出结果,注意,在同一个事务里面,以同一个方法和同一个参数返回的总是同一个对象,不管这个对象有没有提交,而且这种情况下会直接在缓存里面拿,不会去查询数据库。但是,不同方法的同一个参数,即使sql实现是相同的,也总是返回不同的对象。
另外,一级缓存是以单个Mapper为维度的,当这个Mapper里面执行了删除,修改,添加的操作时,总是会清空一级缓存。这个时候以同一个方法和同一个参数执行查询时,就会总是查询数据库,因为一级缓存是空的。
6.2 二级缓存
package mybatis_test.mapper;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Created by fish on 2021/3/31.
*/
@Mapper
public interface CountryMapper2 {
List<Country> selectAll();
}
定义一个CountryMapper2
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper2">
<cache
< eviction="FIFO"
flushInterval="5000"
size="512"
readOnly="false"
/>select id="selectAll" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country;select>
</mapper> </
这是带有二级缓存的设置,它表明缓存大小最大只有512个对象,而且每隔5000秒就会自动清空一次。当缓存满了以后,会以FIFO的方式来删除旧缓存。
public void test2(){
.info("------ test2 ------");
logger
try{
List<Country> countries = countryMapper2.selectAll();
.info("countries select1 {}",countries.size());
logger
List<Country> countries2 = countryMapper2.selectAll();
.info("countries select2 {}",countries2.size());
logger
Thread.sleep(5001,0);
List<Country> countries3 = countryMapper2.selectAll();
.info("countries select2 {}",countries3.size());
logger}catch(Exception e){
.printStackTrace();
e}
}
这是执行代码,注意没有开启事务。
------ test2 ------
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.0
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country;
==> Parameters:
<== Total: 11
countries select1 11
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.5
countries select2 11
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.3333333333333333
==> Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country;
==> Parameters:
<== Total: 11
countries select2 11
Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@13acb0d1: startup date [Mon Apr 12 12:50:57 CST 2021]; root of context hierarchy
Unregistering JMX-exposed beans on shutdown
Unregistering JMX-exposed beans
HikariPool-1 - Shutdown initiated...
HikariPool-1 - Shutdown completed.
这是输出结果,第一次查询以后,后面的都会跳过数据库查询,直接用二级缓存的。当超时以后,二级缓存被清空了,就会去查询数据库。
7 分页插件
代码在这里
package mybatis_test.model;
import java.io.Serializable;
import java.util.Date;
/**
* Created by fish on 2021/3/23.
*/
public class Country implements Serializable {
public Long id;
public String countryName;
public String countryCode;
public Date createTime;
public Date modifyTime;
public String toString(){
return String.format("id:%s,name:%s,code:%s",id,countryName,countryCode);
}
}
定义Country实体,它的特点是需要实现Serializable接口。
package mybatis_test.mapper;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* Created by fish on 2021/3/27.
*/
@Mapper
public interface CountryMapper {
List<Country> selectAll();
}
这是CountryMapper接口,注意接口上没有分页参数。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0 //EN"
>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"mapper namespace="mybatis_test.mapper.CountryMapper">
<select id="selectAll" resultType="Country">
<
select id,countryName,countryCode,createTime,modifyTime from t_country;select>
</mapper> </
这是普通的select实现
public void test1(){
.info("------ test1 ------");
logger
int pageIndex = 1;
int pageSize = 10;
String orderBy = "id asc";
//分页信息
.startPage(pageIndex, pageSize, orderBy);
PageHelper
<Country> countryPageInfo = new PageInfo<Country>(countryMapper.selectAll());
PageInfo
.getList().stream().forEach(System.out::println);
countryPageInfo
//打印分页信息
System.out.println("当前页码:第" + countryPageInfo.getPageNum() + "页");
System.out.println("分页大小:每页" + countryPageInfo.getPageSize() + "条");
System.out.println("数据总数:共" + countryPageInfo.getTotal() + "条");
System.out.println("总页数:共" + countryPageInfo.getPages() + "页");
}
这是执行代码,我们在selectAll之前调用一次PageHelper就能做分页了,相当的简单暴力。
8 总结
MyBatis的特点是:
- 简单,清晰,其实就是一个简单的SQL模板引擎而已,加入了返回值映射,和set,where,if这些便利的标签。
- 繁琐,抽象的程度很低,该写的SQL代码一行都没有少。当遇上简单的CRUD操作时,总是要在xml文件和mapper文件之间不断切换。
但是,MyBatis的设计最糟糕的问题在于缓存:
- 一级缓存只能以单个方法和参数为缓存的key,它无法理解到不同方法返回的可能是同一个实体对象的问题。一级缓存的意义在于,写入操作时的批量性,可以只在事务commit的之前,才对数据库执行多个实体的批量update操作。但是,MyBatis的这个设计大大约束了一级缓存的批量update的能力。
- 先走二级缓存,再走一级缓存。当二级缓存打开的时候,一级缓存失效,这个时候调用同一个方法和参数,会返回不同引用的对象。因为二级缓存返回的对象是反序列化生成出来的。这个基本上是个坑。
- 二级缓存是在事务提交的时候保存到缓存的。所以,如果二级缓存读取出来的数据,在内存中修改了,但又没有对数据库进行update操作的话,会产生意外的错误缓存问题。详情看书本的P186页。
因此,二级缓存的使用场景必须局限在:
- 无事务
- 仅能依赖只读数据+interval缓存刷新来使用,不要使用缓存的通知刷新机制(当遇到UPDATE,INSERT和DELETE时就清空二级缓存,迷惑性很大,这样需要引入cache-ref的机制)
- 有二级缓存的接口,切勿将数据读出来作为写操作的依据。
参考资料:
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!