Mybatis使用总结

1. 前言

本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:

  1. 存储
  2. 传输
  3. Spring机制
  4. 缓存组件

本文属于存储部分的第二篇,Mybatis

2. mybatis

mybatis可以认为是SQL模板,做的工作就是从Java存储对象(DO)到SQL语句以及从SQL返回到JAVA存储对象(DO)的转换工作。它通过一系列的概念将SQL语句的书写、返回变得简单一些。
通过typeHandler简化数据库类型(jdbcType)与Java类型(javaType)之间的转换
通过Dynamic SQL将insert、update等得书写快捷

mybatis与Hibernate相同也是一种ORM框架,有3种基本元素:

  • DO
  • Table
  • Do与Table的mapper

mybatis配置

  • pom

    <dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>1.3.2</version>
    </dependency>
    
  • application.properties中增加配置

    mybatis.mapper-locations=classpath*:mybatis/mapper/*Mapper.xml
    mybatis.config-location=classpath:mybatis/mybatis-config.xml
    mybatis.type-handlers-package=geektime.spring.data.mybatisdemo.handler
    mybatis.configuration.map-underscore-to-camel-case=true
    

    上一个由于配置Mapper文件,下一个是mybatis的配置文件
    这里在resouces下增加一个mybatis目录,用于存放与其相关的xml。

    注意:
    这里有个小坑上一个locations,下一个是location,如果写成 mybatis.config-locations,那mybatis-config.xml就无法生效了。

  • mybatis-config.xml中进行配置

    mybatis的配置项还是很多的,这里有详细的说明

    这里简单配置了一下defaultEnumTypeHandler与typeAliases。

    <?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="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
       </settings>
    
       <typeAliases>
           <typeAlias alias="Long" type="java.lang.Long" />
           <typeAlias alias="HashMap" type="java.util.HashMap" />
           <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
           <typeAlias alias="ArrayList" type="java.util.ArrayList" />
           <typeAlias alias="LinkedList" type="java.util.LinkedList" />
    
           <package name="com.example.domain.entity"/>
       </typeAliases>
    
    </configuration>
    

    解释一下这两个配置,前文说道,Mybatis大多做的是转换工作,typeHandler就是对jdbcType与JavaType进行转换的一个handler,大部分的转换可以免配置,但有些就需要配置,比如对Enum,我们定义的org.apache.ibatis.type.EnumOrdinalTypeHandler,是将Enum转换成对应的Ordinal(顺序)整型数据进行存储。如果不指定,默认为EnumTypeHandler,它是按VARCHAR进行存储。这一点有点像Converter。

    另外一个typeAliases就是Java类型的别名,在写mapper时,一般需要完成的包名来代表一个类,但那样比较啰嗦,于是就发明这这个玩意儿。 通过<package>指明PO对应的包即可。

    mapper过程中,势必会遇到DO与Table类型转换问题,使用TypeHandler进行转换

    另外一条是开启下划线到驼峰的转换

  • sql

    create table t_coffee (
        id bigint not null auto_increment,
        name varchar(255),
        price bigint not null,
        create_time timestamp,
        update_time timestamp,
        primary key (id)
    );
    
  • Entity(DO)

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class Coffee {
        private Long id;
        private String name;
        private Money price;
        private Date createTime;
        private Date updateTime;
    }
    
  • mapper

    mapper有2种实践方式,可以直接写mapper类,在类上写SQL;也可以写mapper,然后在xml中写SQL实现,现实中后一种实践会多一些。

    • 仅Mapper类方式

      @Mapper
      public interface CoffeeMapper {
          @Insert("insert into t_coffee (name, price, create_time, update_time)"
                  + "values (#{name}, #{price}, now(), now())")
          @Options(useGeneratedKeys = true)
          int save(Coffee coffee);
      
          @Select("select * from t_coffee where id = #{id}")
          @Results({
                  @Result(id = true, column = "id", property = "id"),
                  @Result(column = "create_time", property = "createTime"),
                  // map-underscore-to-camel-case = true 可以实现一样的效果
                  // @Result(column = "update_time", property = "updateTime"),
          })
          Coffee findById(@Param("id") Long id);
      }
      
    • Mapper类加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.example.dao.task.ProjectDao">
      
       </mapper>
      

mapper的基本概念

Mybatis就是了转换工作,而转换在这里叫mapper,也就是映射
这里是SQL模板书写的地方,并且与DAO进行关联在一起,算是DAO的一个实现。

  • parameterType与resultType
    paramterType是形参的类型,resultType是返回的Java对象类型

  • resultMap
    在写JDBC时(示例 ),最后的结果是存在一个map里,key值是column名,如果column与Java对象的field完全相同,那么可以很happy的用resultType,但如果不同,尤其在复杂的select语句时,就可以用resultMap来做这种映射。

几个例子:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

这里一定要注意 #{}, 而不是#(),都是坑。

save时如何返回对象的id

<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

useGeneratedKeys自动生成主键,keyProperty指明目标属性。
执行完成后,insertAuthor的形参中就包括了id。

批量插入

<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

链表查询

链表查询有2个标签:<association><collection>,前者用于有一个,后者用于有多个的情况。

  • association例子
    每个博客都只有一个作者

    <select id="selectBlog" resultMap="blogResult">
     select
         B.id            as blog_id,
         B.title         as blog_title,
         B.author_id     as blog_author_id,
         A.id            as author_id,
         A.username      as author_username,
         A.password      as author_password,
         A.email         as author_email,
         A.bio           as author_bio
     from Blog B left outer join Author A on B.author_id = A.id
     where B.id = #{id}
     </select>
    
     <resultMap id="blogResult" type="Blog">
     <id property="id" column="blog_id" />
     <result property="title" column="blog_title"/>
     <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
     </resultMap>
    
     <resultMap id="authorResult" type="Author">
     <id property="id" column="author_id"/>
     <result property="username" column="author_username"/>
     <result property="password" column="author_password"/>
     <result property="email" column="author_email"/>
     <result property="bio" column="author_bio"/>
     </resultMap>
    
  • collection例子

    每个博客可以有多个posts

     <select id="selectBlog" resultMap="blogResult">
         select
             B.id as blog_id,
             B.title as blog_title,
             B.author_id as blog_author_id,
             P.id as post_id,
             P.subject as post_subject,
             P.body as post_body,
             from Blog B
         left outer join Post P on B.id = P.blog_id
         where B.id = #{id}
     </select>
    
    <resultMap id="blogResult" type="Blog">
        <id property="id" column="blog_id" />
        <result property="title" column="blog_title"/>
        <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
    </resultMap>
    
    <resultMap id="blogPostResult" type="Post">
        <id property="id" column="id"/>
        <result property="subject" column="subject"/>
        <result property="body" column="body"/>
    </resultMap>
    

嵌套select

对于oneToMany的情况,可以用嵌套查询。
注意:要查以one的list时,这个方法效率是比较低的

<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

dynamic SQL

刚才在批量插入的时候,使用了foreach,它就是一个dynamic SQL

这里再写一个常用的update的示例:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

在update时,我们经常会遇到为空时,不进行update的操作,就是用 test来完成

mybatisGenerator

mybatisGenerator是mybatis官方自带的代码生成器,根据设置的generator.xml,创建数据库表、生成Model、生成Mapper(xml与interface)。

  • pom

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-core</artifactId>
        <version>1.3.7</version>
    </dependency>
    
  • 配置

    mybatis.mapper-locations=classpath*:/mapper/**/*.xml
    mybatis.type-aliases-package=geektime.spring.data.mybatis.model
    mybatis.type-handlers-package=geektime.spring.data.mybatis.handler
    mybatis.configuration.map-underscore-to-camel-case=true
    
  • generatorConfig.xml

    • generatorConfiguration

    • context

      plugin => jdbcConnection => JavaModelGenerator => sqlMapGenerator => javaClientGenerator => table

      注意配置的顺序

    <generatorConfiguration>
        <context id="H2Tables" targetRuntime="MyBatis3">
            <plugin type="org.mybatis.generator.plugins.FluentBuilderMethodsPlugin" />
            <plugin type="org.mybatis.generator.plugins.ToStringPlugin" />
            <plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
            <plugin type="org.mybatis.generator.plugins.RowBoundsPlugin" />
    
            <jdbcConnection driverClass="org.h2.Driver"
                            connectionURL="jdbc:h2:mem:testdb"
                            userId="sa"
                            password="">
            </jdbcConnection>
    
            <javaModelGenerator targetPackage="geektime.spring.data.mybatis.model"
                                targetProject="./src/main/java">
                <property name="enableSubPackages" value="true" />
                <property name="trimStrings" value="true" />
            </javaModelGenerator>
    
            <sqlMapGenerator targetPackage="geektime.spring.data.mybatis.mapper"
                             targetProject="./src/main/resources/mapper">
                <property name="enableSubPackages" value="true" />
            </sqlMapGenerator>
    
            <javaClientGenerator type="MIXEDMAPPER"
                                 targetPackage="geektime.spring.data.mybatis.mapper"
                                 targetProject="./src/main/java">
                <property name="enableSubPackages" value="true" />
            </javaClientGenerator>
    
            <table tableName="t_coffee" domainObjectName="Coffee" >
                <generatedKey column="id" sqlStatement="CALL IDENTITY()" identity="true" />
                <columnOverride column="price" javaType="org.joda.money.Money" jdbcType="BIGINT"
                                typeHandler="geektime.spring.data.mybatis.handler.MoneyTypeHandler"/>
            </table>
        </context>
    </generatorConfiguration>
    
  • 生成

    一般有3种生成方式:命令行、maven、java程序

    maven命令会更方便一些

