前言
Mybatis是一款优秀的Java ORM框架,它是对传统JDBC编程的增强
-
ORM(Object Relational Mapping)
对象关系映射,就是数据库的表与POJO(plain old Java object)两者映射
-
JDBC(Java Database Connectivity)
Java数据库连接,它是API,定义了Java 程序如何访问数据库。最早在SUN公司在1997发布的JDK1.1就包含它,
java.sql
和javax.sql
这两个包就包含相关类
传统的JAVA程序通过JDBC连接数据库,在使用过程中不免发现,经常需要编写建立连接对象、对数据库结果集的Java对象进行映射等重复代码,mybatis的诞生就是为了简化这些代码的编写
Mybatis的前身是Apache ibatis,在mybatis.jar下面也会发现它的包名是org.apache.ibatis
一、基本使用
下面演示在Springboot中如何使用Mybatis,pom如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
数据库表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
`birthdate` date DEFAULT NULL,
`last_login_time` timestamp NOT NULL,
PRIMARY KEY (`id`)
)
application.yml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password:
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.aabond.demo2022.enity
configuration:
map-underscore-to-camel-case: true
default-fetch-size: 100
default-statement-timeout: 30
User.xml
<?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="com.aabond.demo2022.dao.UserMapper">
<cache/>
<select id="getAll" resultType="User">
select id, username, password, salt, birthdate, last_login_time from user;
</select>
<select id="getUserById" resultType="User" parameterType="integer">
select id, username, password, salt, birthdate, last_login_time from user where id = #{id};
</select>
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into user (username, password, salt, birthdate, last_login_time)
values(#{username}, #{password}, #{salt}, #{birthdate}, #{lastLoginTime})
</insert>
<insert id="insertUserBatch" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into user (username, password, salt, birthdate, last_login_time)
values
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.password}, #{item.salt}, #{item.birthdate}, #{item.lastLoginTime})
</foreach>
</insert>
<update id="updateUser" parameterType="User">
update user
set
username = #{username},
password = #{password},
salt = #{salt},
birthdate = #{birthdate},
last_login_time = #{lastLoginTime}
where id = #{id}
</update>
<update id="updateUserIfNecessary" parameterType="User">
update user
<set>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="salt != null">salt = #{salt},</if>
<if test="birthdate != null">birthdate = #{birthdate},</if>
<if test="lastLoginTime != null">last_login_time = #{lastLoginTime}</if>
</set>
where id = #{id}
</update>
<delete id="deleteOne" parameterType="integer">
delete user where id = #{id}
</delete>
<select id="getUserByCondition" parameterType="map" resultType="User">
select id, username, password, salt, birthdate, last_login_time from user
<where>
<foreach collection="map" index="key" item="value" nullable="true">
AND ${key} = #{value}
</foreach>
</where>
</select>
</mapper>
DAO接口
public interface UserMapper {
List<User> getAll();
User getUserById(Integer id);
int insertUser(User user);
int insertUserBatch(List<User> userList);
int deleteOne(Integer id);
int updateUser(User user);
int updateUserIfNecessary(User user);
List<User> getUserByCondition(@Param("map") Map<String, String> condition);
}
实体类 User
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private String salt;
private LocalDate birthdate;
private LocalDateTime lastLoginTime;
}
测试类
@SpringBootTest
@Transactional
class Demo2022ApplicationTests {
@Autowired
private UserMapper userMapper;
private User user;
private static final Logger logger = LoggerFactory.getLogger(Demo2022ApplicationTests.class);
@BeforeEach
void setUp() {
user = User.builder().username("赵立").password("qiwjqieq1w31").salt("avewe32")
.birthdate(LocalDate.of(2000, 1, 1))
.lastLoginTime(LocalDateTime.of(2022, 2, 2, 2, 2, 2)).build();
userMapper.insertUser(user);
}
@Test
void shouldInsertUser() {
assertThat(user.getId()).isNotNull();
}
@Test
void selectUserTest() {
User one = userMapper.getUserById(user.getId());
assertThat(one).isNotNull();
}
@Test
void updateUserTest() {
user.setUsername("赵立一");
userMapper.updateUser(user);
assertThat(userMapper.getUserById(user.getId())).isEqualTo(user);
}
@Test
void insertBatchUserTest() {
User u1 = User.builder().username("王伟").password("wangwei").salt("qww")
.birthdate(LocalDate.of(1999, 1, 1))
.lastLoginTime(LocalDateTime.of(2022, 2, 2, 12, 12, 12)).build();
User u2 = User.builder().username("陈晨").password("chenchen").salt("ccc")
.birthdate(LocalDate.of(2000, 1, 1))
.lastLoginTime(LocalDateTime.of(2022, 1, 1, 13, 13, 13)).build();
List<User> userList = Lists.list(u1, u2);
userMapper.insertUserBatch(userList);
assertThat(userMapper.getUserById(u1.getId())).isEqualTo(u1);
assertThat(userMapper.getUserById(u2.getId())).isEqualTo(u2);
}
@Test
void shouldSelectUserByCondition() {
User u1 = User.builder().username("王伟").password("wangwei").salt("qww")
.birthdate(LocalDate.of(1999, 1, 1))
.lastLoginTime(LocalDateTime.of(2022, 2, 2, 12, 12, 12)).build();
userMapper.insertUser(u1);
Map<String, String> conditions = new HashMap<>();
conditions.put("username", "王伟");
conditions.put("birthdate", "1999-01-01");
List<User> userByCondition = userMapper.getUserByCondition(conditions);
assertThat(userByCondition.size()).isEqualTo(1);
}
}
二、application属性配置
在application.yml中可以配置mybatis的属性
常用的有
-
config-location
配置xml文件位置
-
mapper-locations mapper
mapper文件所在位置
-
type-aliases-package
给实体别名
-
configuration.map-underscore-to-camel-case
配置下划线到驼峰命名
三、XML 文件
MyBatis可以通过xml文件来配置 MyBatis 行为的设置和属性信息。
像上面在SpringBoot中可以在application.yml配置,同样也可以在XML中配置,最终这个是由org.apache.ibatis.session.Configuration
这个配置类控制MyBatis各项属性
具体信息官方文档:
四、动态SQL
mybatis强大的特性之一动态SQL,能够帮助程序员解决很多头疼的Sql拼接问题
-
if
这个是最常用的,通过test里的条件来判断,test里面的条件是OGNL表达式,具体语法可以查看https://commons.apache.org/proper/commons-ognl/language-guide.html
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
-
choose (when, otherwise)
choose是用来支持多个选择选其一,类似于java中switch语句
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose> </select>
-
trim (where, set)
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
上面在使用where时,有时会遇到后面的条件都不满足,那么where 关键字就会显的多于。还有一种情况,只满足第二项,那时AND也显得多余。
这时可以使用where标签,它会当没有内容时不会显示where ,或者当前面是 “AND” 或 “OR”,where 也会将它们去除。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG <where> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </where> </select>
在使用update set时也会遇到类似的问题,那可以直接使用set标签,会删除多余的逗号。
上述where和set其实都可以用trim实现,通过trim可以自定义各种其他实现
<trim prefix="WHERE" prefixOverrides="AND |OR "> <!-- 等价<where></where> --> </trim> <trim prefix="SET" suffixOverrides=","> <!-- 等价<set></set> --> </trim>
自定义一个trim用来实现动态插入。
<insert id="insertSelective"> insert into user <trim prefix="(" suffix=")" suffixOverrides="," > <if test="firstName != null" > first_name, </if> <if test="lastName != null" > last_name, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides="," > <if test="firstName != null" > #{firstName}, </if> <if test="lastName != null" > #{lastName}, </if> </trim> </insert>
-
foreach
foreach用来遍历集合(list, array, set, map)
- item 表示集合中的元素,如果是map就表示value
- index 表示集合遍历时的索引,如果是map就表示key
- collection 表示要遍历的集合,如果这个集合是某个类内的属性,那么只需要写明属性名。如果简单的是个list,那么就只要填入list就可以,如果是个map,那么需要用@Param指定
- open 和 close 表示 前缀和后缀,集合为空就不会显示
- separator 表示集合遍历时的分隔符
- nullable 表示是否接受集合为null,false时集合为null会抛出异常,而true会直接跳过。
<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P <where> <foreach item="item" index="index" collection="list" open="ID in (" separator="," close=")" nullable="true"> #{item} </foreach> </where> </select>
map例子
List<User> getUserByCondition(@Param("map") Map<String, String> condition);
<select id="getUserByCondition" parameterType="map" resultType="User"> select id, username, password, salt, birthdate, last_login_time from user <where> <foreach collection="map" index="key" item="value" > AND ${key} = #{value} </foreach> </where> </select>
五、缓存
-
一级缓存
Mybatis默认情况下,会自动开启一级缓存,这个缓存是session级别,在一个SqlSession下所有查询结果都会被缓存,可以通过设置
localCacheScope=statement
关闭一级缓存,每次执行完statement之后都会清空一级缓存在执行select完毕后,如果执行insert/update/delete操作,一级缓存会清空。在同一个SqlSession下不管是哪个mapper的非查询都会导致缓存被清除。
-
二级缓存
二级缓存默认情况下不开启,需要手动开启,这个是SqlSessionFactory 级别的。这个可以通过
configuration.cache-enabled=true
开启 ,这个是总开关,还需要在mapper文件中加入<cache />
来开启,并且所关联的实体类需要实现Serializable
接口二级缓存和mapper相关,在mapper文件内所有select都会被缓存, insert、update 和 delete 则会刷新缓存,默认缓存1024个引用,并使用LRU来淘汰多于的缓存。这些默认的选项可以被修改,具体属性可以查看文档
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
不建议使用MyBatis自带的二级缓存,它的弊端很明显,是基于application为生命周期的,范围是按照每个mapper文件对应一个缓存来存贮和维护,同一个mapper的执行结果放到一个缓存对象中,当这个mapper文件中执行了非select语句,整个namespace中的缓存全部清除掉。
只能在只有单表操作的mapper使用缓存,还要确保该表所有操作都在一个mapper下,否则会造成脏数据
六、源码及流程分析
-
从@MapperScan开始,在源码中可以看到注入了``MapperScannerRegistrar`这个类
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) @Repeatable(MapperScans.class) public @interface MapperScan { ... }
在
MapperScannerRegistrar
这个类中, 从下面这段代码可以看出将MapperScannerConfigurer
这个类注入容器void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); ... }
在
MapperScannerConfigurer
中, 从下面这段代码可以看到又将ClassPathMapperScanner
实例化public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); ... }
而
ClassPathMapperScanner
的doscan方法实现了扫描了mapper文件,将这些mapper接口的beanDefinition配置好,正如注释所说,将这些mapper接口的类型设置为MapperFactoryBeanprivate void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { ... // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59 try { // for spring-native definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName)); } catch (ClassNotFoundException ignore) { // ignore } definition.setBeanClass(this.mapperFactoryBeanClass); ... }
在使用@Autowire注入mapper接口时,spring容器就会调用从beanDefinitions获取mapper接口的实际类型,而且MapperFactoryBean从名字上可以看出是FactoryBean的实现类,spring容器会调用getObject获取实际的类
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
...
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
...
}
从上面getObject这个方法里可以看到需要获取SqlSession,这个SqlSession从父类可以看到实际是SqlSessionTemplate,而这个实际由
MybatisAutoConfiguration加载
- 查看mybatis-springboot-starter-autoconfigure这个包下的spring.factories这个文件,这里面配置了需要自动加载的类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
第一个是跟脚本语言驱动相关的,暂时不表,重点是第二个类MybatisAutoConfiguration
,有一些会注入IOC容器的类
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
...
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
...
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
...
}
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
...
}
}
而在MapperFactoryBean中SqlSessionTemplate会从这里注入,接下来,依次从getMapper方法就是Configuration类的方法,MapperRegistry获取mapperProxyFactory,最终由mapperProxyFactory通过JDK动态代理获取mapper接口的实现类
public class MapperProxyFactory<T> {
...
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
...
}
七、运行错误和解决方法
-
java.lang.AbstractMethodError: Method com/mysql/jdbc/JDBC4ResultSet.getObject(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; is abstract
之前使用的mysql驱动版本为5.1.8, 太旧的版本,更换为最新驱动5.1.47。
出现
java.lang.AbstractMethodError
这个错误多半是版本问题 -
Error instantiating class ....... with invalid types Cause: java.lang.IllegalArgumentException: argument type mismatch
出现的原因是实体类使用lombok的@Data和@Builder注解,没有生成无参构造方法,有下面两种解决方法:
- 手动加上无参构造方法,并加上@Tolerate使之忽略
- 在实体类上加上
@AllArgsConstructor
,@NoArgsConstructor
两个注解