写在最开始,一直是tkmybatis的拥趸,不想写sql经常被人鄙视,但是我恬不知耻...
目录
为了表达对tkmaybatis的敬意,研习了一下他的源码,总结一下就是做了下边两件事
- 对于查询的通用mapper,构造出能够适配他的example的sql模板.
- 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件事
- 构造select段sql模板
- 构造from段sql模板
- 构造适配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个坑
- 对于多表联查,entity肯定不能是简单pojo,select字段也需要带上表名前缀,这个地方markA
- 对于多表联查,from字段也需要配置多表之间的连接关系,这个地方markB
- 对于多表联查,example中的不同表的字段和"表.字段"的sql转换是重要修改部分,这个地方markC
- 解析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,帮小弟测试测试,也欢迎提出您的宝贵意见(我就是不改).
谢谢各位,拜拜.