pageHelper

pageHelper是辅助处理mybatis的分页的,使分页更加方便

  • pom

    <dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>1.3.2</version>
    </dependency>
    <dependency>
    			<groupId>com.github.pagehelper</groupId>
    			<artifactId>pagehelper-spring-boot-starter</artifactId>
    			<version>1.2.10</version>
    </dependency>
    
  • 配置

    mybatis.type-handlers-package=geektime.spring.data.mybatisdemo.handler
    mybatis.configuration.map-underscore-to-camel-case=true
    
    pagehelper.offset-as-page-num=true
    pagehelper.reasonable=true
    pagehelper.page-size-zero=true
    pagehelper.support-methods-arguments=true
    

    - offset-as-page-num:RowBounds中的offset当成页码来使用

    - resonable:页面小于0

    - page-size-zero: 当limit为0时返回全部

    - support-methods-arguments

  • 使用

    @Mapper
    public interface CoffeeMapper {
        @Select("select * from t_coffee order by id")
        List<Coffee> findAllWithRowBounds(RowBounds rowBounds);
    
        @Select("select * from t_coffee order by id")
        List<Coffee> findAllWithParam(@Param("pageNum") int pageNum,
                                      @Param("pageSize") int pageSize);
    }
    
    • RowBounds

      coffeeMapper.findAllWithRowBounds(new RowBounds(1, 3))
      				.forEach(c -> log.info("Page(1) Coffee {}", c));
      
    • 参数方式

      coffeeMapper.findAllWithParam(1, 3)
      				.forEach(c -> log.info("Page(1) Coffee {}", c));
      

