MyBatis-X injects base CRUD, one-to-one, one-to-many join table query methods through custom annotation

Keywords: Mybatis SQL Java xml

Background

The advantages of MyBatis-X persistent layer architecture are simple configuration, flexible one-to-one, one-to-many join table queries. After a join table query configuration, when the related tables have added, changed or subtracted fields, the join table query does not need to be modified anymore and will be automatically modified according to the entity class corresponding to the modified tables.*

MyBatis is an excellent persistence layer framework that supports customization of SQL, stored procedures, and advanced mappings.MyBatis avoids almost all JDBC code and setting parameters manually and getting result sets.

However, due to the need for MyBatis to CRUD operations on each table, a number of persistent layer frameworks based on MyBatis have emerged, but these frameworks also lack the lack of one-to-one, one-to-many join queries. The author has learned and deeply understood the source code of MyBatis and MyBatis-Plus persistent layer frameworks by dao (mapper)Layer annotations are used to query joined tables (collectively referred to as MyBatis-X below), which simplifies development and minimizes the amount of xml MyBatis uses to manipulate databases.

2. MyBatis-X Source Parsing

MyBatis-X rewrites Mybatis XMLMapperBuilder to replace XMLMapperBuilder, primarily to replace content in the private method bindMapperForNamespace

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        configuration.addLoadedResource("namespace:" + namespace);
        
        //TODO start begins with custom Mapper injection
        InjectorMapperRegistry registry = new InjectorMapperRegistry((MybatisConfiguration) configuration);
        if (!registry.hasMapper(boundType)) {
            registry.addMapper(boundType);
        }
        //end
             
        configuration.addMapper(boundType);
      }
    }
  }
}

The above code is the bindMapperForNamespace content in MybatisXMLMapperBuilder, mainly to implement custom InjectorMapperRegistry annotation parsing registration class

/**
 * <p>
 * Injecting the underlying CRUD methods through annotations and one-to-one, one-to-many join table methods
 * <p>
 * @author yuyi (1060771195@qq.com)
 */
public class InjectorMapperRegistry {

    private MybatisConfiguration configuration;

    public InjectorMapperRegistry(MybatisConfiguration configuration) {
        this.configuration = configuration;
    }
    
    public <T> boolean hasMapper(Class<T> type) {
        return configuration.getMapperRegistry().hasMapper(type);
    }
    
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO Return Directly if Previous Injection
                return;
            }
            boolean loadCompleted = false;
            try {
                InjectorMapperAnnotationBuilder builder = new InjectorMapperAnnotationBuilder(configuration, type);
                builder.parse();
                
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                }
            }
        }
    }
}

Annotation resolution implementation class InjectorMapperAnnotationBuilder implemented in InjectorMapperRegistry

/**
 * <p>
 * Injecting the underlying CRUD methods through annotations and one-to-one, one-to-many join table methods
 * <p>
 * @author yuyi (1060771195@qq.com)
 */
public class InjectorMapperAnnotationBuilder extends MapperAnnotationBuilder {

    private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<>();
    // private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<>();

    private final MybatisConfiguration configuration;
    private final MapperBuilderAssistant assistant;
    private final Class<?> type;
    private final InjectorMapperAnnotationAssistant injectorMapperAssistant;
    
    public InjectorMapperAnnotationBuilder(MybatisConfiguration configuration, Class<?> type) {
        
        super(configuration, type);
        
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
        
        this.injectorMapperAssistant = new InjectorMapperAnnotationAssistant(configuration, assistant, type);

        sqlAnnotationTypes.add(Link.class);
    }
    
