mybatis 拓展 -- 通用mapper 和 動態 resultMap

前言

以前公司用的jpa, 我的感受很方便, 新的項目選擇使用mybatis, sql都是寫在xml文件裏, 雖然基本的方法都有工具生成, 可是一旦數據增長一個字段, 修改這些方法真的是不爽, 並且我的看xml文件感受是真的累, 就這樣不爽裏一段時間, 趁着項目空閒的時候, 研究下如何拋棄xml文件, 徹底使用註解的方式, 而且把通用的方法抽出到一個基類中。java

本文代碼已整理上傳githubgit

如何實現BaseMapper<T>

通用mapper通常包含基本的增刪改, 根據id查, 根據某一個屬性查, 根據條件集合查 這些方法。
使用的時候直接繼承, 泛型就是具體的實體類。
mybatis 提供了@InsertProvider, @SelectProvider等來動態生成sql, 因此通用mapper就是使用的這些註解。github

通用mapper動態生成sql的思路就是拿到實體類的class, 根據class解析出對應表的元數據, 包括表名, 主鍵信息, 數據庫字段等, 在根據這些信息動態生成sql。spring

對於insert和update來講, 方法的參數就是實體對象, 直接getClass()就能拿到, 可是對於查詢和刪除,方法的參數就不是實體對象了, 在通用mapper裏怎麼拿到 class對象, mybatis 3.4.5 以前的版本是作不到的, 從源碼裏就限制了, 參考mybatis的這個issue, 3.4.5版本開始, 在調用provider方法時 能夠多傳遞一個參數-ProviderContext, 這個ProviderContext 就能夠獲取當前具體是哪一個mapper的class和調用的方法。sql

這樣經過 具體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註解寫在方法上, 來自定義實體屬性和數據庫字段的映射, mybatis

可是都已經在實體類上寫上@Column表示映射關係來,再在方法上寫註解,很不雅觀,
因此咱們須要動態生成ResultMapapp

mybatis裏的接口方法 最終都會生成 MappedStaement 與之對應, 數據庫字段和實體屬性映射的信息也是保存在這裏的, 因此只須要修改 MappedStaement 裏的信息就能夠了,ide

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");
    }

clipboard.png

根據查詢條件查詢*

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

    }

clipboard.png

默認是以實體中存在的屬性且值不爲空做爲查詢條件, 默認是 = 條件,
因此condition 中 codes 雖然有值, 可是實體中沒這個屬性, 因此不做爲查詢條件,
能夠加 @Condition 註解改變默認條件 和匹配的實體屬性,將上面的註釋打開,再次執行

clipboard.png

能夠看到 註解生效

本文代碼已整理上傳github

以爲有用的同窗 給個 star 啊 ……^_^

相關文章
相關標籤/搜索