这部分属于 mybatis-mapper/provider 核心部分提供的基础注解,可以直接配合 mapper 使用。

2.0 版本之后,使用默认注解时,最简单的情况下只需要给实体添加 @Entity.Table 注解,给主键添加 @Entity.Column(id = true)注解,不再需要给所有字段添加其他注解。

# 3.1.1 注解方法介绍

注解提供了大量的配置属性,详细介绍看代码注释。

点击查看 @Entity 代码
/**
 * 表对应的实体
 *
 * @author liuzh
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Entity {
  /**
   * 对应实体类
   */
  Class<?> value();

  /**
   * 表名
   */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.TYPE)
  @interface Table {
    /**
     * 表名,默认空时使用对象名(不进行任何转换)
     */
    String value() default "";

    /**
     * 备注,仅用于在注解上展示,不用于任何其他处理
     */
    String remark() default "";

    /**
     * catalog 名称,配置后,会在表名前面加上 catalog 名称,规则为:catalog.schema.tableName,支持全局 mybatis.provider.catalog 配置
     */
    String catalog() default "";

    /**
     * schema 名称,配置后,会在表名前面加上 schema 名称,规则为:catalog.schema.tableName,支持全局 mybatis.provider.schema 配置
     */
    String schema() default "";

    /**
     * 名称规则、样式,同时应用于表名和列名,不想用于表名时,直接指定表名 {@link #value()}即可。
     * <p>
     * 2.0版本之前默认为 {@link Style#NORMAL}, 2.0版本之后默认使用 {@link Style#LOWER_UNDERSCORE}
     * <p>
     * 可以通过 {@link Style#DEFAULT_STYLE_KEY} = 格式 来修改默认值
     */
    String style() default "";

    /**
     * 使用指定的 <resultMap>
     */
    String resultMap() default "";

    /**
     * 自动根据字段生成 <resultMap>
     */
    boolean autoResultMap() default false;

    /**
     * 属性配置
     */
    Prop[] props() default {};

    /**
     * 排除指定父类的所有字段
     */
    Class<?>[] excludeSuperClasses() default {};

    /**
     * 排除指定类型的字段
     */
    Class<?>[] excludeFieldTypes() default {};

    /**
     * 排除指定字段名的字段
     */
    String[] excludeFields() default {};
  }

  /**
   * 属性配置,优先级高于 {@link io.mybatis.config.ConfigHelper } 提供的配置
   */
  @interface Prop {
    /**
     * 属性名
     */
    String name();

    /**
     * 属性值
     */
    String value();
  }

  /**
   * 排除列
   */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.FIELD)
  @interface Transient {
  }

  /**
   * 列名
   */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.FIELD)
  @interface Column {
    /**
     * 列名,默认空时使用字段名(不进行任何转换)
     */
    String value() default "";

    /**
     * 备注,仅用于在注解上展示,不用于任何其他处理
     */
    String remark() default "";

    /**
     * 标记字段是否为主键字段
     */
    boolean id() default false;

    /**
     * 主键策略1,优先级1:是否使用 JDBC 方式获取主键,优先级最高,设置为 true 后,不对其他配置校验
     */
    boolean useGeneratedKeys() default false;

    /**
     * 主键策略2,优先级2:取主键的 SQL,当前SQL只能在 INSERT 语句执行后执行,如果想要在 INSERT 语句执行前执行,可以使用 {@link #genId()}
     */
    String afterSql() default "";

    /**
     * 主键策略3,优先级3:Java 方式生成主键,可以和发号器一类的服务配合使用
     */
    Class<? extends GenId> genId() default GenId.NULL.class;

    /**
     * 执行 genId 的时机,仅当 {@link #genId()} 不为空时有效,默认插入前执行
     */
    boolean genIdExecuteBefore() default true;

    /**
     * 排序方式,默认空时不作为排序字段,只有手动设置 ASC 和 DESC 才有效
     */
    String orderBy() default "";

    /**
     * 排序的优先级,多个排序字段时,根据该值确定顺序,数值越小优先级越高
     */
    int orderByPriority() default 0;

    /**
     * 可查询
     */
    boolean selectable() default true;

    /**
     * 可插入
     */
    boolean insertable() default true;

    /**
     * 可更新
     */
    boolean updatable() default true;

    /**
     * 数据库类型 {, jdbcType=VARCHAR}
     */
    JdbcType jdbcType() default JdbcType.UNDEFINED;

    /**
     * 类型处理器 {, typeHandler=XXTypeHandler}
     */
    Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;

    /**
     * 小数位数 {, numericScale=2}
     */
    String numericScale() default "";

    /**
     * 属性配置
     */
    Prop[] props() default {};
  }
}

