基于 mybatis-mapper/provider 核心部分实现的基础的增删改查操作,提供了一个核心的 io.mybatis.mapper.BaseMapper 接口和一个 预定义io.mybatis.mapper.Mapper 接口,BaseMapper 接口定义如下:

/**
 * 基础 Mapper 方法,可以在此基础上继承覆盖已有方法
 *
 * @param <T> 实体类类型
 * @param <I> 主键类型
 * @author liuzh
 */
public interface BaseMapper<T, I extends Serializable>
    extends EntityMapper<T, I>, ExampleMapper<T, Example<T>>, CursorMapper<T, Example<T>> {

  /**
   * Example 查询封装
   */
  default ExampleWrapper<T, I> wrapper() {
    return new ExampleWrapper<>(BaseMapper.this, example());
  }

  /**
   * 根据主键更新实体中不为空的字段,强制字段不区分是否 null,都更新
   * <p>
   * 当前方法来自 {@link io.mybatis.mapper.fn.FnMapper},该接口中的其他方法用 {@link ExampleMapper} 也能实现
   *
   * @param entity            实体类
   * @param forceUpdateFields 强制更新的字段,不区分字段是否为 null,通过 {@link Fn#of(Fn...)} 创建 {@link Fn.Fns}
   * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @UpdateProvider(type = FnProvider.class, method = "updateByPrimaryKeySelectiveWithForceFields")
  int updateByPrimaryKeySelectiveWithForceFields(@Param("entity") T entity, @Param("fns") Fn.Fns<T> forceUpdateFields);

  /**
   * 根据指定字段集合查询:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> List<T> selectByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return selectByExample(example);
  }

  /**
   * 根据指定字段集合删除:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> int deleteByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return deleteByExample(example);
  }

}

这个接口展示了好几个通用方法的特点:

  1. 可以继承其他通用接口
  2. 可以直接复制其他接口中的通用方法定义
  3. 可以使用 Java8 默认方法灵活实现通用方法

在看 Mapper 接口:

/**
 * 自定义 Mapper 示例,这个 Mapper 基于主键自增重写了 insert 方法,主要用作示例
 * <p>
 * 当你使用 Oracle 或其他数据库时,insert 重写时也可以使用 @SelectKey 注解对主键进行定制
 *
 * @param <T> 实体类类型
 * @param <I> 主键类型
 * @author liuzh
 */
public interface Mapper<T, I extends Serializable> extends BaseMapper<T, I> {

  /**
   * 保存实体,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  //@SelectKey(statement = "SELECT SEQ.NEXTVAL FROM DUAL", keyProperty = "id", before = true, resultType = long.class)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(T entity);

  /**
   * 保存实体中不为空的字段,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  //@SelectKey(statement = "SELECT SEQ.NEXTVAL FROM DUAL", keyProperty = "id", before = true, resultType = long.class)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  @InsertProvider(type = EntityProvider.class, method = "insertSelective")
  int insertSelective(T entity);

}

这个接口中通过重写继承接口对主键进行了设置,除非你系统正好使用自增的 id 字段作为主键,否则不应该继承 Mapper 接口使用,应该使用 BaseMapper 作为基础。这个接口主要体现了一个特点: 4. 可以重写继承接口的定义

除了上面已经提到的4个特点外,在下面内容中,还能看到一个特点,5. 那就是一个 provider 实现,通过修改接口方法的返回值和入参,就能变身无数个通用方法,通用方法的实现极其容易。

下面开始详细介绍这些特性。

# 2.1.1 继承其他通用接口

上面接口定义中,继承了 EntityMapper, ExampleMapperCursorMapper 接口。这些接口中定义了大量的通用方法, 通过继承使得 BaseMapper 接口获得了大量的通用方法,通过继承可以组合不同类别的方法。 你可以以 BaseMapper 为基础创建自己的基类接口,也可以完全自己创建集成 EntityMapper 等接口来选择需要的通用方法。

提供的最基础的接口可以通过 2.2~2.7 来了解其中具体的方法。

# 2.1.2 复制其他接口中的通用方法定义

这是最灵活的一点,在 BaseMapper 中直接复制了 FnMapper 的一个方法:

/**
 * 根据主键更新实体中不为空的字段,强制字段不区分是否 null,都更新
 * <p>
 * 当前方法来自 {@link io.mybatis.mapper.fn.FnMapper},该接口中的其他方法用 {@link ExampleMapper} 也能实现
 *
 * @param entity            实体类
 * @param forceUpdateFields 强制更新的字段,不区分字段是否为 null,通过 {@link Fn#of(Fn...)} 创建 {@link Fn.Fns}
 * @return 1成功,0失败
 */
@Lang(Caching.class)
@UpdateProvider(type = FnProvider.class, method = "updateByPrimaryKeySelectiveWithForceFields")
int updateByPrimaryKeySelectiveWithForceFields(@Param("entity") T entity, @Param("fns") Fn.Fns<T> forceUpdateFields);

这就是完全的复制粘贴,利用这一点,你可以不用 BaseMapper 接口作为自己的基类接口,你可以定义一个自己的接口,复制粘贴自己的需要的通用方法作为基础接口, 例如一个 GuozilanMapper 示例如下:

public interface GuozilanMapper<T> {
  
  /**
   * 保存实体
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(T entity);

  /**
   * 根据主键查询实体
   *
   * @param id 主键
   * @return 实体
   */
  @Lang(Caching.class)
  @SelectProvider(type = EntityProvider.class, method = "selectByPrimaryKey")
  Optional<T> selectByPrimaryKey(Long id);
}

只要继承了上面的接口,你就直接拥有了这两个基础方法。

使用这种方式可以自定义一些自己项目需要用到的不同类别的通用接口,例如,如果你有大量实体都没有主键,默认的 BaseMapper<T, I> 就不太适合, 此时你可以自己创建一个 NoIdMapper<T>,把除了主键操作方法外的其他方法(有选择的)都拷过来,就形成了符合自己实际需要的通用 Mapper。

推而广之之后,还有更绝的用法,不继承接口,或者基础接口没有某个方法,直接复制注解过来,不需要自己写 XML:

public interface UserMapper {

   /**
    * 保存实体
    *
    * @param entity 实体类
    * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(User entity);
}

你不需要任何具体的 SQL,上面的 insert 方法就可以直接使用了。

# 2.1.3 使用 Java8 默认方法灵活实现通用方法

BaseMapper 接口中,利用现有的 Example 方法,实现了两个非常常用的通用方法:

/**
 * 根据指定字段集合查询:field in (fieldValueList)
 * <p>
 * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
 *
 * @param field          字段
 * @param fieldValueList 字段值集合
 * @param <F>            字段类型
 * @return 实体列表
 */
default <F> List<T> selectByFieldList(Fn<T, F> field, List<F> fieldValueList) {
  Example<T> example = new Example<>();
  example.createCriteria().andIn((Fn<T, Object>) field, fieldValueList);
  return selectByExample(example);
}

/**
 * 根据指定字段集合删除:field in (fieldValueList)
 * <p>
 * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
 *
 * @param field          字段
 * @param fieldValueList 字段值集合
 * @param <F>            字段类型
 * @return 实体列表
 */
default <F> int deleteByFieldList(Fn<T, F> field, List<F> fieldValueList) {
  Example<T> example = new Example<>();
  example.createCriteria().andIn((Fn<T, Object>) field, fieldValueList);
  return deleteByExample(example);
}

这两个方法可以直接根据某个字段值的集合进行批量查询或者删除,用法示例如下:

List<User> users = mapper.selectByFieldList(User::getUserName, Arrays.asList("张无忌", "赵敏", "周芷若"));
mapper.deleteByFieldList(User::getUserName, Arrays.asList("张无忌", "赵敏", "周芷若"));

除了这个例子外,还有一段 EntityMapper 被注释的示例:

/**
 * 根据实体字段条件分页查询
 *
 * @param entity    实体类
 * @param rowBounds 分页信息
 * @return 实体列表
 */
List<T> selectList(T entity, RowBounds rowBounds);

/**
 * 根据查询条件获取第一个结果
 *
 * @param entity 实体类
 * @return 实体
 */
default Optional<T> selectFirst(T entity) {
  List<T> entities = selectList(entity, new RowBounds(0, 1));
  if (entities.size() == 1) {
    return Optional.of(entities.get(0));
  }
  return Optional.empty();
}

/**
 * 根据查询条件获取指定的前几个对象
 *
 * @param entity 实体类
 * @param n      指定的个数
 * @return 实体
 */
default List<T> selectTopN(T entity, int n) {
  return selectList(entity, new RowBounds(0, n));
}

合理的通过 Java8 的默认方法,能够实现海量的通用方法。至于那些是真正需要用到的通用方法,就需要根据自己的需要来选择,因此虽然上面的方法能通用, 但是在缺乏频繁使用场景的情况下,BaseMapper 接口并没有接纳这几个方法。

特别注意

上面示例中 List<T> selectList(T entity, RowBounds rowBounds); 没有添加 @SelectProvider 注解, 这是因为 MyBatis 中不允许出现相同名称的方法,同时对于 RowBounds 参数有特殊处理, 这个方法会直接复用List<T> selectList(T entity);方法,这个方法已经有了 @SelectProvider 注解配置。

# 2.1.4 重写继承接口的定义

EntityMapper 中有 insert 方法定义如下:

/**
 * 保存实体
 *
 * @param entity 实体类
 * @return 1成功,0失败
 */
@Lang(Caching.class)
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(T entity);

这个定义没有处理主键,需要自己设置好主键后调用该方法新增数据。

特别注意

在 2.x 版本之后支持在实体上配置主键策略,因此在实体配置主键策略的情况下,这个方法可以直接使用。 主键策略示例如下:

@Entity.Table("user")
public class User {
  @Entity.Column(value = "user_id", id = true, useGeneratedKeys = true)
  private Long   userId;

当调用 insert(user) 方法的时候会自动处理主键,而且也可以避免主键名称必须固定为统一名称的问题。

如果我使用的 MySql 自增怎么办?主键null也能直接保存,但是不回写。

如果使用 Oracle 序列怎么办?直接用这个方法是没有办法的。

因为可以 重写继承接口的定义,所以可以支持所有 MyBatis 本身能支持的所有主键方式。

Mapper 中,覆盖定义如下:

/**
 * 保存实体,默认主键自增,并且名称为 id
 * <p>
 * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
 *
 * @param entity 实体类
 * @return 1成功,0失败
 */
@Override
@Lang(Caching.class)
@Options(useGeneratedKeys = true, keyProperty = "id")
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(T entity);

首先 @Override 是重写父接口定义,然后和原来相比增加了下面的注解:

@Options(useGeneratedKeys = true, keyProperty = "id")

这个注解对应 xml 中的配置如下:

<insert id="insert" useGeneratedKeys="true" keyProperty="id">

useGeneratedKeys 意思是要用JDBC接口方式取回主键,主键字段对应的属性名为 id,就是要回写到 id 字段。

上面的配置对 MySQL 这类自增数据库是可行的,如果你自己的主键不叫 id,甚至如果每个表的主键都不统一(如 {tableName}_id), 你需要在每个具体实现的接口中重写。例如:

public interface UserMapper extends Mapper<User, Long> {
  /**
   * 保存实体,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  @Options(useGeneratedKeys = true, keyProperty = "userId")
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(User entity);

}

如果是Oracle序列或者需要执行SQL生成主键或者取回主键时,可以配置 @SelectKey 注解,示例如下:

@Override
@Lang(Caching.class)
@SelectKey(statement = "CALL IDENTITY()", keyProperty = "id", resultType = Long.class, before = false)
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(User entity);

上面还只是通过增加注解重新定义了接口方法。实际上你还可以更换 @InsertProvider(type = EntityProvider.class, method = "insert"), 将其中的实现换成其他的也可以,如果对默认的方法和逻辑不满意,就可以改成别的。

通过 重写继承接口的定义,应该能感觉出有多强大,多么灵活。

特别注意

在 2.x 版本之后支持在实体上配置主键策略,这种方式更方便,详情看 3. 实体类注解

# 2.1.5 通过修改接口方法的返回值和入参,就能变身无数个通用方法

EntityProvider 中的 select 方法为例,方法的具体实现如下:

/**
 * 根据实体字段条件查询唯一的实体,根据实体字段条件批量查询,查询结果的数量由方法定义
 *
 * @param providerContext 上下文
 * @return cacheKey
 */
public static String select(ProviderContext providerContext) {
  return SqlScript.caching(providerContext, new SqlScript() {
    @Override
    public String getSql(EntityTable entity) {
      return "SELECT " + entity.baseColumnAsPropertyList()
          + " FROM " + entity.table()
          + ifParameterNotNull(() ->
          where(() ->
              entity.whereColumns().stream().map(column ->
                  ifTest(column.notNullTest(), () -> "AND " + column.columnEqualsProperty())
              ).collect(Collectors.joining(LF)))
      )
          + entity.groupByColumn().orElse("")
          + entity.havingColumn().orElse("")
          + entity.orderByColumn().orElse("");
    }
  });
}

最终会生成一个 SELECT .. FROM .. WHERE ... 的 SQL,在 MyBatis 中,SQL 只定义了如何在数据库执行, 执行后的结果和取值方式是通过接口方法定义决定的,因此就这样一个 SELECT 查询,能够实现很多个方法,举例如下:

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
Optional<T> selectOne(T entity);

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
List<T> selectList(T entity);

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
List<T> selectAll();

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
Cursor<T> selectCursor(T entity); 

利用这一特点,通过修改接口方法的返回值和入参,就能变身无数个通用方法。

如果在加个 RowBounds 分页参数,通用方法直接翻倍。