Awesome
通用 Mapper 核心实现 Provider
核心中定义了对象和表之间映射关系数据的结构和获取方式,通过 SPI 支持部分自定义扩展。
核心中也定义了 Provider 中的实现中需要返回 namespace.methodName
类型的字符串(而不是SQL),通过中间缓存在 Caching
中获取真正的 SQL 信息。
当前项目没有直接提供可用的通用方法,方法在 mybatis-mapper/mapper( gitee | GitHub )中提供。
注解详细使用文档: https://mapper.mybatis.io/docs/v2.x/3.1.entity.html
注解
核心提供了一套实体类的注解,简单示例如下:
@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;
}
2.x 版本默认将所有字段映射为表字段,使用驼峰转小写下划线方式,还可以像下面这样给所有字段加注解:
@Entity.Table(value = "user")
public class User {
@Entity.Column(id = true)
private Long id;
@Entity.Column("name")
private String userName;
@Entity.Column
private String sex;
//省略其他
}
除了最基本的注解配置外,还有更多可配置的属性,下面是个复杂的例子:
//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.Column(id = 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;
//不是表字段
private String noEntityColumn;
//省略其他
}
拼接 SQL 的方法
提供了 SqlScript
类用于拼接 XML 形式的 SQL,简单示例如下:
class DemoProvider {
/**
* 根据主键删除
*
* @param providerContext 上下文
* @return cacheKey
*/
public static String deleteByPrimaryKey(ProviderContext providerContext) {
return SqlScript.caching(providerContext, entity -> "DELETE FROM " + entity.table()
+ " WHERE " + entity.idColumns().stream().map(EntityColumn::columnEqualsProperty).collect(Collectors.joining(" AND ")));
}
}
SqlScript.caching
会缓存拼接 SQL 的 lambda 方法,并且返回方法的 id。
特别注意,这里返回的不是 sql,而且缓存 SQL 后的 key key值形式如:
io.mybatis.mapper.UserMapper.deleteByPrimaryKey
。
上面方法在执行时,最终拼接的 SQL 示例如下:
<script>
DELETE FROM user WHERE id = #{id}
</script>
复杂一点的:
class DemoProvider {
/**
* 根据主键查询实体
*
* @param providerContext 上下文
* @return cacheKey
*/
public static String selectByPrimaryKey(ProviderContext providerContext) {
return SqlScript.caching(providerContext, new SqlScript() {
@Override
public String getSql(EntityTable entity) {
return "SELECT " + entity.baseColumnAsPropertyList()
+ " FROM " + entity.table()
+ where(() -> entity.idColumns().stream().map(EntityColumn::columnEqualsProperty).collect(Collectors.joining(" AND ")));
}
});
}
}
上面方法在执行时,最终拼接的 SQL 示例如下:
<script>
SELECT id,name AS userName,sex FROM user
<where>
id = #{id}
</where>
</script>
更复杂的:
class DemoProvider {
/**
* 保存实体中不为空的字段
*
* @param providerContext 上下文
* @return cacheKey
*/
public static String insertSelective(ProviderContext providerContext) {
return SqlScript.caching(providerContext, new SqlScript() {
@Override
public String getSql(EntityTable entity) {
return "INSERT INTO " + entity.table()
+ trimSuffixOverrides("(", ")", ",", () ->
entity.insertColumns().stream().map(column ->
ifTest(column.notNullTest(), () -> column.column() + ",")
).collect(Collectors.joining(LF)))
+ trimSuffixOverrides(" VALUES (", ")", ",", () ->
entity.insertColumns().stream().map(column ->
ifTest(column.notNullTest(), () -> column.variables() + ",")
).collect(Collectors.joining(LF)));
}
});
}
}
上面方法在执行时,最终拼接的 SQL 示例如下:
<script>
INSERT INTO user
<trim prefix="(" suffixOverrides="," suffix=")">
<if test="id != null">
id,
</if>
<if test="userName != null">
name,
</if>
<if test="sex != null">
sex,
</if>
</trim>
<trim prefix=" VALUES (" suffixOverrides="," suffix=")">
<if test="id != null">
#{id},
</if>
<if test="userName != null">
#{userName},
</if>
<if test="sex != null">
#{sex},
</if>
</trim>
</script>
更多用法文档看 mybatis-mapper/mapper( gitee | GitHub )。
EntityTable
和 EntityColumn
EntityTable
和 EntityColumn
记录了实体类和表之间的映射信息和额外的很多配置信息。
这两个类中提供了大量便于在 XML 中使用的方法,例如: entity.insertColumns()
,这个方法会把 insertable=false
的排除后返回。
还有 EntityColumn::columnEqualsProperty
这样的方法返回 column = #{property}
形式的字符串。
为了使用上的统一,应该尽可能使用提供的现成方法,尽量避免自己拼接常用的字符串。
扩展
主要提供下面 3 个支持 SPI 扩展的接口:
EntityClassFinder
如何找到执行方法的实体类类型EntityTableFactory
根据实体类构造EntityTable
对象(表信息)EntityColumnFactory
根据实体类中的字段构建EntityColumn
对象(列信息)
更具体的信息可以查看本项目中的默认实现,还有 mybatis-mapper/mapper( gitee | GitHub ) 中提供的 jpa 实现。
更复杂的还有一个兼容 tk-mapper 的项目:mybatis-mapper/tk-mapper( gitee | GitHub ) 的实现。
Caching - LanguageDriver
这是整个通用机制的核心,Caching
实现了 LanguageDriver
接口, 允许 Provider 方法的实现返回缓存后的方法 key, 在真正执行的时候,再从缓存中找到 SQL 真正执行。
为了让 Caching
生效,需要在接口方法添加 @Lang(Caching.class)
注解,例如:
class DemoMapper<T> {
/**
* 根据主键查询实体
*
* @param id 主键
* @return 实体
*/
@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "selectByPrimaryKey")
Optional<T> selectByPrimaryKey(I id);
}