Bootstrap

MyBatis Link(MyBatis Plus X) 通过自定义注解方式,注入基础CRUD,一对一,一对多连表查询方法(1)

一. 背景

    MyBatis Link(MyBatis Plus X)持久层架构的优点:配置简单,一对一,一对多等连表查询灵活,在一个连表查询配置后,其中相关表有增改减字段时,不需要再去修改连表查询,会根据修改表对应的实体类自动进行修改。    

    MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

    但是由于MyBatis需要对每张表进行CRUD操作,应运而生了很多基于MyBatis的持久层框架,但也是这些框架缺乏一对一,一对多等联查查询的缺失,作者对MyBatis以及MyBatis-Plus持久层框架源码的学习和深入理解,通过对dao(mapper)层注解的方式来进行连表查询(下文统称为MyBatis Link),继而简化开发,尽量减少MyBatis通过xml的方式来操作数据库。

二. MyBatis Link源码解析

    MyBatis Link重写了MybatisXMLMapperBuilder替换XMLMapperBuilder,主要是替换私有方法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 先进行自定义Mapper注入
        InjectorMapperRegistry registry = new InjectorMapperRegistry((MybatisConfiguration) configuration);
        if (!registry.hasMapper(boundType)) {
            registry.addMapper(boundType);
        }
        //end
             
        configuration.addMapper(boundType);
      }
    }
  }
}

    以上代码是MybatisXMLMapperBuilder中的bindMapperForNamespace内容,主要是为了实现自定义InjectorMapperRegistry 注解解析注册类

/**
 * <p>
 * 通过注解的方式注入基础CRUD方法,和一对一, 一对多连表方法
 * <p>
 * @author yuyi ([email protected])
 */
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;
            }
            boolean loadCompleted = false;
            try {
                InjectorMapperAnnotationBuilder builder = new InjectorMapperAnnotationBuilder(configuration, type);
                builder.parse();
                
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                }
            }
        }
    }
}

    InjectorMapperRegistry中实现了注解解析实现类InjectorMapperAnnotationBuilder 

/**
 * <p>
 * 通过注解的方式注入基础CRUD方法,和一对一, 一对多连表方法
 * <p>
 * @author yuyi ([email protected])
 */
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 注入 CURD 动态 SQL (应该在连表注解之前注入)
            if (BaseDao.class.isAssignableFrom(type)) {
                //这里直接写死,不影响原有CRUD注入
                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) {
                  //生成resultMap,生成相应的sql
                  injectorMapperAssistant.parse(link, method);
              }
              
          } 
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
    }
}

    InjectorMapperAnnotationBuilder 类继承了MapperAnnotationBuilder 类,主要是为了覆写parse()方法。

// TODO 注入 CURD 动态 SQL (应该在连表注解之前注入)
if (BaseDao.class.isAssignableFrom(type)) {
    //这里直接写死,不影响原有CRUD注入
    ISqlInjector sqlInjector = new SoftSqlInjector();
    sqlInjector.inspectInject(assistant, type);
}

    如果持久层业务Dao继承BaseDao类,会自动注入基础CRUD,如SysUserDao类,:public interface SysUserDao extends BaseDao<SysUserVo, SysUserDto> {},其中SysUserVo是在新增修改传入的参数,SysUserDto是返回的查询结果。

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) {
              //生成resultMap,生成相应的sql
              injectorMapperAssistant.parse(link, method);
          }
          
      } 
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
}

    如果持久层业务Dao继承BaseDao类,且自定义方法有@Link注解,就会去解析@Link注解,并生成相应的sql

InjectorMapperAnnotationAssistant注解解析工具类,通过注解生成resultMap,以及列表查询方法和行数查询方法。

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");
        }
        //查询列表resultMap
        parseResultMap(link, method);
        if (link.print()) {
            logger.info(method.getName() + "--sql");
        }
        //查询列表
        parseListSql(link, method);
    } else {
        //查询条数
        parseCountSql(link, method);
    }
}

   InjectorMapperAnnotationAssistant.parse方法通过returnType 类来分别进行列表查询和行数查询,由于InjectorMapperAnnotationAssistant类代码众多,其他方法就不进行复制粘贴了。

    以上MyBatis为核心类,下面来分析其中关键类