    public void parse() {
        String resource = type.toString();
        if (!InjectorConfig.isInjectorResource(resource)) {
            loadXmlResource();
            InjectorConfig.addInjectorResource(resource);
            assistant.setCurrentNamespace(type.getName());
            
            parseCache();
            parseCacheRef();
            
            // TODO injection CURD dynamic SQL (should be injected before table join comment)
            if (BaseDao.class.isAssignableFrom(type)) {
                //Write dead directly here without affecting the original CRUD injection
                ISqlInjector sqlInjector = new SoftSqlInjector();
                sqlInjector.inspectInject(assistant, type);
            }
            
            Method[] methods = type.getMethods();
            for (Method method : methods) {
                try {
                    // issue #237
                    if (!method.isBridge()) {
                        parseStatement(method);
                    }
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
    }
    
    public void parseStatement(Method method) {
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method);
        parseAnnotations(method, parameterTypeClass, languageDriver);
    }
    
    private void parseAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
          Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
          if (sqlAnnotationType != null) {
              Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
              Link link = (Link) sqlAnnotation;
              
              if (null != link) {
                  //Generate resultMap, generate corresponding sql
                  injectorMapperAssistant.parse(link, method);
              }
              
          } 
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
    }
}

The InjectorMapperAnnotationBuilder class inherits the MapperAnnotationBuilder class, primarily to override the parse() method.

// TODO injection CURD dynamic SQL (should be injected before table join comment)
if (BaseDao.class.isAssignableFrom(type)) {
    //Write dead directly here without affecting the original CRUD injection
    ISqlInjector sqlInjector = new SoftSqlInjector();
    sqlInjector.inspectInject(assistant, type);
}

If the persistence layer business Dao inherits the BaseDao class, the underlying CRUD, such as the SysUserDao class, is automatically injected: public interface SysUserDao extends BaseDao <SysUserVo, SysUserDto > {}, where SysUserVo is the newly modified parameter passed in, and SysUserDto is the returned query result.

private void parseAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      if (sqlAnnotationType != null) {
          Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
          Link link = (Link) sqlAnnotation;
          
          if (null != link) {
              //Generate resultMap, generate corresponding sql
              injectorMapperAssistant.parse(link, method);
          }
          
      } 
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
}

If the persistence layer business Dao inherits the BaseDao class and the custom method has the @Link annotation, the @Link annotation will be resolved and the corresponding sql generated

InjectorMapperAnnotationAssistant annotation parsing tool class that generates resultMap from annotations, as well as list and number of rows query methods.

public void parse(Link link, Method method) {    
    Class<?> returnType = method.getReturnType();
    if (returnType != Integer.class && returnType != Long.class) {
        if (link.printRm()) {
            logger.info(method.getName() + "--resultMap");
        }
        //Query List resultMap
        parseResultMap(link, method);
        if (link.print()) {
            logger.info(method.getName() + "--sql");
        }
        //Query List
        parseListSql(link, method);
    } else {
        //Number of queries
        parseCountSql(link, method);
    }
}

The InjectorMapperAnnotationAssistant.parse method uses the returnType class to query the list and the number of rows, respectively. Because the InjectorMapperAnnotationAssistant class has a lot of code, other methods do not copy and paste.

MyBatis is the core class above, and the key classes are analyzed below

/**
 * <p>
 * Connection parameters
 * <p>
 * 
 * @author yuyi (1060771195@qq.com)
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Link {
    
    /**
     * <p>
     * Mapping result map id
     * </p>
     */
    String resultMapId() default "";
    /**
     * <p>
     * One-to-one set
     * </p>
     */
    OneToOne[] ones() default {};
    /**
     * <p>
     * One-to-many set
     * </p>
     */
    OneToMany[] manys() default {};
    
    /**
     * <p>
     * Whether to print sql logs
     * </p>
     */
    boolean print() default false;
    
    /**
     * <p>
     * Whether to print the resultMap log
     * </p>
     */
    boolean printRm() default false;
    
}

resultMapId: If a value is set, it is set to Id when resultMap is generated. If not, a unique ID is automatically generated.

ones: A collection of one-to-one notes.

manys: A one-to-many collection of annotations.

print: defaults to false, if set to true, the query sql will be printed automatically at startup.

PritRm: Default is false, if set to true, the query resultMap will be printed automatically at startup.

print, printRm.Attributes are primarily for viewing the results of annotation resolution during the development phase.

package yui.comn.mybatisx.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import yui.comn.mybatisx.annotation.model.Clazz;
import yui.comn.mybatisx.annotation.model.JoinType;

