mybatis - 通用mapper

作为持久层的 ORM 框架, 目前在国内主流之一就是 MyBatis, 学会用它, 用好它肯定是必备的功课

我会主要从下面几个方面入整理本篇博客

  1. 快速搭建快发环境
  2. 常见的注解
  3. 怎么玩?

一. 快速搭建开发环境#

小插曲, 添加测试模块的时候, 引入 junit 模块和 spring-boot-text-starter 模块有先顺序, 不然 ide 会报错...

坐标

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/><!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--通用mapper启动器-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.3</version>
        </dependency>
        <!--分页助手-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <!--测试-->
        <!--先添加 junit  再添加下面的text  stater-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>text</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
  • 另外插一嘴 ---mysql 连接的版本适配

我现在用的云主机 docker 上的官方版 mysql, 版本比较新, 因此我的调整版本到 8以上, 不然会报错说什么

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could

配置文件:

  • 主要是配置数据库的连接, 如果我们使用的是通用 mapper,Mybatis 可以做到零配置
server:
  port: 8089
spring:
  application:
    name: text-mybatis
  datasource:
    url: jdbc:mysql://211.159.XXX.XXX:8888/changwu?serverTimezone=UTC&useUnijava=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 2424zcw..
    driver-class-name: com.mysql.jdbc.Driver
#输出sql
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

启动类

  • 在这里告诉通用 mapper 我们的 mapper 包路径
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.changwu.mapper")
public class MybatisApp {
    public static void main(String[] args) {
        SpringApplication.run(MybatisApp.class);
    }
}

编写 Mapper

  • 让我们自己的 Mapper 继承通用 mapper, 泛型是实体类名
  • 给它实体类名, 就相当与告诉它了我们的表里的字段, 不让他怎么会知道如何动态生成 sql ?
import tk.mybatis.mapper.common.Mapper;
public interface MyMapper extends Mapper<MyBrand> {}

ok, 到现在环境就搭建好了


二. 常见的注解#

  • 这里的注解主要是作用在实体类上的注解, 当我们撸起袖子写代码的时候, 被给它难住喽, 尴尬

情景 1: 对于实体类

标记他对应那张表

import javax.persistence.Table;
@Table(name = "tb_brand")

情景 2: 对于主键

大多数情况主键一般都叫 id,bigint 类型, 没什么超级特殊的情况我们都希望他可以自己增长, 下面两个注解都可以做到这件事, 但是前提表 id 属性必须设置成 autoincreament , 不然报错说,id 不能不写

  • 注解 1:
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/*
*  使用的是 jpa 的策略生成器
*JPA 提供的四种标准用法为 TABLE,SEQUENCE,IDENTITY,AUTO. 
* TABLE:使用一个特定的数据库表格来保存主键。 
* SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列
* IDENTITY:主键由数据库自动生成(主要是自动增长型) 
* AUTO:主键由程序控制。
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) 
  • 注解 2:

直接使用 Mybatis 的原生注解

@Id
@KeySql(useGeneratedKeys = true)
private Long id;

情景 3:

我们都知道 springMVC 会把前端发送过来的 json 转化为我们的 javaBean, 因此我们的 javaBean 里面的属性名和前端发送的那个对象的属性名要意义对应的, 这是规范!

设想, 加入前端一顿收集数据, 把商品的品牌信息和库存信息都发送过来了, 只有关于品牌的 javabean, 数据接受不全怎么办? 没关系, 下面的注解可以搞定

  • @Transient (意味短暂的) 告诉 mapper 他不是 PO(持久层对象,Persistence Object) 的属性, 而且,springMvc 还会把信息关于库存的信息, 封装进去
@Transient

情景 4:

我们的 pojo 属性名, 和数据库中的关键字重名怎么办?

@Column() 可以解决 数据库中的字段为数据库的关键字的问题

@Column(name="`numeric`")  //
private Boolean numeric;   // 是否是数字类型

情景 5 :

数据表中 tingint(1) 对应的 javaBean 的属性咋写?

tingint(1) // 它是 boolean 的同义词

情景 5:

关于 VO , 如何选择性的返回给前端 javaBean 的属性? 就比如说我们查询有没有这个用户, 总不至于把密码, 私人信息一块返回给前端吧?

import com.fasterxml.jackson.annotation.JsonIgnore;
@JsonIgnore

它是 jackson 的注解

  <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>RELEASE</version>
        <scope>compile</scope>
    </dependency>

三. 怎么玩?#

 下面就是介绍常用的 mapper 的 API

一. CRUD#

  • 两种新增
// 会有选择性的新增, 智能判断 brand 里面有没有空的字段, 有值, 用这个值, 没有值而使用数据库的默认值
insertSelective(brand) 

// 假如前端提交过来的 brand 里面的数据是全的, 用词方法
brandMapper.insert(brand);
  • 简单查询
/**
 * 根据实体中的属性值进行查询,查询条件使用等号
 * @param record
 * @return
 */
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> select(T record);


/**
 * 根据实体中的属性值进行查询,查询条件使用等号
 * @param record
 * @return
 */
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> select(T record);


/**
 * 查询全部结果
 * @return
 */
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> selectAll();


/**
 * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常,查询条件使用等号
 * @param record
 * @return
 */
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
T selectOne(T record);


/**
 * 根据实体中的属性查询总数,查询条件使用等号
 * @param record
 * @return
 */
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
int selectCount(T record);
  • 简单删除
/**
 * 根据主键字段进行删除,方法参数必须包含完整的主键属性
 * @param key
 * @return
 */
@DeleteProvider(type = BaseDeleteProvider.class, method = "dynamicSQL")
int deleteByPrimaryKey(Object key);


