基于tkmybatis的无xml联表通用mapper

写在最开始,一直是tkmybatis的拥趸,不想写sql经常被人鄙视,但是我恬不知耻...


目录

构造通用sql模板

构造select段sql模板

构造from段sql模板

构造适配example的通用条件sql模板

sql查询结果java对象映射

初始化个说事的数据库

xml的联表查询

xml到ResultMap

java类到resultMap

coding

sql字段模板

联表sql模板

java字段名与sql列名

java类生成resultMap对象

写在最后


为了表达对tkmaybatis的敬意,研习了一下他的源码,总结一下就是做了下边两件事

  1. 对于查询的通用mapper,构造出能够适配他的example的sql模板.
  2. sql查询结果的java对象映射,也就是xml里面对应的resultmap

构造通用sql模板

我们先看一下tkmybatis自己的查询sql模板

tk.mybatis.mapper.provider.ExampleProvider.java    

    public String selectByExample(MappedStatement ms) {
        Class<?> entityClass = this.getEntityClass(ms);
        this.setResultType(ms, entityClass);
        StringBuilder sql = new StringBuilder("SELECT ");
        if (this.isCheckExampleEntityClass()) {
            sql.append(SqlHelper.exampleCheck(entityClass));
        }

        sql.append("<if test=\"distinct\">distinct</if>");
        sql.append(SqlHelper.exampleSelectColumns(entityClass));
        sql.append(SqlHelper.fromTable(entityClass, this.tableName(entityClass)));
        sql.append(SqlHelper.exampleWhereClause());
        sql.append(SqlHelper.exampleOrderBy(entityClass));
        sql.append(SqlHelper.exampleForUpdate());
        return sql.toString();
    }

可以看出主要做了3件事

  1. 构造select段sql模板
  2. 构造from段sql模板
  3. 构造适配example的通用条件sql模板

构造select段sql模板

由于tkmybatis的通用mapper目前只对单表进行操作,制作通用sql查询模板是需要解析单表entity类

tk.mybatis.mapper.mapperhelper.MapperTemplate.java

    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 + " 方法的泛型信息!");
    }

其中EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());是核心代码.他负责解析对应entity类的字段名,对应数据相关属性等.这个里面我们发现他解析的entity类只能是简单pojo,代码如下

tk.mybatis.mapper.mapperhelper.resolveDefaultEntityResolve.java


public EntityTable resolveEntity(Class<?> entityClass, Config config) {
...

for (EntityField field : fields) {
            //如果启用了简单类型,就做简单类型校验,如果不是简单类型,直接跳过
            //3.5.0 如果启用了枚举作为简单类型,就不会自动忽略枚举类型
            //4.0 如果标记了 Column 或 ColumnType 注解,也不忽略
            if (config.isUseSimpleType()
                    && !field.isAnnotationPresent(Column.class)
                    && !field.isAnnotationPresent(ColumnType.class)
                    && !(SimpleTypeUtil.isSimpleType(field.getJavaType())
                    ||
                    (config.isEnumAsSimpleType() && Enum.class.isAssignableFrom(field.getJavaType())))) {
                continue;
            }
            processField(entityTable, field, config, style);
        }
...
}

对于多表联查,entity肯定不能是简单pojo,select字段也需要带上表名前缀,这个地方markA

构造from段sql模板

这部分基于单表的很简单,就是entity类上边table注解.如果没有注解用一个内部定义方式把class名转换成为数据库表名.

对于多表联查,from字段也需要配置多表之间的连接关系,这个地方markB

构造适配example的通用条件sql模板

这部分基本可以复用已有的example解析模板,单表查询entity字段名和数据库字段名在example中进行了对应

对于多表联查,example中的不同表的字段和"表.字段"的sql转换是重要修改部分,这个地方markC

sql查询结果java对象映射

初始化个说事的数据库

DROP DATABASE IF EXISTS `demo`;
CREATE DATABASE IF NOT EXISTS `demo` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `demo`;