# 3.1.2 @Entity.Table 注解

这个注解有下面几个属性:

  • value:表名,默认空时使用对象名(不进行任何转换)
  • remark:备注,仅用于在注解上展示,不用于任何其他处理
  • catalog:catalog 名称,配置后,会在表名前面加上 catalog 名称,规则为:catalog.schema.tableName,支持全局 mybatis.provider.catalog 配置
  • schema:schema 名称,配置后,会在表名前面加上 schema 名称,规则为:catalog.schema.tableName,支持全局 mybatis.provider.schema 配置
  • style:名称规则、样式,同时应用于表名和列名,不想用于表名时,直接指定表名即可。默认支持下面几种转换规则:
    • Style.NORMAL:不做任何转换
    • Style.LOWER_UNDERSCORE:默认值,驼峰转换为小写下划线,如:userName 转换为 user_name
    • Style.UPPER_UNDERSCORE:驼峰转换为大写下划线,如:userName 转换为 USER_NAME
    • Style.LOWER:转换为小写,如:userName 转换为 username
    • Style.UPPER:转换为大写,如:userName 转换为 USERNAME
    • 使用 SPI 继承 io.mybatis.provider.Style 接口,实现自定义的转换规则
  • resultMap:使用指定的 <resultMap>
  • autoResultMap:自动根据字段生成 <resultMap>,生成的 resultMap 时机很晚,只能用于 mybatis-mapper 本身,手写的代码无法引用,如果需要引用,需要手动定义 <resultMap>,可以借助代码生成器生成
  • props: 属性配置,没有明确的作用,实体类上的配置信息会写入 EntityTable.props 中,可以在扩展时使用
  • excludeSuperClasses:排除指定父类的所有字段
  • excludeFieldTypes:排除指定类型的字段
  • excludeFields:排除指定字段名的字段

通过 resultMap 可以指定在其他地方定义好的 <resultMap>, 可以直接在 XML 中定义好,这里直接使用。autoResultMapresultMap 相反,但功能相同, 这个配置会自动根据当前字段的配置生成 <resultMap>,所有查询方法都会使用生成的 <resultMap>, 这样就能支持查询结果中的 jdbcTypeTypeHandler 等配置的应用。

这部分的简单示例如下:

@Entity.Table("sys_user")
public class User {
//忽略其他
}

复杂点的示例如:

//autoResultMap 自动生成 <resultMap> 结果映射,支持查询结果中的 typeHandler 等配置
@Entity.Table(value = "sys_user", remark = "系统用户", autoResultMap = true,
  props = {
    //deleteByExample方法中的Example条件不能为空,默认允许空,另外两个配置类似
    @Entity.Prop(name = "deleteByExample.allowEmpty", value = "false", type = Boolean.class),
    @Entity.Prop(name = "updateByExample.allowEmpty", value = "false", type = Boolean.class),
    @Entity.Prop(name = "updateByExampleSelective.allowEmpty", value = "false", type = Boolean.class)
  })
public class User {
//忽略其他
}

如果想排除父类还可以配置如下:

@Entity.Table(value = "sys_user", excludeSuperClasses = {BaseId.class})
public class User extends BaseId {
//忽略其他
}

# 3.1.3 @Entity.Column 注解

