这部分属于 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 中定义好,这里直接使用。autoResultMap
和 resultMap
相反,但功能相同,
这个配置会自动根据当前字段的配置生成 <resultMap>
,所有查询方法都会使用生成的 <resultMap>
,
这样就能支持查询结果中的 jdbcType
和 TypeHandler
等配置的应用。
这部分的简单示例如下:
@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
:排序方式,默认空时不作为排序字段,只有手动设置ASC
和DESC
才有效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
注解还提供了 excludeSuperClasses
,excludeFieldTypes
,excludeFields
属性,这些属性可以用来排除不需要映射到数据库的字段。
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)
),会判断接口中的泛型类型。
优先级顺序:
- 接口方法上的
@Entity
注解 - 接口上的
@Entity
注解 - 接口方法返回值类型是否有
@Entity.Table
注解 - 接口方法参数类型是否有
@Entity.Table
注解 - 接口泛型类型是否有
@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
注解还提供了 excludeSuperClasses
,excludeFieldTypes
,excludeFields
属性,
这些配置在调用 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));
}
}
了解这部分的实现信息有助于帮我们了解如何使用这些注解。
在 EntityTableFactory
和 EntityColumnFactory
接口中都有 Chain
接口定义,实体类和字段扩展的处理过程是一个链式调用,
也就是引入 JPA 实现后,这一节的内容仍然有效,会作为链式调用的最后一级进行调用,相对初始创建 EntityTable
和 EntityColumn
对象来说优先级会更高,
如果没有找到这一节提到的注解,就会使用 JPA 中的逻辑对实体和列进行处理。
如果有这些注解,就会使用 JPA 中的逻辑覆盖默认实现中的配置,也就是扩展中的配置优先级更高。
这部分的具体逻辑还是需要看具体实现,建议扩展时遵循这里给出的逻辑进行扩展。