Mybatis通用Mapper应用


通用Mapper

日期:2019-06-25

目录:

  • 概述
  • 集成(spring-boot)
    • 1. 引入依赖包
    • 2. 配置yml
    • 3. MapperScan
  • 应用案例
    • 1. 实体类
    • 2. 字段类型处理器
    • 3. Mapper使用
    • 4. 扩展自己Mapper
      • 4.1 实现SelectResultProvider类
      • 4.2 自动关联Mapper实现
    • 5. Weekend动态查询使用
  • 通用Mapper实现原理
    • 1. Mybatis架构图
    • 2. Mapper 原理

概述

通用Mapper就是为了解决单表增删改查,基于Mybatis的插件。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。

集成(spring-boot)

1. 引入依赖包

  • 引用通用Mapper

     
        tk.mybatis
        mapper-spring-boot-starter
        2.1.2
     
        
    

2. 配置yml


 mybatis:
   type-aliases-package: com.suixingpay.udip.manager.core #领域对象扫描路径
   mapper-locations: classpath:mapper/*.xml
   type-handlers-package: com.suixingpay.udip.manager.core.handler #字段为枚举类型的Handler
 mapper:
   mappers:
     - com.suixingpay.udip.common.mapper.BaseMapper #mapper的父接口
   not-empty: true  #insert和update中,是否判断字符串类型!='',少数方法会用到
   identity: MYSQL
   enum-as-simple-type: true  # 允许bean 接受 enum 类型
    

3. MapperScan

  • mapper扫描路径

    @MapperScan(basePackages = "com.suixingpay.udip.manager.core")
    
  • 说明

  1. mapperScan为tk.mybatis.spring.annotation.MapperScan而不是org.mybatis.spring.annotation.MapperScan
  2. 不能扫描到mapper的基础和自定义接口,比如com.suixingpay.udip.common.mapper.BaseMapper等。

应用案例

1. 实体类

  • Person实体类

    
    @Data
    public class Person implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        private Integer age;
    
        @ColumnType(typeHandler = BaseHandler.class)
        private StatusEnum status;
    
        @ColumnType(typeHandler = BaseHandler.class)
        private AuthEnum role;
    
        private Long countryId;
    
        @Transient
        private Country country;
    
        @Transient
        private List personAddresses;
    
    }
    
    
  • 说明

  1. 实体字段值映射到数据库字段,采用驼峰字段映射;
  2. 主键字段使用@id注解;
  3. 非数据库字段使用@Transient标注;
  4. 枚举类型使用@ColumnType 注解标注;并指明Hanler处理器;

2. 字段类型处理器

  • 通用枚举类型BaseHandler
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = {StatusEnum.class, AuthEnum.class})
public class BaseHandler extends BaseTypeHandler {

    private Class types;

    public BaseHandler(Class types) {
        this.types = types;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, EnumType parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getValue());
    }

    @Override
    public EnumType getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int id = rs.getInt(columnName);
        if (rs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }

    @Override
    public EnumType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int id = rs.getInt(columnIndex);
        if (rs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }

   private EnumType getEnumType(int id) {
        try {
            Method valueOfType = Arrays.stream(types.getDeclaredMethods())
                    .filter(m -> m.getName().equals("valueOfType"))
                    .findFirst()
                    .orElse(null);
            return (EnumType) ReflectionUtils.invokeMethod(valueOfType, types.getEnumConstants()[0], id);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert to " + types.getName() + " by ordinal value.", ex);
        }
    }

    @Override
    public EnumType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int id = cs.getInt(columnIndex);
        if (cs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }
}

  • 说明
  1. class使用注解@MappedJdbcTypes
    @MappedTypes,并继承BaseTypeHandler;
  2. 枚举类要实现接口EnumType,该接口valueOfType用反射来获取实例;
  3. 该类主要就是对PreparedStatementResultSet设值和获取值,从数据库到java有个类型映射问题;
  4. 该类型是在SqlSessionFactoryBean类中,创建
    SqlSessionFactory时注册字段映射类型;
    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
     ......
        if (hasLength(this.typeHandlersPackage)) {
          String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
              ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
          for (String packageToScan : typeHandlersPackageArray) {
            configuration.getTypeHandlerRegistry().register(packageToScan);
            if (LOGGER.isDebugEnabled()) {
              LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
            }
          }
        }
    
        if (!isEmpty(this.typeHandlers)) {
          for (TypeHandler
  1. 该handler会在DefaultResultSetHandlerle类中处理ResultMap时创建返回值的java对象时使用:
public class DefaultResultSetHandler implements ResultSetHandler{
.......
  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List> constructorArgTypes, List constructorArgs, String columnPrefix) throws SQLException {
        final Class resultType = resultMap.getType();
        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
        final List constructorMappings = resultMap.getConstructorResultMappings();
        if (hasTypeHandlerForResultObject(rsw, resultType)) {
            return  createPrimitiveResultObject(rsw, resultMap, columnPrefix); 
        } else if (!constructorMappings.isEmpty()) {
            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
            return objectFactory.create(resultType);
        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
            return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
        }
        throw new ExecutorException("Do not know how to create an instance of " + resultType);
    }  
.......
} 
  
 
 
  1. 已经存在的基础类型映射在SimpleTypeUtil中;
static {
        SIMPLE_TYPE_SET.add(byte[].class);
        SIMPLE_TYPE_SET.add(String.class);
        SIMPLE_TYPE_SET.add(Byte.class);
        SIMPLE_TYPE_SET.add(Short.class);
        SIMPLE_TYPE_SET.add(Character.class);
        SIMPLE_TYPE_SET.add(Integer.class);
        SIMPLE_TYPE_SET.add(Long.class);
        SIMPLE_TYPE_SET.add(Float.class);
        SIMPLE_TYPE_SET.add(Double.class);
        SIMPLE_TYPE_SET.add(Boolean.class);
        SIMPLE_TYPE_SET.add(Date.class);
        SIMPLE_TYPE_SET.add(Timestamp.class);
        SIMPLE_TYPE_SET.add(Class.class);
        SIMPLE_TYPE_SET.add(BigInteger.class);
        SIMPLE_TYPE_SET.add(BigDecimal.class);
        //反射方式设置 java8 中的日期类型
        for (String time : JAVA8_DATE_TIME) {
            registerSimpleTypeSilence(time);
        }
    }    

3. Mapper使用

  • AddresMapper实现
@org.apache.ibatis.annotations.Mapper
public interface AddresMapper extends BaseMapper {
}

  • 说明
  1. 继承BaseMapper,就实现了该类CRUD及复杂查询相关操作;

4. 扩展自己Mapper

@RegisterMapper
public interface ResultMapper {
    @SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
    List selectByExample2Result(Object example);
}
  • 说明
  1. 使用注解@RegisterMapper,在创建SqlSessionFactory时,会自动注入该类;
  2. 该类不能被MapperScan扫描到,主要是因为需要获取到范型中实体类型;

4.1 实现SelectResultProvider类

public class SelectResultProvider extends MapperTemplate {
    public SelectResultProvider(Class mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }

    public String selectByExample2Result(MappedStatement ms) {
        Class entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder("SELECT ");
        if (isCheckExampleEntityClass()) {
            sql.append(SqlHelper.exampleCheck(entityClass));
        }
        sql.append("distinct");
        //支持查询指定列
        sql.append(SqlHelper.exampleSelectColumns(entityClass));
        sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
        sql.append(SqlHelper.exampleWhereClause());
        sql.append(SqlHelper.exampleOrderBy(entityClass));
        sql.append(SqlHelper.exampleForUpdate());
        return sql.toString();
    }
}
  • 说明
  1. 继承MapperTemplate,拼装SQL;
  2. 去掉返回类型setResultType(ms, entityClass),而是采用ResultMap("id")进行自动关联查询;

4.2 自动关联Mapper实现

@org.apache.ibatis.annotations.Mapper
public interface PersonMapper extends Mapper{

    @Select("select  * from person u where u.id = #{id}")
    @Results(id = "personResultMap",
            value = {
                    @Result(id = true, property = "id", column = "id"),
                    @Result(property = "countryId", column = "country_id"),
                    @Result(property = "country",
                            column = "country_id",
                            one = @One(select = "mybatis.example.domain.country.CountryMapper.selectByPrimaryKey", fetchType = FetchType.EAGER))
                    ,
                    @Result(property = "personAddresses",
                            column = "id",
                            many = @Many(select = "mybatis.example.domain.addres.PersonAddressMapper.selectByUserId", fetchType = FetchType.EAGER))
            }
    )
    Person getPersonById(@Param("id") Long id);


    @ResultMap("personResultMap")
    @SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
    List selectByExample2Result(Object example);


}

  • 说明
  1. 在方法selectByExample2Result上增加注解@ResultMap("personResultMap"),实现自动关联功能;
  2. 可以根据需要写复杂SQL@Select("select * from person u where u.id =#{id}")来实现特殊需求;

5. Weekend动态查询使用

    public void selectByExample2Result() {
        Weekend of = Weekend.of(Person.class);
        of.weekendCriteria()
                .andGreaterThan(Person::getAge, 1)
                .andLike(Person::getName, "%ndy%");
        List list = personMapper.selectByExample2Result(of);
        Assert.isTrue(!list.isEmpty(), "list is null");
    }
    
    public void weekendSqls() {
        Example example = Example.builder(Person.class)
                .select(FiledHelper.fieldName(Person::getId),
                        FiledHelper.fieldName(Person::getName),
                        FiledHelper.fieldName(Person::getCountryId))
                .where(WeekendSqls.custom()
                        .andLike(Person::getName, "%d%"))
                .orWhere(WeekendSqls.custom()
                        .andGreaterThan(Person::getCountryId, 1)
                        .andLessThanOrEqualTo(Person::getCountryId, 100))
                .build();
        List list = personMapper.selectByExample(example);
        Assert.isTrue(list.size() > 0, "list is null");
    }   
    
  • 说明
  1. Weekend和WeekendSqls实现通用性的包装,通过反射和Lambda表达式,实现声明式接口;
  2. 不需要写表的字段,而是使用Lambda表达式,简洁友好;
  3. 生成的动态Sql如下;
    SELECT distinct
    
        
            ${selectColumn}
        
        id,name,age,status,role,country_id
     FROM person 
    
        ${@tk.mybatis.mapper.util.OGNL@andNotLogicDelete(_parameter)} 
          
            
              ${@tk.mybatis.mapper.util.OGNL@andOr(criteria)}      
                
                  
                    
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
                    
                    
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value}
                    
                    
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                    
                    
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
                      
                        #{listItem}
                      
                    
                  
                
              
            
          
         
        
    
    order by ${orderByClause}
    FOR UPDATE

通用Mapper实现原理

1. Mybatis架构图

[图片上传失败...(image-11a4ec-1561497072794)]

  • 说明
  1. SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
  2. Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  3. StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  4. ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
  5. ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  6. TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
  7. MappedStatement MappedStatement维护了一条节点的封装,
  8. SqlSource
    负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
    BoundSql 表示动态生成的SQL语句以及相应的参数信息 Configuration
    MyBatis所有的配置信息都维持在Configuration对象之中。

2. Mapper 原理

  1. 通用Mapper提供的接口如下:
@RegisterMapper
public interface SelectMapper {

    /**
     * 根据实体中的属性值进行查询,查询条件使用等号
     *
     * @param record
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    List select(T record);

}
  • 该接口是使用java范型获取到实体类,通过实体类与数据库的映射,获取到相关字段;也就是说SQL是从实体上动态生成,而不再是读取xml;
    如下是通过反射获取到实体类的代码;

public abstract class MapperTemplate{
    /**
     * 获取返回值类型 - 实体类型
     */
    public Class getEntityClass(MappedStatement ms) {
        String msId = ms.getId();
        if (entityClassMap.containsKey(msId)) {
            return entityClassMap.get(msId);
        } else {
            Class mapperClass = getMapperClass(msId);
            Type[] types = mapperClass.getGenericInterfaces();
            for (Type type : types) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType t = (ParameterizedType) type;
                    if (t.getRawType() == this.mapperClass || this.mapperClass.isAssignableFrom((Class) t.getRawType())) {
                        Class returnType = (Class) t.getActualTypeArguments()[0];
                        //获取该类型后,第一次对该类型进行初始化
                        EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());
                        entityClassMap.put(msId, returnType);
                        return returnType;
                    }
                }
            }
        }
        throw new MapperException("无法获取 " + msId + " 方法的泛型信息!");
    }
    
}
  1. Mybatis中每个方法都会包装成MappedStatement实例,这个对象是对jdbc的statement包装;
    这个对象包含id(namespace+id)、结果映射、缓存配置、SqlSource、参数对象等信息;
  1. Mybatis的在扫描mapper注入mapper时,会解析mapper,并根据该注解@SelectProvider,会生成ProviderSqlSource类,而该类会创建StaticSqlSource来执行SQL;