参考

官网

3. Mybatis Plus

Mybatis Plus对Mybatis做了增强,使用体验更加丝滑,感性上看Mybatis与Mybatis Plus一起使用与JPA的体验相差无几。

接口

  • Mapper接口

    Mybatis Plus提供了BaseMapper来对定义的Mapper类进行增强,Mapper接口从它继承,就如同JPA中继承JpaRepository类似,自动增加了很多查询方式,对于JPA是Example,这里是Wrapper方式。

    BaseMapper接口
  • IService接

    除了Mapper,Mybatis Plus也对Service进行了增强

    IService接口

    从使用上看,更喜欢简单的查询从IService直接走,复杂的查询从Mapper从XML原生SQL。

示例

  • QueryWrapper示例

    查询:

    QueryWrapper<SysRole> queryWrapper = new QueryWrapper();
    queryWrapper.lambda().eq(SysRole::getId, roleId);
    List<SysRole> roleList = roleService.list(queryWrapper);
    

    删除:

    trolleyParamService.remove(
        new QueryWrapper<TrolleyParam>()
                               .lambda()
                               .eq(TrolleyParam::getPlanId, plan.getId())
    );
    
  • UpdateWrapper示例

    UpdateWrapper<SysOrg> updateWrapper = new UpdateWrapper<>();
    updateWrapper.lambda()
        .set(SysOrg::getName, updateDto.getName())
        .eq(SysOrg::getId, updateDto.getId());
    orgMapper.update(null, updateWrapper);
    

分页

MybatisPlus对分页的支持很强大,写好普通的SQL,然后mapper中增加IPage相关参数,MybatisPlus通过PaginationInterceptor过滤器自动生成相应分页Mapper类,简单查询是也可以使用它,如下:

@Test
    public void test(){
        QueryWrapper<User>  queryWrapper=new QueryWrapper<>();
        queryWrapper.gt("age",20);
        Page<User>  page=new Page<>(1,2);
        IPage<User> userPage = userMapper.selectPage(page,queryWrapper);
        System.out.println("数据总条数:" + userPage.getTotal());
        System.out.println("总页数:" + userPage.getPages());
        List<User> users = userPage.getRecords();
        for (User user : users) {
            System.out.println("user = " + user);
        }
    }

其他

  • 通用枚举

    这里解决的是DO的枚举属性与Table之间的映射

    public enum GradeEnum {
    
        PRIMARY(1, "小学"),  SECONDORY(2, "中学"),  HIGH(3, "高中");
    
        GradeEnum(int code, String descp) {
            this.code = code;
            this.descp = descp;
        }
    
        @EnumValue	 //标记数据库存的值是code
      	@JsonValue		//标记响应json值
        private final int code;
    }
    

    配置

    mybatis-plus:
        # 支持统配符 * 或者 ; 分割
        typeEnumsPackage: com.baomidou.springboot.entity.enums
    
  • 逻辑删除

    配置增加

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    

    实体类上增加:

    @TableLogic
    private Integer deleted;
    

    Insert时字段在数据库定义默认值(推荐)

  • 自定义ID生成器

4. 总结

在前一节JPA中,简单的查询直接使用JpaRepository提供的接口即可,复杂的查询可以使用原生SQL(我比较喜欢),也可使用JPQL。对比Mybatis+MybatisPlus情况,简单的SQL可以使用MybatisPlus提供的Wrapper接口来实现,复杂的查询自己写Mapper.xml在xml中写原生SQL。两者的使用体验很接近了。

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×