/**
 * <p>
 * 连接参数
 * <p>
 * 
 * @author yuyi ([email protected])
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Link {
    
    /**
     * <p>
     * 映射result map id
     * </p>
     */
    String resultMapId() default "";
    /**
     * <p>
     * 一对一集合
     * </p>
     */
    OneToOne[] ones() default {};
    /**
     * <p>
     * 一对多集合
     * </p>
     */
    OneToMany[] manys() default {};
    
    /**
     * <p>
     * 是否打印sql日志
     * </p>
     */
    boolean print() default false;
    
    /**
     * <p>
     * 是否打印resultMap日志
     * </p>
     */
    boolean printRm() default false;
    
}

    resultMapId:如果设值,生成resultMap时,就用该值设置为Id,如果不设置,系统自动生成一个唯一id。

    ones:一对一注解集合。

    manys:一对多注解集合。

    print:默认为false,如果设置为true,启动的时候会自动打印查询sql。

    printRm:默认为false,如果设置为true,启动的时候会自动打印查询resultMap。

    print,printRm。属性主要是为了在开发阶段对注解解析结果进行查看。

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>
 * 一对一 注解配置类
 * <p>
 * 
 * @author yuyi ([email protected])
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OneToOne {
    
    /**
     * <p>
     * 连表查询左表对应实体类,选填,默认就是当前Dao中的对象
     * </p>
     */
    Class<?> leftClass() default Clazz.class;
    
    /**
     * <p>
     * 连表查询右表对应实体类,必填
     * </p>
     */
    Class<?> rightClass();
    
    /**
     * <p>
     * 连表查询左对象别名,选填,默认当前leftClass类对象名
     * </p>
     */
    String leftAlias() default "";
    
    /**
     * <p>
     * 连表查询右对象别名,选填,默认当前rightClass类对象名
     * </p>
     */
    String rightAlias() default "";
    
    /**
     * <p>
     * 连表查询方式,选填默认为内连接查询
     * </p>
     * {@link JoinType}
     */
    JoinType joinType() default JoinType.INNER;
    
    /**
     * <p>
     * 左表连接字段,选填,默认就是leftClass主键
     * </p>
     */
    String leftColumn() default "";
    
    /**
     * <p>
     * 右表连接字段,选填,默认就是leftClass主键
     * </p>
     */
    String rightColumn() default "";
    
    /**
     * <p>
     * 如果是左连接或者右连接,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>
 * 一对多 注解配置类
 * <p>
 * 
 * @author yuyi ([email protected])
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OneToMany {
    
    /**
     * <p>
     * 连表查询左表对应实体类,选填,默认就是当前Dao中的对象
     * </p>
     */
    Class<?> leftClass() default Clazz.class;
    
    /**
     * <p>
     * 连表查询左对象别名,选填,默认当前leftClass类对象名
     * </p>
     */
    String leftAlias() default "";
    
    /**
     * <p>
     * 一对多,一中的连接字段,选填,默认就是leftClass主键
     * </p>
     */
    String leftColumn();
    
    /**
     * <p>
     * 一对多,多的实体类, 必填
     * </p>
     */
    Class<?> ofTypeClass();
    /**
     * <p>
     * 一对多,多的实体类,选填, 默认通过ofTypeClass来获取rightClass类
     * </p>
     */
    Class<?> rightClass() default Clazz.class;
    
    /**
     * <p>
     * 一对多,多中查询列表别名,选填,默认当前rightClass类对象名
     * </p>
     */
    String rightAlias() default "";
    
    /**
     * <p>
     * 一对多,多中的连接字段,选填,默认就是rightClass主键
     * </p>
     */
    String rightColumn() default "";
    
    /**
     * <p>
     * 一对多,多的实体对象名称
     * </p>
     */
    String property();
    
    /**
     * <p>
     * 一对多,多中可以进行一对一集合
     * </p>
     */
    OneToOne[] ones() default {};

}
/**
 * <p>
 * Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
 * </p>
 * <p>
 * 这个 Mapper 支持 id 泛型
 * </p>
 *
 * @author yuyi ([email protected])
 */
public interface BaseDao<V, D> {

    /**
     * <p>
     * 插入一条记录
     * </p>
     *
     * @param entity 实体对象
     */
    int insert(V entity);