public class MapperRegistry{
   public  void addMapper(Class type) {
       if (type.isInterface()) {
         if (hasMapper(type)) {
           throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
         }
         boolean loadCompleted = false;
         try {
           knownMappers.put(type, new MapperProxyFactory(type));
           MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
           parser.parse();
           loadCompleted = true;
         } finally {
           if (!loadCompleted) {
             knownMappers.remove(type);
           }
         }
       }
     }
}
     
  • 在MapperAnnotationBuilder类中生成ProviderSqlSource;
public class MapperAnnotationBuilder{ 
    void parseStatement(Method method) { 
        Class parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method); SqlSource
        sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass,languageDriver);
     ...... 
    }
    
    private SqlSource getSqlSourceFromAnnotations(Method method, Class parameterType, LanguageDriver languageDriver) {
        try {
          Class sqlAnnotationType = getSqlAnnotationType(method);
          Class sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
          if (sqlAnnotationType != null) {
            if (sqlProviderAnnotationType != null) {
              throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
            }
            Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
            final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
            return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
          } else if (sqlProviderAnnotationType != null) {
            Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
            
            //FIXME 生成ProviderSqlSource类;
            return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
          }
          return null;
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
      }
} 
  1. 通用Mapper就是通过ProviderSqlSource生成MappedStatement替换掉静态的StaticSqlSource,而改成可以支持动态的Sql类;
    通过MappedStatement类获取到接口和方法,并通反射调用该方法生成动态SQL;用反射替把ProviderSqlSource换成动态Sql;
    代码如下:
    public SqlSource createSqlSource(MappedStatement ms, String xmlSql) {
        return languageDriver.createSqlSource(ms.getConfiguration(), "", null);
    }
    
    protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) {
        MetaObject msObject = MetaObjectUtil.forObject(ms);
        msObject.setValue("sqlSource", sqlSource);
    }    
    
  1. 通用Mapper何时替换ProviderSqlSource

    1. 初始化
      SqlSessionFactory时注册mapper后,通过
      configuration.getMappedStatements()获取并循环替换;
  1. Spring的情况下,以继承的方式重写了MapperScannerConfigurer 和
    MapperFactoryBean,在扫描配置Mapper时Spring 调用 checkDaoConfig
    的时候对 SqlSource 进行替换。

    public class MapperScannerConfigurer{
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
            if (this.processPropertyPlaceHolders) {
                processPropertyPlaceHolders();
            }
            ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
            scanner.setAddToConfig(this.addToConfig);
            scanner.setAnnotationClass(this.annotationClass);
            scanner.setMarkerInterface(this.markerInterface);
            scanner.setSqlSessionFactory(this.sqlSessionFactory);
            scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
            scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
            scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
            scanner.setResourceLoader(this.applicationContext);
            scanner.setBeanNameGenerator(this.nameGenerator);
            scanner.registerFilters();
            //设置通用 Mapper
            scanner.setMapperHelper(this.mapperHelper);
            scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
        }
    }
    
    public class ClassPathMapperScanner{
     private MapperFactoryBean mapperFactoryBean = new MapperFactoryBean();
     private void processBeanDefinitions(Set beanDefinitions){
         ......
     }
    }
      
      
    
                                
                            
                        
                        
                        

    你可能感兴趣的:(Mybatis通用Mapper应用)