/**
 * 根据实体属性作为条件进行删除,查询条件使用等号
 * @param record
 * @return
 */
@DeleteProvider(type = BaseDeleteProvider.class, method = "dynamicSQL")
int delete(T record);

  • 如何玩修改?

修改主要用到下面这个 api

/**
 * 根据主键更新属性不为 null 的值
 * @param record
 * @return
 */
@UpdateProvider(type = BaseUpdateProvider.class, method = "dynamicSQL")
int updateByPrimaryKeySelective(T record);

看到这个方法有没有觉得很爽? 它根据主键, 修改不为 null 的属性, 意思就是说, 我们把不需要修改的地方设置为 null 就好了
, 这样看, 前面需要我们一顿整, 整啥呢? 结合实际的业务逻辑, 把前端给交过来的数据最新的数据放到我们的 bean 中直接修改, 把不需要修改的属性设置为 null, 有属性可能需要直接删除, 有的属性可能要去别的表中查询 (别忘了加上事务 @Transactional)

/**
 * 根据主键更新实体全部字段,null 值会被更新
 * @param record
 * @return
 */
@UpdateProvider(type = BaseUpdateProvider.class, method = "dynamicSQL")
int updateByPrimaryKey(T record);

这个方法和上面的神似

二 . 分页, 过滤 (模糊查询), 搜索#

API

mapper.selectByExample(example)

参照下面原生的 sql. 拼接查询条件

 select * from 表名
        where name like '% X %' or  letter== 'x'
        order by  id desc

逻辑

public VO queryBrandByPage(Integer page, String key, Integer rows, String sortBy, Boolean desc) {
    // 分页  -- 使用分页助手, 在我们真正查询之前, 调用这个下面的方法, 开启分页查询, 他很智能, 会用 mybatis 的拦截器
    // 对接下来要执行查询的 sql 进行拦截, 自动的在其后面拼接  limit 语句
    PageHelper.startPage(page,rows); // 当前页码, 每页显示的数目

    // 过滤 --key     (StringUtils 用的是 comment lang3 下面的)
    // 过滤条件,key 是前端用户传递进来的, 可能仅仅是一个 小米,  也可能是 小 ---  模糊查询
   /*   select * from tb_brand
    where name like '% X %' or  letter== 'x'
    order by  id desc
    */
    Example example = new Example(Brand.class);
    if(StringUtils.isNotBlank(key)){  // 不为空, 过滤
       example.createCriteria().orLike("name","%"+key+"%").andEqualTo("letter",key.toUpperCase());
      //  example.createCriteria().orLike("name","%"+key+"%").or
    }

    // 排序
    if(StringUtils.isNotBlank(sortBy)) { // 传递进来的排序不为空, 设置我们的排序条件
        // 上面的 order by 可以帮我们生成, 但是后面的 id  desc 需要我们自己写
        // String orderByClause = "id desc" ; 写死了
        String orderByClause = sortBy + (desc ? "DESC" : "ASC");  // 坑坑坑   注意要加上 空格 不然拼接完了 就是 orderBy idASC 而不是 orderBy id ASC
        example.setOrderByClause(orderByClause);
    }

    // 查询 获取到 list , 其实是  当前页的数据 page 对象
    List<Brand> list = brandMapper.selectByExample(example);
    if (CollectionUtils.isEmpty(list)){
        throw new Exception ;
    }

   // 解析 List
    PageInfo<Brand> pageInfo = new PageInfo<>(list);

    return new PageResult<Brand>(pageInfo.getTotal(),list);
}

三 . 自定义 sql#

Mybatis 只能针对单表为我们生成 sql, 如果我们的需求是跨表操作, 比如说涉及到两张表, 我们就得去 mapper 里面自己写 sql

原生 sql

select * from tb_brand b
inner join tb_category_brand cb on b.id=cb.brand_id  -- 去笛卡尔积
where cb.category_id= ? ;

mapper

? 用 #{id} 取代

@Select("select * from tb_brand b inner join tb_category_brand cb on b.id=cb.brand_id where cb.category_id=#{cid}")
  List<Brand>  queryBrandByCid(@Param("cid") Long cid);

四 . 拓展包 -- 批量操作#

  • 批量查询, 批量删除

注意他的包啊!

import tk.mybatis.mapper.additional.idlist.IdListMapper;
public interface Mapper extends Mapper<Category> , IdListMapper<Category,Long> {}
/**
 * 根据主键字符串进行查询,类中只有存在一个带有@Id注解的字段
 * @param idList
 * @return
 */
@SelectProvider(type = IdListProvider.class, method = "dynamicSQL")
List<T> selectByIdList(@Param("idList") List<PK> idList);

/**
 * 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段
 * @param idList
 * @return
 */
@DeleteProvider(type = IdListProvider.class, method = "dynamicSQL")
int deleteByIdList(@Param("idList") List<PK> idList);

  • 批量插入:
import tk.mybatis.mapper.additional.insert.InsertListMapper;
public interface BaseMapper<T> extends InsertListMapper<T> {}


@RegisterMapper
public interface InsertListMapper<T> {
@InsertProvider(
    type = InsertListProvider.class,
    method = "dynamicSQL"
)
int insertList(List<? extends T> var1);
}

抽取出一个 baseMapper, 添加 @RegisterMapper 它才会被扫描到生效

import tk.mybatis.mapper.additional.idlist.IdListMapper;
import tk.mybatis.mapper.additional.insert.InsertListMapper;
import tk.mybatis.mapper.annotation.RegisterMapper;
import tk.mybatis.mapper.common.Mapper;

@RegisterMapper
public interface BaseMapper<T> extends IdListMapper<T,Long>,Mapper<T>, InsertListMapper<T> {
}