    /**
     * <p>
     * 根据 ID 修改全部的
     * </p>
     *
     * @param entity 实体对象
     */
    int update(@Param(Constants.ENTITY) V entity, @Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * 根据 ID 修改有值的
     * </p>
     *
     * @param entity 实体对象
     */
    int updateById(@Param(Constants.ENTITY) V entity);
    
    /**
     * <p>
     * 根据 ID 修改所有值
     * </p>
     *
     * @param entity 实体对象
     */
    int updateAllById(@Param(Constants.ENTITY) V entity);

    /**
     * <p>
     * 根据 ID 删除
     * </p>
     *
     * @param id 主键ID
     */
    int deleteById(Serializable id);

    /**
     * <p>
     * 删除(根据ID 批量删除)
     * </p>
     *
     * @param idList 主键ID列表(不能为 null 以及 empty)
     */
    int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
    
    /**
     * <p>
     * 根据 ID 查询
     * </p>
     *
     * @param id 主键ID
     */
    D getById(Serializable id);
    
    /**
     * <p>
     * 根据 entity 条件,查询一条记录
     * </p>
     *
     * @param wrapper 实体对象
     */
    D get(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * 查询(根据 columnMap 条件)
     * </p>
     *
     * @param columnMap 表字段 map 对象
     */
    D getByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
    
    /**
     * <p>
     * 根据字段条件,查询一条记录
     * </p>
     *
     * @param entity 实体对象
     */
    default D get(String colomn, Object value) {
        Map<String, Object> columnMap = new HashMap<>();
        columnMap.put(colomn, value);
        return getByMap(columnMap);
    }
    
    /**
     * <p>
     * 根据 Wrapper 条件,查询总记录数
     * </p>
     *
     * @param wrapper 实体对象
     */
    Integer count(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * 根据 entity 条件,查询全部记录
     * </p>
     *
     * @param wrapper 实体对象封装操作类(可以为 null)
     */
    List<D> list(@Param(Constants.WRAPPER) Wrapper<V> wrapper);
    
    /**
     * <p>
     * 查询(根据ID 批量查询)
     * </p>
     *
     * @param idList 主键ID列表(不能为 null 以及 empty)
     */
    List<D> listBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

    /**
     * <p>
     * 查询(根据 columnMap 条件)
     * </p>
     *
     * @param columnMap 表字段 map 对象
     */
    List<D> listByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
    
    /**
     * <p>
     * 根据字段条件,查询全部记录
     * </p>
     *
     * @param entityList 实体对象集合
     */
    default List<D> list(String colomn, Object value) {
        Map<String, Object> columnMap = new HashMap<>();
        columnMap.put(colomn, value);
        return listByMap(columnMap);
    }

    /**
     * <p>
     * 根据 Wrapper 条件,查询全部记录
     * </p>
     *
     * @param wrapper 实体对象封装操作类(可以为 null)
     */
    List<Map<String, Object>> listMaps(@Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * 根据 Wrapper 条件,查询全部记录
     * 注意: 只返回第一个字段的值
     * </p>
     *
     * @param wrapper 实体对象封装操作类(可以为 null)
     */
    List<Object> listObjs(@Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * 根据 entity 条件,查询全部记录(并翻页)
     * </p>
     *
     * @param page         分页查询条件(可以为 RowBounds.DEFAULT)
     * @param wrapper 实体对象封装操作类(可以为 null)
     */
    IPage<D> page(IPage<D> page, @Param(Constants.WRAPPER) Wrapper<V> wrapper);

    /**
     * <p>
     * 根据 Wrapper 条件,查询全部记录(并翻页)
     * </p>
     *
     * @param page         分页查询条件
     * @param wrapper 实体对象封装操作类
     */
    IPage<Map<String, Object>> pageMaps(IPage<D> page, @Param(Constants.WRAPPER) Wrapper<V> wrapper);
}
/**
 * <p>
 * SQL 逻辑删除注入器
 * </p>
 *
 * @author yuyi ([email protected])
 */
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());
    }

}

   相应的目录结构

    以上是MyBatis Link通过自定义注解的方式,注入基础CRUD,一对一,一对多连表查询的核心类,下一节会对一对一,一对多连表查询进行配置,注解解析结果,以及查询结果进行展示。

;