Mybatis extension — General mapper and dynamic resultmap

Time:2021-10-21

preface

The JPA used by the company in the past is very convenient. For new projects, I choose to use mybatis. SQL is written in XML files. Although the basic methods are generated by tools, once a field is added to the data, it is really uncomfortable to modify these methods. Moreover, I feel really tired from reading XML files. For a while, while the project is free, Study how to discard XML files, completely use annotations, and extract the general methods into a base class.

The code of this article has been sorted and uploaded to GitHub

How to implement basemapper < T >

General mapper generally includes basic additions, deletions and modifications. These methods are queried according to ID, a certain attribute and condition set.
When used, it inherits directly. Generics are concrete entity classes.
Mybatis provides @ insertprovider, @ selectprovider, etc. to dynamically generate SQL, so the general mapper uses these annotations.

The idea of dynamic SQL generation by general mapper is to get the class of the entity class, parse the metadata of the corresponding table according to the class, including table name, primary key information, database fields, etc., and dynamically generate SQL according to these information.

For insert and update, the parameters of the method are entity objects, which can be obtained directly by getClass (). However, for query and deletion, the parameters of the method are not entity objects,How to get the class object in the general mapper, the version before mybatis 3.4.5 can’t do this, which is limited from the source code. Refer to the issue of mybatis. From version 3.4.5, when calling the provider method, you can pass an additional parameter – providercontext, which can get the current mapper’s class and called method.

In this way, the generic parameter is obtained through the interface of the specific mapper. This generic parameter is the entity object, which is the specific value of T
The following is the specific implementation. The version of mybatis is 3.4.6, which depends on some spring tool classes

BaseMapper.java

public interface BaseMapper<Entity> {

    /**
     *Add a new record
     *
     *@ param entity entity
     *@ return affected records
     */
    @InsertProvider(type = BaseSqlProvider.class, method = "insert")
    @Options(useGeneratedKeys = true, keyColumn = "id")
    int insert(Entity entity);

    /**
     *Update a record
     *
     * @param entity entity
     *@ return affected records
     */
    @UpdateProvider(type = BaseSqlProvider.class, method = "update")
    int update(Entity entity);

    /**
     *Delete a record
     *
     * @param id id
     *@ return affected records
     */
    @DeleteProvider(type = BaseSqlProvider.class, method = "delete")
    int delete(Long id);

    /**
     *Query by ID
     *
     * @param id id
     * @return Entity
     */
    @SelectProvider(type = BaseSqlProvider.class, method = "selectById")
    Entity selectById(Long id);

    /**
     *Query a record by attribute
     *
     * @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);

    /**
     *Query record list according to attribute
     *
     * @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);

    /**
     *Query records according to query criteria
     *
     * @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()) {
            //Ignore primary key
            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()) {
            //Ignore primary key
            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();
    }
}

Some other classes

@Getter
public class TableMataDate {

    private static final Map<Class<?>, TableMataDate> TABLE_CACHE = new ConcurrentHashMap<>(64);

    /**
     *Table name
     */
    private String tableName;

    /**
     *Primary key attribute name
     */
    private String pkProperty;

    /**
     *Column name corresponding to primary key
     */
    private String pkColumn;

    /**
     *Map of attribute name and field name mapping relationship
     */
    private Map<String, String> fieldColumnMap;

    /**
     *Field type
     */
    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();
    }

    /**
     *According to the annotation initialization table information,
     *
     *@ param clazz class of entity 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) {

            //Filter static fields and fields with @ transient annotation
            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);

            //Primary key information: for fields with @ ID annotation, the default is class name + ID
            if (field.isAnnotationPresent(Id.class) || (property.equalsIgnoreCase("id") && pkProperty == null)) {
                pkProperty = property;
                pkColumn = columnName;
            }
            //Put the column corresponding to the field into the 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());
            }
        }
    }

}

The inconsistency between database fields and entity attributes is also an underline to hump. What should I do

When dynamic SQL is generated above, @ column, @ table, @ ID annotations can be added to the entity to ensure that the generated SQL is OK
However, for the query, after the query, it will be converted into an entity class. If the attributes do not correspond, the transferred entity will lack values. Mybatis also provides a class @ results annotation written on the method to define the mapping of entity attributes and database fields,

However, it is not elegant to write @ column on the entity class to represent the mapping relationship, and then write annotations on the methods,
So we needDynamically generate resultmap

The interface methods in mybatis will eventually generate mappedstaement corresponding to it. The information of database field and entity attribute mapping is also saved here, so you only need to modify the information in mappedstaement,

Mappedstaement can intercept the query method of the executor through the built-in interception mechanism of mybatis

The code is as follows, which has been sorted and uploaded to 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 and SQL are not processed
        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) {

    }
}

Use effect

New goodsmapper inherits 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;

}

query

Query according to an attribute of the entity, eg: query a commodity record according to the commodity code:

    @Test
    public void test4() {
        Goods goods = goodsMapper.selectByProperty(Goods::getCode, "2332");
    }

Mybatis extension -- General mapper and dynamic resultmap

Query according to query criteria*

New query criteria 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;

}

Query using the selectbycondition method of the generic mapper

    @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 extension -- General mapper and dynamic resultmap

The default is to use the attribute existing in the entity and the value is not empty as the query condition. The default is = condition,
Therefore, although codes in condition has a value, it does not have this attribute in the entity, so it is not used as a query condition,
Can add@The condition annotation changes the default condition and matching entity attributes, open the above comment and execute it again

Mybatis extension -- General mapper and dynamic resultmap

You can see that the annotation takes effect

The code of this article has been sorted and uploaded to GitHub

Students who feel useful give a star^_^

Recommended Today

Swift advanced (XV) extension

The extension in swift is somewhat similar to the category in OC Extension can beenumeration、structural morphology、class、agreementAdd new features□ you can add methods, calculation attributes, subscripts, (convenient) initializers, nested types, protocols, etc What extensions can’t do:□ original functions cannot be overwritten□ you cannot add storage attributes or add attribute observers to existing attributes□ cannot add parent […]