列的注解包含了大量的属性,这些属性涉及到了查询列、插入列、更新列,以及所有可能出现在 <result>#{} 中的参数:

  • value:列名,默认空时使用字段名(不进行任何转换)
  • remark:备注,仅用于在注解上展示,不用于任何其他处理
  • id:标记字段是否为主键字段
  • useGeneratedKeys:主键策略,优先级最高:是否使用 JDBC 方式获取主键,设置为 true 后,不对其他配置校验
  • afterSql:主键策略,优先级次之:取主键的 SQL,当前 SQL 只能在 INSERT 语句执行后执行,如果想要在 INSERT 语句执行前执行,可以使用 genId
  • genId:主键策略,优先级最低:Java 方式生成主键,可以和发号器一类的服务配合使用
  • genIdExecuteBefore:执行 genId 的时机,仅当 genId 不为空时有效,默认插入前执行
  • orderBy:排序方式,默认空时不作为排序字段,只有手动设置 ASCDESC 才有效
  • orderByPriority:排序的优先级,多个排序字段时,根据该值确定顺序,数值越小优先级越高
  • selectable:可查询
  • insertable:可插入
  • updatable:可更新
  • jdbcType:数据库类型
  • typeHandler:类型处理器
  • numericScale:小数位数
  • props:属性配置,没有明确的作用,实体类上的配置信息会写入 EntityColumn.props 中,可以在扩展时使用

下面是一个最简单的示例:

@Entity.Table("sys_user")
public class User {
  @Entity.Column(id = true, useGeneratedKeys = true)
  private Long id;
  private String name;
  private boolean admin;
  private Integer seq;
  private Double points;
  private String password;
  private Date whenCreated;
  private String info;
  private String noEntityColumn;
}

默认驼峰转下划线的情况下,如果实体类只有表字段,这个示例就是最简单的情况,只需要设置表名和主键字段即可。

其他没有注解的字段默认都会作为表字段处理,如果不想处理,可以使用 @Entity.Transient 注解排除。

下面是一个稍微复杂点的示例:

@Entity.Table(value = "sys_user", remark = "系统用户", autoResultMap = true)
public class User {
  @Entity.Column(id = true, useGeneratedKeys = true, remark = "主键", updatable = false, insertable = false)
  private Long id;
  @Entity.Column(value = "name", remark = "帐号")
  private String name;
  @Entity.Column(value = "is_admin", remark = "是否为管理员", updatable = false)
  private boolean admin;
  @Entity.Column(remark = "顺序号", orderBy = "DESC")
  private Integer seq;
  @Entity.Column(numericScale = "4", remark = "积分(保留4位小数)")
  private Double points;
  @Entity.Column(selectable = false, remark = "密码")
  private String password;
  @Entity.Column(value = "when_created", remark = "创建时间", jdbcType = JdbcType.TIMESTAMP)
  private Date whenCreated;
  @Entity.Column(remark = "介绍", typeHandler = StringTypeHandler.class)
  private String info;
  //不是表字段,排除字段
  @Entity.Transient
  private String noEntityColumn;
  //省略其他
}

