这部分属于 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()}即可。
* 2.0版本之前默认为 {@link Style#NORMAL}, 2.0版本之后默认使用 {@link Style#LOWER_UNDERSCORE}
* 可以通过 {@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 中的逻辑覆盖默认实现中的配置,也就是扩展中的配置优先级更高。
这部分的具体逻辑还是需要看具体实现,建议扩展时遵循这里给出的逻辑进行扩展。
2.8. LogicalMapper 接口 3.2. JPA 注解