DROP TABLE IF EXISTS `test_class`;
CREATE TABLE IF NOT EXISTS `test_class` (
  `id` varchar(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `school_id` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DELETE FROM `test_class`;
INSERT INTO `test_class` (`id`, `name`, `school_id`) VALUES
	('1', '大一班', '1'),
	('2', '大二班', '1'),
	('3', '大三班', '1');

DROP TABLE IF EXISTS `test_school`;
CREATE TABLE IF NOT EXISTS `test_school` (
  `id` varchar(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DELETE FROM `test_school`;
INSERT INTO `test_school` (`id`, `name`) VALUES
	('1', '幼儿园');

DROP TABLE IF EXISTS `test_teacher`;
CREATE TABLE IF NOT EXISTS `test_teacher` (
  `id` varchar(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `class_id` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DELETE FROM `test_teacher`;
INSERT INTO `test_teacher` (`id`, `name`, `class_id`) VALUES
	('1', '王老师', '1'),
	('2', '李老师', '1'),
	('3', '王二老师', '2'),
	('4', '李二老师', '2'),
	('5', '王三老师', '3'),
	('6', '李三老师', '3');

数据库关系如下

每个entity生成之后是这样的

@ApiModel(value = "test_school")
@Data
@Table(name = "test_school")
public class TestSchool {
    @Id
    @Column(name = "id")
    @ApiModelProperty(value = "")
    private String id;

    @Column(name = "`name`")
    @ApiModelProperty(value = "")
    private String name;
}
@ApiModel(value = "test_class")
@Data
@Table(name = "test_class")
public class TestClass {
    @Id
    @Column(name = "id")
    @ApiModelProperty(value = "")
    private String id;

    @Column(name = "`name`")
    @ApiModelProperty(value = "")
    private String name;

    @Column(name = "school_id")
    @ApiModelProperty(value = "")
    private String schoolId;
}
@ApiModel(value = "test_teacher")
@Data
@Table(name = "test_teacher")
public class TestTeacher {
    @Id
    @Column(name = "id")
    @ApiModelProperty(value = "")
    private String id;

    @Column(name = "`name`")
    @ApiModelProperty(value = "")
    private String name;

    @Column(name = "class_id")
    @ApiModelProperty(value = "")
    private String classId;
}

xml的联表查询

联表需要的select段sql

  <sql id="School_Join_Column_List">
    <!--@mbg.generated-->
    test_school.id as test_school_id,
    test_school.name as test_school_name
  </sql>
  <sql id="Class_Join_Column_List">
    <!--@mbg.generated-->
    test_class.id as test_class_id,
    test_class.name as test_class_name,
    test_class.school_id as test_class_school_id
  </sql>
  <sql id="Teacher_Join_Column_List">
    <!--@mbg.generated-->
    test_teacher.id as test_teacher_id,
    test_teacher.name as test_teacher_name,
    test_teacher.class_id as test_teacher_class_id
  </sql>

 联表返回的resultMap

  <resultMap id="School_JoinResultMap" type="com.sharp.join.tk.mybatis.dao.entity.TestSchool">
    <!--@mbg.generated-->
    <id column="test_school_id" property="id"/>
    <result column="test_school_name" property="name"/>
  </resultMap>
  <resultMap id="Class_JoinResultMap" type="com.sharp.join.tk.mybatis.dao.entity.TestClass">
    <!--@mbg.generated-->
    <id column="test_class_id" property="id"/>
    <result column="test_class_name" property="name"/>
    <result column="test_class_school_id" property="schoolId"/>
  </resultMap>
  <resultMap id="Teacher_JoinResultMap" type="com.sharp.join.tk.mybatis.dao.entity.TestTeacher">
    <!--@mbg.generated-->
    <id column="test_teacher_id" property="id"/>
    <result column="test_teacher_name" property="name"/>
    <result column="test_teacher_class_id" property="classId"/>
  </resultMap>

  <resultMap id="ClassTeacherJoin" type="com.sharp.join.tk.mybatis.dao.entity.ClassTeacherJoin"
             extends="Class_JoinResultMap">
    <collection property="teacherList" resultMap="Teacher_JoinResultMap">
    </collection>
  </resultMap>
  <resultMap id="SchoolTeacherJoin" type="com.sharp.join.tk.mybatis.dao.entity.SchoolTeacherJoin"
             extends="School_JoinResultMap">
    <collection property="classTeacherJoinList" resultMap="ClassTeacherJoin"/>
  </resultMap>

可以看出来我们想返回的是一个这样的java

@Data
@EqualsAndHashCode(callSuper = true)
public class SchoolTeacherJoin extends TestSchool {
    private List<ClassTeacherJoin> classTeacherJoinList;
}

@Data
@EqualsAndHashCode(callSuper = true)
public class ClassTeacherJoin extends TestClass {
    private List<TestTeacher> teacherList;
}

最终我们可以写出一个dao接口

public interface SchoolTeacherJoinDao {
    List<SchoolTeacherJoin> select(@Param("schoolId") String schoolId,
                                   @Param("classId") String classId,
                                   @Param("teacherId") String teacherId);
}


  <select id="select" resultMap="SchoolTeacherJoin">
    select
    <include refid="School_Join_Column_List"/>,
    <include refid="Class_Join_Column_List"/>,
    <include refid="Teacher_Join_Column_List"/>
    from test_school
    join test_class on test_school.id = test_class.school_id
    join test_teacher on test_class.id = test_teacher.class_id
    where
    1=1
    <if test="schoolId != null and schoolId != ''">
      AND test_school.id = #{schoolId,jdbcType=VARCHAR}
    </if>
    <if test="classId != null and classId != ''">
      AND test_class.id = #{classId,jdbcType=VARCHAR}
    </if>
    <if test="teacherId != null and teacherId != ''">
      AND test_teacher.id = #{teacherId,jdbcType=VARCHAR}
    </if>
  </select>

相信大多数童鞋们联表查询就是这么一个过程,写了这么多,定死了3个参数,要扩展还得再写xml,手都写酸了有没有.

xml到ResultMap

xml里面写了那么多就是为了构造一个复杂的java类型让sql返回的数据能够转换成java.只能探究一下原理了

org.apache.ibatis.builder.xml.XMLMapperBuilder.java

private void configurationElement(XNode context) {
...
resultMapElements(context.evalNodes("/mapper/resultMap"));
...
}

一路debug下去会发现xml树形结构都会到这里

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
...
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
...
}

每个resultMap都会在这里被变成resultMap存在内存里.每一个java字段都对应着resultMappings里面的一个item

下图就是School_JoinResultMap转成的resultMap对象(org.apache.ibatis.mapping.ResultMap.java)

 我们再来看看复杂resultMap中associate, collection是怎么处理的

 可以看出来每个字段仍然是一个resultMapping, 只不过association,collection如果是一个resultMap的话就会将对应的reslutMapId记录在nestedResultMapId这个字段中

总结一下resultMap这个对象对应着一个java类,字段List对应resultMappings,如果是复杂字段类型就是对应另外一个resultMap,存在resultMapping里面的nestedResultMapId字段中.

xml最终也是在初始化的时候被解析转换成java对象存在内存中,所以我们的目标就是将想转换的据java类转换成resultMap对象.

java类到resultMap

目标明确了就好办了,xml是个树形结构,java类也是个树形结构,xml有association,collection我们给java字段做一个注解,模仿着XMLMapperBuilder我们可以将一个java类转换成一个resultMap对象.标注JoinAssociation,JoinCollection的字段先做一个resultMap将id存在对应字段resultMapping中的nestedResultMapId字段.

解析Java类型转换成resultMap对象,这个地方MarkD

coding

分析了一大堆,给自己挖了4个坑

  1. 对于多表联查,entity肯定不能是简单pojo,select字段也需要带上表名前缀,这个地方markA
  2. 对于多表联查,from字段也需要配置多表之间的连接关系,这个地方markB
  3. 对于多表联查,example中的不同表的字段和"表.字段"的sql转换是重要修改部分,这个地方markC
  4. 解析Java类型转换成resultMap对象,这个地方MarkD

sql字段模板

        要处理复杂java类型的字段,就需要递归每个字段,直到他们都是简单类型的字段,这里我们自定义了2个新的注解,标注该复杂字段是我们需要递归处理的.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface JoinAssociation {
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface JoinCollection {
    String value() default "";
}

分别对应resultMap xml中的association标签和collection标签.递归解析entity类核心代码如下:

com.sharp.join.tk.mybatis.framework.join.JoinEntityResolve.java

    public EntityTable resolveEntity(EntityTable entityTable, String fieldName, Class<?> entityClass, Config config) {
...
        //处理所有列
        List<EntityField> fields;
        if (config.isEnableMethodAnnotation()) {
            // modify by Sharp for Join Mapper
            fields = JoinFieldHelper.getAll(fieldName, entityClass);
        } else {
            // modify by Sharp for Join Mapper
            fields = JoinFieldHelper.getFields(fieldName, entityClass);
        }
        for (EntityField field : fields) {
            if (field.isAnnotationPresent(JoinAssociation.class)
                    || field.isAnnotationPresent(JoinCollection.class)) {
                // modify by Sharp for join mapper
                resolveEntity(entityTable, field.getName(), field.getJavaType(), config);
            }
            //如果启用了简单类型,就做简单类型校验,如果不是简单类型,直接跳过
            //3.5.0 如果启用了枚举作为简单类型,就不会自动忽略枚举类型
            //4.0 如果标记了 Column 或 ColumnType 注解,也不忽略
            if (config.isUseSimpleType()
                    && !field.isAnnotationPresent(Column.class)
                    && !field.isAnnotationPresent(ColumnType.class)
                    && !(SimpleTypeUtil.isSimpleType(field.getJavaType())
                    ||
                    (config.isEnumAsSimpleType() && Enum.class.isAssignableFrom(field.getJavaType())))) {
                continue;
            }
            processField(entityTable, field, entityClass, config, style);
        }
...
    }

联表sql模板

既然是联表查询,除了标明entity,还需要描述清涉及的表,以及表和表之间的关系

@Data
@EqualsAndHashCode(callSuper = true)
public class SchoolTeacherJoin extends TestSchool implements IJoinRelationship {
    @JoinCollection
    private List<ClassTeacherJoin> classTeacherJoinList;

    @Override
    public List<JoinRelationship> joinRelationship() {
        return buildJoinRelationship(TestClass.class, "schoolId", TestSchool.class, "id", JoinRelationship.Type.LEFT_JOIN)
                .add(TestClass.class, "id", TestTeacher.class, "classId", JoinRelationship.Type.LEFT_JOIN)
                .build();
    }
}

joinRelationship作为一个联表entity必须实现的接口,表述了单表对应entity中与另一单表entity字段之间的关系.

这里表和表之间的关系表示方法还值得改进,现在的写法左联还是右联在一定的条件下会出现错误.我是写这个逻辑的时候是希望能够尽量少约束去写关系,所以处理这个关系转化成联表关系的时候会将表A和表B的顺序颠倒,这样左联和右联就产生的错乱.这个地方欢迎童鞋们给点建议.

com.sharp.join.tk.mybatis.framework.join.JoinSqlHelper.java

    public static String fromJoinTable(Class<?> entityClass, String defaultTableName) {
        Set<String> alreadyJoinTable = new HashSet<>();
        Object entity = ReflectionUtil.newInstance(entityClass);
        List<JoinRelationship> joinRelationshipList = (List<JoinRelationship>) ReflectionUtil.invoke(entity, "joinRelationship");
        StringBuilder sql = new StringBuilder();
        sql.append(" FROM ");
        sql.append(defaultTableName);
        alreadyJoinTable.add(defaultTableName);
        int waitForParse;
        do {
            waitForParse = joinRelationshipList.size();
            List<JoinRelationship> delayParseRelationshipList = new ArrayList<>();
            Iterator<JoinRelationship> iterator = joinRelationshipList.iterator();
            while (iterator.hasNext()) {
                JoinRelationship joinRelationship = iterator.next();
                DbField field1;
                DbField field2;
                if (alreadyJoinTable.contains(joinRelationship.getField1().getTable())
                        && !alreadyJoinTable.contains(joinRelationship.getField2().getTable())) {
                    field1 = joinRelationship.getField2();
                    field2 = joinRelationship.getField1();
                } else if (alreadyJoinTable.contains(joinRelationship.getField2().getTable())
                        && !alreadyJoinTable.contains(joinRelationship.getField1().getTable())) {
                    field1 = joinRelationship.getField1();
                    field2 = joinRelationship.getField2();
                } else {
                    if (!alreadyJoinTable.contains(joinRelationship.getField1().getTable())
                            && !alreadyJoinTable.contains(joinRelationship.getField2().getTable())) {
                        delayParseRelationshipList.add(joinRelationship);
                    }
                    iterator.remove();
                    continue;
                }
                sql.append(joinRelationship.getType().getValue());
                sql.append(field1.getTable());
                sql.append(" ON ");
                sql.append(field1.getTable());
                sql.append(".");
                sql.append(field1.getField());
                sql.append(" = ");
                sql.append(field2.getTable());
                sql.append(".");
                sql.append(field2.getField());
                alreadyJoinTable.add(field1.getTable());
                iterator.remove();
            }
            joinRelationshipList.addAll(delayParseRelationshipList);
        } while (waitForParse != joinRelationshipList.size());
        return sql.toString();
    }

java字段名与sql列名

对于单表来说,sql列名就是字段上的column标注,字段名就是字段名本身.但是到了多表联查,每个表中都可能出现相同的sql字段,也有可能出现相同的java字段名.为了每个字段唯一,我参考了xml中***_Join_Column_List的写法,每个sql列名都重命名成为table_name_column_name的形式,对应的java字段名也采用"对象.属性"的格式.最终example的写法如下:

    public List<SchoolTeacherJoin> getSchoolTeacherJoinList(String schoolId, String classId, String teacherId) {
        Example example = new Example(SchoolTeacherJoin.class);
        Example.Criteria criteria = example.createCriteria();
        if (StringUtils.isNotEmpty(schoolId)) {
            criteria.andEqualTo("id", schoolId);
        }
        if (StringUtils.isNotEmpty(classId)) {
            criteria.andEqualTo("classTeacherJoinList.id", classId);
        }
        if (StringUtils.isNotEmpty(teacherId)) {
            criteria.andEqualTo("classTeacherJoinList.teacherList.id", teacherId);
        }
        return schoolTeacherJoinDao.joinSelectByExample(example);
    }

java类生成resultMap对象

这部分就是仿照xml树形结构的解析,递归遍历java字段树形结构,最终生成resultMap对象,核心代码如下:

    public ResultMap getResultMap(Configuration configuration, EntityTable entityTable) {
        String resultMapId = entityTable.getEntityClass().getTypeName() + DEFAULT_RESULT_MAP;
        ResultMap res = this.resultMap.get(entityTable.getEntityClass());
        if (res != null) {
            return res;
        } else {
            if (MapUtils.isNotEmpty(resultMappingMap)) {
                List<Field> fields = ReflectionUtil.getFields(entityTable.getEntityClass());
                List<ResultMapping> resultMappings = buildResultMappingList(configuration, resultMapId, fields);
                ResultMap.Builder builder = new ResultMap.Builder(configuration, resultMapId, entityTable.getEntityClass(), resultMappings, null);
                saveResultMap(configuration, entityTable.getEntityClass(), builder.build());
                return this.resultMap.get(entityTable.getEntityClass());
            }
        }
        return null;
    }

    private void saveResultMap(Configuration configuration, Class<?> entityClazz, ResultMap resultMap) {
        this.resultMap.put(entityClazz, resultMap);
        configuration.addResultMap(resultMap);
    }

    private List<ResultMapping> buildResultMappingList(Configuration configuration, String resultMapId, List<Field> fields) {
        List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
        for (Field field : fields) {
            ResultMapping resultMapping;
            if (field.isAnnotationPresent(JoinAssociation.class)) {
                ResultMap resultMap = getResultMap(configuration, field, field.getType(), resultMapId);
                resultMapping = buildResultMapping(configuration, resultMap, field.getName(), field.getType());
            } else if (field.isAnnotationPresent(JoinCollection.class)
                    && field.getGenericType() instanceof ParameterizedType
                    && ((ParameterizedType) field.getGenericType()).getActualTypeArguments().length == 1) {
                try {
                    Class<?> fieldActualClazz = Class.forName(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0].getTypeName());
                    ResultMap resultMap = getResultMap(configuration, field, fieldActualClazz, resultMapId);
                    resultMapping = buildResultMapping(configuration, resultMap, field.getName(), field.getType());
                } catch (ClassNotFoundException e) {
                    throw new MapperException(e);
                }
            } else {
                resultMapping = resultMappingMap.get(field);
            }
            if (resultMapping != null) {
                resultMappings.add(resultMapping);
            }
        }
        return resultMappings;
    }

    private ResultMap getResultMap(Configuration configuration, Field parentField, Class<?> fieldActualClazz, String parentResultMapId) {
        ResultMap res = this.resultMap.get(fieldActualClazz);
        if (res != null) {
            return res;
        } else {
            String resultMapId = buildResultMapId(parentField, parentResultMapId);
            List<Field> fields = ReflectionUtil.getFields(fieldActualClazz);
            List<ResultMapping> resultMappings = buildResultMappingList(configuration, resultMapId, fields);
            ResultMap.Builder builder = new ResultMap.Builder(configuration, resultMapId, fieldActualClazz, resultMappings, null);
            saveResultMap(configuration, fieldActualClazz, builder.build());
            return this.resultMap.get(fieldActualClazz);
        }
    }

写在最后

贴了那么多代码,看的晕头转向,我为各位童鞋写好了测试工程.join-tk.mybatis: 基于tkmybatis的联表无xml配置欢迎各位童鞋拍砖

希望大家有兴趣的话运行一下demo,帮小弟测试测试,也欢迎提出您的宝贵意见(我就是不改).

谢谢各位,拜拜.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值