这个示例展示了如何使用 @Entity.Table@Entity.Column 注解来定义一个实体类和它的字段。下面是每个注解的详细解释:

  • @Entity.Table(value = "sys_user", remark = "系统用户", autoResultMap = true):这个注解定义了实体类对应的数据库表名(sys_user),备注(系统用户),并且启用了自动结果映射(autoResultMap = true)。
  • @Entity.Column(id = true, useGeneratedKeys = true, remark = "主键", updatable = false, insertable = false):这个注解定义了一个主键字段(id),它使用了数据库自动生成的键(useGeneratedKeys = true),并且它不可更新(updatable = false)和不可插入(insertable = false)。
  • @Entity.Column(value = "name", remark = "帐号"):这个注解定义了一个名为 name 的字段,它的备注是 帐号
  • @Entity.Column(value = "is_admin", remark = "是否为管理员", updatable = false):这个注解定义了一个名为 is_admin 的字段,它的备注是 是否为管理员,并且它不可更新(updatable = false)。
  • @Entity.Column(remark = "顺序号", orderBy = "DESC"):这个注解定义了一个字段,它的备注是 顺序号,并且在查询时按照降序排序(orderBy = "DESC")。
  • @Entity.Column(numericScale = "4", remark = "积分(保留4位小数)"):这个注解定义了一个字段,它的备注是 积分(保留4位小数),并且它的数值精度是4位小数(numericScale = "4")。
  • @Entity.Column(selectable = false, remark = "密码"):这个注解定义了一个字段,它的备注是 密码,并且它不可查询(selectable = false)。
  • @Entity.Column(value = "when_created", remark = "创建时间", jdbcType = JdbcType.TIMESTAMP):这个注解定义了一个名为 when_created 的字段,它的备注是 创建时间,并且它的 JDBC 类型是 TIMESTAMP
  • @Entity.Column(remark = "介绍", typeHandler = StringTypeHandler.class):这个注解定义了一个字段,它的备注是 介绍,并且它的类型处理器是 StringTypeHandler
  • @Entity.Transient:这个注解表示 noEntityColumn 字段不是表字段,因此在数据库操作时会被忽略。

# 3.1.4 @Entity.Transient 注解

@Entity.Transient 注解用于标记实体类中不需要映射到数据库表的字段。这些字段在执行数据库操作时会被忽略。

例如,假设我们有一个 User 实体类,其中包含一个 passwordConfirmation 字段,这个字段只在应用程序中使用,不需要保存到数据库。我们可以使用 @Entity.Transient 注解来标记这个字段:

@Entity.Table("sys_user")
public class User {
  @Entity.Column(id = true, useGeneratedKeys = true)
  private Long id;
  private String name;
  private String password;
  
  @Entity.Transient
  private String passwordConfirmation;
}

在这个例子中,passwordConfirmation 字段在执行数据库操作时会被忽略。

此外,@Entity.Table 注解还提供了 excludeSuperClassesexcludeFieldTypesexcludeFields 属性,这些属性可以用来排除不需要映射到数据库的字段。

  • excludeSuperClasses:排除指定父类的所有字段
  • excludeFieldTypes:排除指定类型的字段
  • excludeFields:排除指定字段名的字段

除了使用 @Entity.Transient 注解来排除不需要映射到数据库的字段外,还可以通过 @Entity.Table 注解的 excludeFields 属性来排除指定字段名的字段。 我们可以使用 excludeFields 属性排除这个字段达到相同的效果:

@Entity.Table(value = "sys_user", excludeFields = {"passwordConfirmation"})
public class User {
  @Entity.Column(id = true, useGeneratedKeys = true)
  private Long id;
  private String name;
  private String password;
  private String passwordConfirmation;
}

在这个例子中,passwordConfirmation 字段在执行数据库操作时会被忽略。

# 3.1.5 默认注解实现

执行通用方法前,首先需要(EntityClassFinder)从接口和方法中获取可能是实体类的类型,找到正确的类型后,(EntityTableFactory)再根据类型初始化 EntityTable, 然后(EntityColumnFactory)根据字段初始化 EntityColumn

每种注解的扩展实现基本上就是实现这 3 个接口,通过这 3 个接口配合完成实体类的初始化。

  • EntityClassFinder:查找实体类类型
  • EntityTableFactory:初始化 EntityTable
  • EntityColumnFactory:初始化 EntityColumn

# 3.1.5.1 EntityClassFinder 接口

在默认实现中,使用 DefaultEntityClassFinder 来查找符合条件的实体类。

点击查看 DefaultEntityClassFinder 代码
public class DefaultEntityClassFinder extends GenericEntityClassFinder {