/**
 * <p>
 * One-to-one Annotation Configuration Class
 * <p>
 * 
 * @author yuyi (1060771195@qq.com)
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OneToOne {
    
    /**
     * <p>
     * Join table query left table corresponding entity class, optional, default is current Dao object
     * </p>
     */
    Class<?> leftClass() default Clazz.class;
    
    /**
     * <p>
     * Joint table query right table corresponding entity class, required
     * </p>
     */
    Class<?> rightClass();
    
    /**
     * <p>
     * Table Query Left Object Alias, Optional, Default Current leftClass Object Name
     * </p>
     */
    String leftAlias() default "";
    
    /**
     * <p>
     * Table Query Right Object Alias, Optional, Default Current rightClass Object Name
     * </p>
     */
    String rightAlias() default "";
    
    /**
     * <p>
     * Join table query mode, optional default is inner join query
     * </p>
     * {@link JoinType}
     */
    JoinType joinType() default JoinType.INNER;
    
    /**
     * <p>
     * Left table join field, optional, default is leftClass primary key
     * </p>
     */
    String leftColumn() default "";
    
    /**
     * <p>
     * Right table join field, optional, default is leftClass primary key
     * </p>
     */
    String rightColumn() default "";
    
    /**
     * <p>
     * If left or right, the name of the parameter required in on
     * </p>
     */
    String onArgName() default "";
    
}
package yui.comn.mybatisx.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import yui.comn.mybatisx.annotation.model.Clazz;

/**
 * <p>
 * One-to-many Annotation Configuration Class
 * <p>
 * 
 * @author yuyi (1060771195@qq.com)
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OneToMany {
    
    /**
     * <p>
     * Join table query left table corresponding entity class, optional, default is current Dao object
     * </p>
     */
    Class<?> leftClass() default Clazz.class;
    
    /**
     * <p>
     * Table Query Left Object Alias, Optional, Default Current leftClass Object Name
     * </p>
     */
    String leftAlias() default "";
    
    /**
     * <p>
     * Join field in one-to-many, one, optional, default is leftClass primary key
     * </p>
     */
    String leftColumn();
    
    /**
     * <p>
     * One-to-many, many entity classes, required
     * </p>
     */
    Class<?> ofTypeClass();
    /**
     * <p>
     * One-to-many, many entity classes, optional, get rightClass class by default through ofTypeClass
     * </p>
     */
    Class<?> rightClass() default Clazz.class;
    
    /**
     * <p>
     * One-to-many, multi-in query list alias, optional, default current rightClass object name
     * </p>
     */
    String rightAlias() default "";
    
    /**
     * <p>
     * One-to-many, multi-in join field, optional, default rightClass primary key
     * </p>
     */
    String rightColumn() default "";
    
    /**
     * <p>
     * One-to-many, many entity object names
     * </p>
     */
    String property();
    
    /**
     * <p>
     * One-to-many, one-to-one set in many
     * </p>
     */
    OneToOne[] ones() default {};

}
/**
 * <p>
 * Mapper After inheriting the interface, you can get CRUD functionality without writing a mapper.xml file
 * </p>
 * <p>
 * This Mapper supports id generics
 * </p>
 *
 * @author yuyi (1060771195@qq.com)
 */
public interface BaseDao<V, D> {

    /**
     * <p>
     * Insert a record
     * </p>
     *
     * @param entity Entity Object
     */
    int insert(V entity);

