mybatis 拓展 -- 通用mapper 和 动态 resultMap
前言
之前公司用的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 啊 ……^_^