前言
之前公司用的jpa, 个人感觉很方便, 新的项目选择使用mybatis, sql都是写在xml文件里, 虽然基本的方法都有工具生成, 但是一旦数据增加一个字段, 修改这些方法真的是不爽, 而且个人看xml文件感觉是真的累, 就这样不爽里一段时间, 趁着项目空闲的时候, 研究下如何抛弃xml文件, 完全使用注解的方式, 并且把通用的方法抽出到一个基类中。
本文代码已整理上传github
如何实现BaseMapper<T>
通用mapper一般包含基本的增删改, 根据id查, 根据某一个属性查, 根据条件集合查 这些方法。
使用的时候直接继承, 泛型就是具体的实体类。
mybatis 提供了@InsertProvider, @SelectProvider等来动态生成sql, 所以通用mapper就是使用的这些注解。
通用mapper动态生成sql的思路就是拿到实体类的class, 根据class解析出对应表的元数据, 包括表名, 主键信息, 数据库字段等, 在根据这些信息动态生成sql。
对于insert和update来说, 方法的参数就是实体对象, 直接getClass()就能拿到, 但是对于查询和删除,方法的参数就不是实体对象了, 在通用mapper里怎么拿到 class对象
, mybatis 3.4.5 之前的版本是做不到的, 从源码里就限制了, 参考mybatis的这个issue, 3.4.5版本开始, 在调用provider方法时 可以多传递一个参数-ProviderContext, 这个ProviderContext 就可以获取当前具体是哪个mapper的class和调用的方法。
这样通过 具体mapper的接口获取到泛型参数, 这个泛型参数就是实体对象, 就是 T 的具体值
下面是具体的实现, 使用mybatis的版本是 3.4.6, 依赖了 spring的一些工具类
BaseMapper.java
public interface BaseMapper<Entity> {
/**
* 新增一条记录
*
* @param entity 实体
* @return 受影响记录
*/
@InsertProvider(type = BaseSqlProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyColumn = "id")
int insert(Entity entity);
/**
* 更新一条记录
*
* @param entity entity
* @return 受影响记录
*/
@UpdateProvider(type = BaseSqlProvider.class, method = "update")
int update(Entity entity);
/**
* 删除一条记录
*
* @param id id
* @return 受影响记录
*/
@DeleteProvider(type = BaseSqlProvider.class, method = "delete")
int delete(Long id);
/**
* 根据id查询
*
* @param id id
* @return Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectById")
Entity selectById(Long id);
/**
* 根据属性查询一条记录
*
* @param function property
* @param value value
* @param <R> R
* @return Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectByProperty")
<R> Entity selectByProperty(@Param("property") PropertyFunction<Entity, R> function, @Param("value") Object value);
/**
* 根据属性查询记录列表
*
* @param function property
* @param value value
* @param <R> R
* @return Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectByProperty")
<R> List<Entity> selectListByProperty(@Param("property") PropertyFunction<Entity, R> function, @Param("value") Object value);
/**
* 根据查询条件查询记录
*
* @param condition condition
* @param <Condition> Condition
* @return List Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectByCondition")
<Condition> List<Entity> selectByCondition(Condition condition);
}
BaseSqlProvider.java
public class BaseSqlProvider {
public <Entity> String insert(Entity entity) {
Assert.notNull(entity, "entity must not null");
Class<?> entityClass = entity.getClass();
TableMataDate mataDate = TableMataDate.forClass(entityClass);
Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap();
SQL sql = new SQL();
sql.INSERT_INTO(mataDate.getTableName());
for (Map.Entry<String, String> entry : fieldColumnMap.entrySet()) {
// 忽略主键
if (Objects.equals(entry.getKey(), mataDate.getPkProperty())) {
continue;
}
PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(entityClass, entry.getKey());
if (ps == null || ps.getReadMethod() == null) {
continue;
}
Object value = ReflectionUtils.invokeMethod(ps.getReadMethod(), entity);
if (!StringUtils.isEmpty(value)) {
sql.VALUES(entry.getValue(), getTokenParam(entry.getKey()));
}
}
return sql.toString();
}
public <Entity> String update(Entity entity) {
Assert.notNull(entity, "entity must not null");
Class<?> entityClass = entity.getClass();
TableMataDate mataDate = TableMataDate.forClass(entityClass);
Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap();
SQL sql = new SQL();
sql.UPDATE(mataDate.getTableName());
for (Map.Entry<String, String> entry : fieldColumnMap.entrySet()) {
// 忽略主键
if (Objects.equals(entry.getKey(), mataDate.getPkProperty())) {
continue;
}
PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(entityClass, entry.getKey());
if (ps == null || ps.getReadMethod() == null) {
continue;
}
Object value = ReflectionUtils.invokeMethod(ps.getReadMethod(), entity);
if (!StringUtils.isEmpty(value)) {
sql.SET(getEquals(entry.getValue(), entry.getKey()));
}
}
return sql.WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty())).toString();
}
public String delete(ProviderContext context) {
Class<?> entityClass = getEntityClass(context);
TableMataDate mataDate = TableMataDate.forClass(entityClass);
return new SQL().DELETE_FROM(mataDate.getTableName())
.WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty()))
.toString();
}
public String selectById(ProviderContext context) {
Class<?> entityClass = getEntityClass(context);
TableMataDate mataDate = TableMataDate.forClass(entityClass);
return new SQL().SELECT(mataDate.getBaseColumns())
.FROM(mataDate.getTableName())
.WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty()))
.toString();
}
public String selectByProperty(ProviderContext context, Map<String, Object> params) {
PropertyFunction propertyFunction = (PropertyFunction) params.get("property");
String property = SerializedLambdaUtils.getProperty(propertyFunction);
Class<?> entityClass = getEntityClass(context);
TableMataDate mataDate = TableMataDate.forClass(entityClass);
String column = mataDate.getFieldColumnMap().get(property);
return new SQL().SELECT(mataDate.getBaseColumns())
.FROM(mataDate.getTableName())
.WHERE(getEquals(column, property))
.toString();
}
public String selectByCondition(ProviderContext context, Object condition) {
Class<?> entityClass = getEntityClass(context);
TableMataDate mataDate = TableMataDate.forClass(entityClass);
Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap();
SQL sql = new SQL().SELECT(mataDate.getBaseColumns()).FROM(mataDate.getTableName());
Field[] fields = condition.getClass().getDeclaredFields();
for (Field field : fields) {
Condition logicCondition = field.getAnnotation(Condition.class);
String mappedProperty = logicCondition == null || StringUtils.isEmpty(logicCondition.property()) ? field.getName() : logicCondition.property();
PropertyDescriptor entityPd = BeanUtils.getPropertyDescriptor(entityClass, mappedProperty);
if (entityPd == null) {
continue;
}
PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(condition.getClass(), field.getName());
if (pd == null || pd.getReadMethod() == null) {
continue;
}
String column = fieldColumnMap.get(mappedProperty);
Object value = ReflectionUtils.invokeMethod(pd.getReadMethod(), condition);
if (!StringUtils.isEmpty(value)) {
Logic logic = logicCondition == null ? Logic.EQ : logicCondition.logic();
if (logic == Logic.IN || logic == Logic.NOT_IN) {
if (value instanceof Collection) {
sql.WHERE(column + logic.getCode() + inExpression(field.getName(), ((Collection) value).size()));
}
} else if (logic == Logic.NULL || logic == Logic.NOT_NULL) {
sql.WHERE(column + logic.getCode());
} else {
sql.WHERE(column + logic.getCode() + getTokenParam(mappedProperty));
}
}
}
return sql.toString();
}
private Class<?> getEntityClass(ProviderContext context) {
Class<?> mapperType = context.getMapperType();
for (Type parent : mapperType.getGenericInterfaces()) {
ResolvableType parentType = ResolvableType.forType(parent);
if (parentType.getRawClass() == BaseMapper.class) {
return parentType.getGeneric(0).getRawClass();
}
}
return null;
}
private String getEquals(String column, String property) {
return column + " = " + getTokenParam(property);
}
private String getTokenParam(String property) {
return "#{" + property + "}";
}
private String inExpression(String property, int size) {
MessageFormat messageFormat = new MessageFormat("#'{'" + property + "[{0}]}");
StringBuilder sb = new StringBuilder(" (");
for (int i = 0; i < size; i++) {
sb.append(messageFormat.format(new Object[]{i}));
if (i != size - 1) {
sb.append(", ");
}
}
return sb.append(")").toString();
}
}
其他一些类
@Getter
public class TableMataDate {
private static final Map<Class<?>, TableMataDate> TABLE_CACHE = new ConcurrentHashMap<>(64);
/**
* 表名
*/
private String tableName;
/**
* 主键属性名
*/
private String pkProperty;
/**
* 主键对应的列名
*/
private String pkColumn;
/**
* 属性名和字段名映射关系的 map
*/
private Map<String, String> fieldColumnMap;
/**
* 字段类型
*/
private Map<String, Class<?>> fieldTypeMap;
private TableMataDate(Class<?> clazz) {
fieldColumnMap = new HashMap<>();
fieldTypeMap = new HashMap<>();
initTableInfo(clazz);
}
public static TableMataDate forClass(Class<?> entityClass) {
TableMataDate tableMataDate = TABLE_CACHE.get(entityClass);
if (tableMataDate == null) {
tableMataDate = new TableMataDate(entityClass);
TABLE_CACHE.put(entityClass, tableMataDate);
}
return tableMataDate;
}
public String getBaseColumns() {
Collection<String> columns = fieldColumnMap.values();
if (CollectionUtils.isEmpty(columns)) {
return "";
}
Iterator<String> iterator = columns.iterator();
StringBuilder sb = new StringBuilder();
while (iterator.hasNext()) {
String next = iterator.next();
sb.append(tableName).append(".").append(next);
if (iterator.hasNext()) {
sb.append(", ");
}
}
return sb.toString();
}
/**
* 根据注解初始化表信息,
*
* @param clazz 实体类的 class
*/
private void initTableInfo(Class<?> clazz) {
tableName = clazz.isAnnotationPresent(Table.class) ? clazz.getAnnotation(Table.class).name()
: NameUtils.getUnderLineName(clazz.getSimpleName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 过滤静态字段和有 @Transient 注解的字段
if (Modifier.isStatic(field.getModifiers()) ||
field.isAnnotationPresent(Transient.class) ||
!BeanUtils.isSimpleValueType(field.getType())) {
continue;
}
String property = field.getName();
Column column = field.getAnnotation(Column.class);
String columnName = column != null ? column.name().toLowerCase() : NameUtils.getUnderLineName(property);
// 主键信息 : 有 @Id 注解的字段,没有默认是 类名+Id
if (field.isAnnotationPresent(Id.class) || (property.equalsIgnoreCase("id") && pkProperty == null)) {
pkProperty = property;
pkColumn = columnName;
}
// 将字段对应的列放到 map 中
PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(clazz, property);
if (descriptor != null && descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null) {
fieldColumnMap.put(property, columnName);
fieldTypeMap.put(property, field.getType());
}
}
}
}
数据库字段和实体属性不一致也是下划线转驼峰怎么办
在上面生成动态sql的时候在实体上可以加 @Column, @Table, @Id 注解保证生成的sql没问题
但是 对于查来说, 查出来后还要转成实体类的, 如果属性不对应, 转出来的实体就会缺少值, mybatis还提供类@Results注解写在方法上, 来自定义实体属性和数据库字段的映射,
但是都已经在实体类上写上@Column表示映射关系来,再在方法上写注解,很不雅观,
所以我们需要动态生成ResultMap
mybatis里的接口方法 最终都会生成 MappedStaement 与之对应, 数据库字段和实体属性映射的信息也是保存在这里的, 所以只需要修改 MappedStaement 里的信息就可以了,
MappedStaement可以通过mybatis 自带的拦截机制, 拦截 Executor 的 query 方法获取
代码如下 已整理上传github:
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class ResultMapInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!(invocation.getTarget() instanceof Executor)) {
return invocation.proceed();
}
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// xml sql 不做处理
if (ms.getResource().contains(".xml")) {
return invocation.proceed();
}
ResultMap resultMap = ms.getResultMaps().iterator().next();
if (!CollectionUtils.isEmpty(resultMap.getResultMappings())) {
return invocation.proceed();
}
Class<?> mapType = resultMap.getType();
if (ClassUtils.isAssignable(mapType, Collection.class)) {
return invocation.proceed();
}
TableMataDate mataDate = TableMataDate.forClass(mapType);
Map<String, Class<?>> fieldTypeMap = mataDate.getFieldTypeMap();
//
List<ResultMapping> resultMappings = new ArrayList<>(fieldTypeMap.size());
for (Map.Entry<String, String> entry : mataDate.getFieldColumnMap().entrySet()) {
ResultMapping resultMapping = new ResultMapping.Builder(ms.getConfiguration(), entry.getKey(), entry.getValue(), fieldTypeMap.get(entry.getKey())).build();
resultMappings.add(resultMapping);
}
ResultMap newRm = new ResultMap.Builder(ms.getConfiguration(), resultMap.getId(), mapType, resultMappings).build();
Field field = ReflectionUtils.findField(MappedStatement.class, "resultMaps");
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, ms, Collections.singletonList(newRm));
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
使用效果
新建 GoodsMapper 继承 BaseMapper
public interface GoodsMapper extends BaseMapper<Goods> {
}
@Data
public class Goods implements Serializable {
private static final long serialVersionUID = -6305173237589282633L;
private Long id;
private String code;
private String fullName;
private Double price;
private Date createdAt;
}
查询
根据实体某个属性查询, eg: 根据商品 code 查询一条商品记录:
@Test
public void test4() {
Goods goods = goodsMapper.selectByProperty(Goods::getCode, "2332");
}
根据查询条件查询*
新建商品的查询条件 GoodCondition
@Data
public class GoodsCondition implements Serializable {
private static final long serialVersionUID = -1113673119261537637L;
private Long id;
// @Condition(logic = Logic.IN, property = "code")
private List<String> codes;
private Double price;
// @Condition(logic = Logic.LIKE)
private String fullName;
private String code;
}
使用 通用mapper的 selectByCondition 方法查询
@Test
public void test3() {
GoodsCondition condition = new GoodsCondition();
condition.setId(2L);
condition.setCodes(Arrays.asList("12", "13"));
condition.setFullName("2312312");
condition.setPrice(12.3);
goodsMapper.selectByCondition(condition);
}
默认是以实体中存在的属性且值不为空作为查询条件, 默认是 = 条件,
所以condition 中 codes 虽然有值, 但是实体中没这个属性, 所以不作为查询条件,
可以加 @Condition 注解改变默认条件 和匹配的实体属性
,将上面的注释打开,再次执行
可以看到 注解生效
本文代码已整理上传github
觉得有用的同学 给个 star 啊 ……^_^
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。