mybatis 拓展 -- 通用mapper 和 动态 resultMap

Nishinoshou 2019-06-28

前言

之前公司用的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");
    }

mybatis 拓展 -- 通用mapper 和 动态 resultMap

根据查询条件查询*

新建商品的查询条件 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);

    }

mybatis 拓展 -- 通用mapper 和 动态 resultMap

默认是以实体中存在的属性且值不为空作为查询条件, 默认是 = 条件,
所以condition 中 codes 虽然有值, 但是实体中没这个属性, 所以不作为查询条件,
可以加 @Condition 注解改变默认条件 和匹配的实体属性,将上面的注释打开,再次执行

mybatis 拓展 -- 通用mapper 和 动态 resultMap

可以看到 注解生效

本文代码已整理上传github

觉得有用的同学 给个 star 啊 ……^_^

相关推荐