    /**
     * <p>
     * Modify all according to ID
     * </p>
     *
     * @param entity Entity Object
     */
    int update(@Param(Constants.ENTITY) V entity, @Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * Modify Valued Based on ID
     * </p>
     *
     * @param entity Entity Object
     */
    int updateById(@Param(Constants.ENTITY) V entity);
    
    /**
     * <p>
     * Modify all values based on ID
     * </p>
     *
     * @param entity Entity Object
     */
    int updateAllById(@Param(Constants.ENTITY) V entity);

    /**
     * <p>
     * Delete by ID
     * </p>
     *
     * @param id Primary Key ID
     */
    int deleteById(Serializable id);

    /**
     * <p>
     * Delete (batch delete based on ID)
     * </p>
     *
     * @param idList Primary Key ID List (cannot be null and empty)
     */
    int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
    
    /**
     * <p>
     * Query by ID
     * </p>
     *
     * @param id Primary Key ID
     */
    D getById(Serializable id);
    
    /**
     * <p>
     * Query a record based on entity criteria
     * </p>
     *
     * @param wrapper Entity Object
     */
    D get(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * Query (according to columnMap criteria)
     * </p>
     *
     * @param columnMap Table field map object
     */
    D getByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
    
    /**
     * <p>
     * Query a record based on field conditions
     * </p>
     *
     * @param entity Entity Object
     */
    default D get(String colomn, Object value) {
        Map<String, Object> columnMap = new HashMap<>();
        columnMap.put(colomn, value);
        return getByMap(columnMap);
    }
    
    /**
     * <p>
     * Query total records based on Wrapper criteria
     * </p>
     *
     * @param wrapper Entity Object
     */
    Integer count(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * Query all records according to entity criteria
     * </p>
     *
     * @param wrapper Entity object encapsulation operation class (null can be used)
     */
    List<D> list(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * Queries (batch queries based on ID)
     * </p>
     *
     * @param idList Primary Key ID List (cannot be null and empty)
     */
    List<D> listBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

    /**
     * <p>
     * Query (according to columnMap criteria)
     * </p>
     *
     * @param columnMap Table field map object
     */
    List<D> listByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
    
    /**
     * <p>
     * Query all records based on field conditions
     * </p>
     *
     * @param entityList Entity Object Collection
     */
    default List<D> list(String colomn, Object value) {
        Map<String, Object> columnMap = new HashMap<>();
        columnMap.put(colomn, value);
        return listByMap(columnMap);
    }

    /**
     * <p>
     * Query all records based on Wrapper criteria
     * </p>
     *
     * @param wrapper Entity object encapsulation operation class (null can be used)
     */
    List<Map<String, Object>> listMaps(@Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * Query all records based on Wrapper criteria
     * Note: Only the value of the first field is returned
     * </p>
     *
     * @param wrapper Entity object encapsulation operation class (null can be used)
     */
    List<Object> listObjs(@Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * Query all records (and page over) according to entity criteria
     * </p>
     *
     * @param page         Paging query criteria (RowBounds.DEFAULT can be used)
     * @param wrapper Entity object encapsulation operation class (null can be used)
     */
    IPage<D> page(IPage<D> page, @Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * Query all records (and page over) according to Wrapper criteria
     * </p>
     *
     * @param page         Paging Query Criteria
     * @param wrapper Entity Object Encapsulation Action Class
     */
    IPage<Map<String, Object>> pageMaps(IPage<D> page, @Param(Constants.WRAPPER) Wrapper<V> wrapper);
}
/**
 * <p>
 * SQL Logical Delete Injector
 * </p>
 *
 * @author yuyi (1060771195@qq.com)
 */
public class SoftSqlInjector extends AbstractSqlInjector {


    @Override
    public List<AbstractMethod> getMethodList() {
        return Stream.of(
            new Insert(),
            new LogicDelete(),
            new LogicDeleteByMap(),
            new LogicDeleteById(),
            new LogicDeleteBatchByIds(),
            new SoftUpdateAllById(),
            new LogicUpdateById(),
            new LogicUpdate(),
            new SoftCount(),
            new SoftGet(),
            new SoftGetById(),
            new SoftGetByMap(),
            new SoftList(),
            new SoftListByMap(),
            new SoftListBatchIds(),
            new SoftListObjs(),
            new SoftListMaps(),
            new SoftPage(),
            new SoftPageMaps()
        ).collect(Collectors.toList());
    }

}

Corresponding directory structure

MyBatis-X injects the core classes of basic CRUD, one-to-one, one-to-many join table queries by customizing annotations. The next section configures one-to-one, one-to-many join table queries, annotates the resolution results, and displays the query results.

 

 

 

Posted by pfdesigns on Sun, 22 Sep 2019 19:16:26 -0700