  @Override
  public Optional<Class<?>> findEntityClass(Class<?> mapperType, Method mapperMethod) {
    if (mapperMethod != null) {
      //首先是接口方法
      if (mapperMethod.isAnnotationPresent(Entity.class)) {
        Entity entity = mapperMethod.getAnnotation(Entity.class);
        return Optional.of(entity.value());
      }
    }
    //其次是接口上
    if (mapperType.isAnnotationPresent(Entity.class)) {
      Entity entity = mapperType.getAnnotation(Entity.class);
      return Optional.of(entity.value());
    }
    //没有明确指名的情况下,通过泛型获取
    return super.findEntityClass(mapperType, mapperMethod);
  }

  @Override
  public boolean isEntityClass(Class<?> clazz) {
    return clazz.isAnnotationPresent(Entity.Table.class);
  }

}

默认实现中,首先从方法上获取 @Entity 注解,如果方法上有 @Entity 注解,就返回 Entity.value() 的值。 其次从接口上获取 @Entity 注解,如果接口上有 @Entity 注解,就返回 Entity.value() 的值。 最后,如果都没有找到,就通过 super.findEntityClass 方法获取。 父类方法中会先获取方法的返回值类型,使用 isEntityClass 方法判断是否符合条件,这里就是判断是否有 @Entity.Table 注解。 返回值不符合时(如 int insert(T entity) 方法),会继续判断参数类型, 参数类型不符合时(如 int deleteByPrimaryKey(I id)),会判断接口中的泛型类型。

优先级顺序:

  1. 接口方法上的 @Entity 注解
  2. 接口上的 @Entity 注解
  3. 接口方法返回值类型是否有 @Entity.Table 注解
  4. 接口方法参数类型是否有 @Entity.Table 注解
  5. 接口泛型类型是否有 @Entity.Table 注解

# 3.1.5.2 EntityTableFactory 接口

通过上面逻辑找到 EntityClass 后,就需要通过 EntityTableFactory 来初始化 EntityTable实体类的默认注解实现在 DefaultEntityTableFactory 中,这个类实现了 EntityTableFactory 接口,用于创建 EntityTable 对象。

点击查看 DefaultEntityTableFactory 代码
/**
 * 默认实现,针对 {@link Entity.Table} 注解实现
 *
 * @author liuzh
 */
public class DefaultEntityTableFactory implements EntityTableFactory {

  @Override
  public EntityTable createEntityTable(Class<?> entityClass, Chain chain) {
    if (entityClass.isAnnotationPresent(Entity.Table.class)) {
      Entity.Table table = entityClass.getAnnotation(Entity.Table.class);
      EntityTable entityTable = EntityTable.of(entityClass)
          .table(table.value().isEmpty() ? Style.getStyle(table.style()).tableName(entityClass) : table.value())
          .catalog(table.catalog().isEmpty() ? ConfigHelper.getStr("mybatis.provider.catalog") : table.catalog())
          .schema(table.schema().isEmpty() ? ConfigHelper.getStr("mybatis.provider.schema") : table.schema())
          .style(table.style())
          .resultMap(table.resultMap())
          .autoResultMap(table.autoResultMap())
          .excludeSuperClasses(table.excludeSuperClasses())
          .excludeFieldTypes(table.excludeFieldTypes())
          .excludeFields(table.excludeFields());
      for (Entity.Prop prop : table.props()) {
        entityTable.setProp(prop);
      }
      return entityTable;
    }
    return null;
  }

}

DefaultEntityTableFactory 中,我们首先检查实体类是否使用了 @Entity.Table 注解, 如果使用了,我们就创建一个 EntityTable 对象, 并设置表名、catalog、schema、style、resultMap、autoResultMap、excludeSuperClasses、excludeFieldTypes 和 excludeFields 属性。

这个实现要求必须有 @Entity.Table 注解,否则不会被识别为实体类。

JPA 实现 中,判断条件更宽松,实体类可以不添加任何注解。

说明

EntityClassFinder 可以有很多实现,例如默认的实现和 JPA 的实现, 所以最终得到的 EntityClass 不一定符合 EntityTableFactory 对实体类的要求, 此时该实现会返回 null,然后会继续调用下一个实现。在下一个实现中符合条件时就会创建 EntityTable 对象。

