SpringBoot小结之MyBatis

25 篇文章 3 订阅
18 篇文章 0 订阅

前言

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.sqljavax.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下,否则会造成脏数据

六、源码及流程分析

  1. 从@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接口的类型设置为MapperFactoryBean

    private 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加载

  1. 查看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);
  	}
    ...
}
registerBeanDefinitions
new
processBeanDefinitions
Bean
Bean
MapperScannerRegistrar
MapperScannerConfigurer
ClassPathMapperScanner
MapperFactoryBean
MybatisAutoConfiguration
SqlSessionTemplate
SqlSessionFactory
Configuration
MapperRegistry
MapperProxyFactory

七、运行错误和解决方法

  1. 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这个错误多半是版本问题

  2. Error instantiating class ....... with invalid types Cause: java.lang.IllegalArgumentException: argument type mismatch

    出现的原因是实体类使用lombok的@Data和@Builder注解,没有生成无参构造方法,有下面两种解决方法:

    1. 手动加上无参构造方法,并加上@Tolerate使之忽略
    2. 在实体类上加上@AllArgsConstructor@NoArgsConstructor两个注解

参考

  1. http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
  2. https://mybatis.org/mybatis-3/zh/getting-started.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值