preface

The jPA used by the company before feels very convenient, but the new project chooses to use Mybatis. SQL is written in XML files. Although basic methods are generated by tools, it is really uncomfortable to modify these methods once a field is added to the data, and I feel really tired when I read XML files. For a while, while the project is idle, explore how to discard XML files, use annotations entirely, and pull out common methods into a base class.

The code has been uploaded to Github

How to implement BaseMapper<T>

A universal mapper generally contains basic additions, deletions, and changes, by ID, by an attribute, and by a set of conditions. When used by direct inheritance, generics are concrete entity classes. Mybatis provides @insertProvider, @selectProvider, etc. to dynamically generate SQL, so the general mapper uses these annotations.

General mapper dynamic generation OF SQL ideas is to get the entity class, according to the class to parse out the corresponding table metadata, including table name, primary key information, database fields, etc., in accordance with these information dynamic generation SQL.

For inserts and updates, the method arguments are entity objects, which can be obtained by getClass(). For queries and deletes, the method arguments are not entity objects. Since version 3.4.5, we can pass an additional parameter — ProviderContext when calling provider methods. The ProviderContext then gets the class and method of the current mapper.

Mybatis version is 3.4.6, which relies on some spring utility classes

BaseMapper<Entity>.java

public interface BaseMapper<Entity> {

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

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

    /** * Delete a record **@param id id
     * @returnAffected 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 the list of records by 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);


}
Copy the code

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 the 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 the 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 instanceofCollection) { 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();
    }

    privateClass<? > 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 + "[j] {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(); }}Copy the code

Some other classes

@Getter
public class TableMataDate {

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

    /** * table name */
    private String tableName;

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

    /** * the column name corresponding to the primary key */
    private String pkColumn;

    /** * Map of attribute and field names */
    private Map<String, String> fieldColumnMap;

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

    /** * Initializes table information based on annotations, **@paramClazz 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 @TRANSIENT fields
            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: @id annotated field, no default class name +Id
            if (field.isAnnotationPresent(Id.class) || (property.equalsIgnoreCase("id") && pkProperty == null)) {
                pkProperty = property;
                pkColumn = columnName;
            }
            // Put the corresponding column of 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()); }}}}Copy the code

Database field and entity attribute inconsistent also underline hump how to do

You can add @column, @table, @ID annotation to the entity to make sure that the SQL generated is ok, but for lookup, you need to convert it to the entity class, if the attributes don’t correspond, the entity generated will be missing values, Mybatis also provides class @Results annotations written on methods to define mappings between entity attributes and database fields,

However, it is unsightly to write annotations on methods after putting @Column on the entity class to represent the mapping, so we need to dynamically generate ResultMap

Mybatis interface methods will eventually generate the corresponding MappedStaement, database field and entity attribute mapping information is also stored here, so you only need to modify the information in MappedStaement.

MappedStaement can be obtained by intercepting the Executor query method using mybatis’s built-in interception mechanism

The following code has been collated 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 SQL is not processed
        if (ms.getResource().contains(".xml")) {
            return invocation.proceed();
        }
        ResultMap resultMap = ms.getResultMaps().iterator().next();
        if(! CollectionUtils.isEmpty(resultMap.getResultMappings())) {returninvocation.proceed(); } Class<? > mapType = resultMap.getType();if (ClassUtils.isAssignable(mapType, Collection.class)) {
            returninvocation.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) {}}Copy the code

Use effect

Create GoodsMapper to inherit BaseMapper

public interface GoodsMapper extends BaseMapper<Goods> {}Copy the code
@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;

}
Copy the code

The query

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

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

Query * according to the query condition

Query condition GoodCondition of a new commodity

@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;

}
Copy the code

Use the selectByCondition method of the universal Mapper

    @Test
    public void test3(a) {

        GoodsCondition condition = new GoodsCondition();
        condition.setId(2L);
        condition.setCodes(Arrays.asList("12"."13"));
        condition.setFullName("2312312");
        condition.setPrice(12.3);

        goodsMapper.selectByCondition(condition);

    }
Copy the code

By default, the attribute that exists in the entity and its value is not empty is used as the query condition. By default, it is = condition. Therefore, although codes in condition have values, they do not exist in the entity, so they are not used as the query condition. You can change the default Condition and matching entity attributes with the @condition annotation, open the comment above, and execute again

You can see the annotations in effect

The code has been uploaded to Github

Give a star to those who think it is useful… ^_^