# 3.1.5.3 EntityColumnFactory 接口

实体类字段的默认注解实现在 DefaultEntityColumnFactory 中,这个类实现了 EntityColumnFactory 接口,用于创建 EntityColumn 对象。

点击查看 DefaultEntityColumnFactory 代码
/**
 * 默认实现,针对 {@link Entity.Column} 注解实现
 *
 * @author liuzh
 */
public class DefaultEntityColumnFactory implements EntityColumnFactory {

  @Override
  public Optional<List<EntityColumn>> createEntityColumn(EntityTable entityTable, EntityField field, Chain chain) {
    if (field.isAnnotationPresent(Entity.Column.class)) {
      Entity.Column column = field.getAnnotation(Entity.Column.class);
      EntityColumn entityColumn = EntityColumn.of(field)
              .column(column.value().isEmpty() ? Style.getStyle(entityTable.style()).columnName(entityTable, field) : column.value())
              .id(column.id())
              .useGeneratedKeys(column.useGeneratedKeys())
              .afterSql(column.afterSql())
              .genId(column.genId())
              .genIdExecuteBefore(column.genIdExecuteBefore())
              .orderBy(column.orderBy())
              .orderByPriority(column.orderByPriority())
              .selectable(column.selectable())
              .insertable(column.insertable())
              .updatable(column.updatable())
              .jdbcType(column.jdbcType())
              .typeHandler(column.typeHandler())
              .numericScale(column.numericScale());
      for (Entity.Prop prop : column.props()) {
        entityColumn.setProp(prop);
      }
      return Optional.of(Arrays.asList(entityColumn));
    } else if (field.isAnnotationPresent(Entity.Transient.class)) {
      return IGNORE;
    } else {
      return Optional.of(Arrays.asList(EntityColumn.of(field)
              .column(Style.getStyle(entityTable.style()).columnName(entityTable, field))
              .numericScale("")
              .jdbcType(JdbcType.UNDEFINED)));
    }
  }

}

DefaultEntityColumnFactory 中,我们首先检查字段是否使用了 @Entity.Column 注解,如果存在该注解就使用注解的配置初始化 EntityColumn, 其次判断字段是否使用了 @Entity.Transient 注解,如果没有使用该注解,就使用默认配置初始化 EntityColumn

前面提过的 @Entity.Table 注解还提供了 excludeSuperClassesexcludeFieldTypesexcludeFields 属性, 这些配置在调用 createEntityColumn 方法之前就在 EntityFactory 中进行了过滤:

for (Field field : declaredFields) {
  int modifiers = field.getModifiers();
  //排除 static 和 transient 修饰的字段
  if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
    EntityField entityField = new EntityField(entityClass, field);
    // 是否需要排除字段                <------这里
    if (entityTable.isExcludeField(entityField)) {
      continue;
    }
    Optional<List<EntityColumn>> optionalEntityColumns = Holder.entityColumnFactoryChain.createEntityColumn(entityTable, entityField);
    optionalEntityColumns.ifPresent(columns -> columns.forEach(entityTable::addColumn));
  }
}

了解这部分的实现信息有助于帮我们了解如何使用这些注解。

EntityTableFactoryEntityColumnFactory 接口中都有 Chain 接口定义,实体类和字段扩展的处理过程是一个链式调用, 也就是引入 JPA 实现后,这一节的内容仍然有效,会作为链式调用的最后一级进行调用,相对初始创建 EntityTableEntityColumn 对象来说优先级会更高, 如果没有找到这一节提到的注解,就会使用 JPA 中的逻辑对实体和列进行处理。 如果有这些注解,就会使用 JPA 中的逻辑覆盖默认实现中的配置,也就是扩展中的配置优先级更高。 这部分的具体逻辑还是需要看具体实现,建议扩展时遵循这里给出的逻辑进行扩展。