前言
这个章节里,我们介绍级联插件的一个重要部分 ->【查询组件和自定义注解】。在mybatis的设定里,如果Mapper要执行一个sql,要么使用xml与Mapper进行映射;要么使用@Select等注解提供sql语句。但是在级联插件里,我们需要执行的查询语句,无论是待查询的表,还是查询条件,都是动态生成的,无法提前指定。
所以就有了这个查询组件,组件的核心功能是动态生成select类型的sql,执行完成后,返回指定ORM类型的数据。
组件介绍
查询组件由两部分构成
- SqlMapper + @SelectProvider,执行查询;
- ORM结果映射,将查询出来的结果,映射到与数据库表绑定的POJO类对象;
动态查询
在这个系列的第21章中,我们介绍了这个注解的使用。其作用机制就是在运行时,根据选定的参数,在Provider类指定方法里,对sql进行拼接。这里就用到了这个特性,表和配套的查询参数,全部都是运行期间动态拼接。
最开始的时候,其实有考虑过使用JDBC,考虑到查询出来的结果,需要与ORM类进行映射,使用mybatis现成的机制,可以省去很多工作量。
SqlMapper源码
PureSqlProvider是一个内部类,提供的功能也非常简单,只是将sql原封不动的返回给SqlMapper.select方法,略过不提。
public interface SqlMapper {
/**
* 执行原始sql语句,返回hash列表
*
* @param sql 原始sql
* @return list
*/
@SelectProvider(type = PureSqlProvider.class, method = "sql")
List<Map<String, Object>> select(String sql);
/**
* 原始sql提供类
*/
class PureSqlProvider {
public String sql(String sql) {
return sql;
}
}
}
查询结果映射
在ORM框架体系中,预期的查询结果,一般是具体的POJO对象(列表),避免再次进行数据封装。而上文中提到的SqlMapper.select方法,只是返回了一个hash列表,这远远不能达到生产的要求。在这里我们使用mybatis里的MetaClass工具类,完成POJO类与数据库表里字段的映射,使用jdk里的反射机制,返回具体的POJO实例。
具体步骤:
- 执行sql,返回hashMap列表;
- 得到具体pojo类的MetaClass工具类实例, MetaClass.forClass(cls, new DefaultReflectorFactory());
- 遍历hashMap表,逐一处理map里的每个key;
- 根据数据库表字段,找到class里对应的field;
- 根据field,返回类实例具体字段的set方法;
- 对类实例的指定field赋值;
- 完成
/**
* 静态查询方法
*
* @param sql 查询语句
* @param cls 需要封装的类
* @param <T> 指定的POJO类
* @return list
*/
public static <T> List<T> selectList(String sql, Class<T> cls) {
SqlMapper mapper = SpringUtil.getBean(SqlMapper.class);
List<T> result = new ArrayList<>();
try {
// 查询数据
List<Map<String, Object>> list = mapper.select(sql);
// MetaClass工具类实例
MetaClass metaClass = MetaClass.forClass(cls, new DefaultReflectorFactory());
// 将hash表转换为表对象
for (Map<String, Object> map : list) {
// 实例化一个POJO对象
T temp = cls.newInstance();
// 遍历hash
for (Map.Entry<String, Object> entry : map.entrySet()) {
// 根据数据库表字段,找到class里对应的field
String property = metaClass.findProperty(entry.getKey(), true);
if (property == null) {
throw BizException.of(BusinessStatus.MYBATIS_CASCADE_PROP_NULL,
"Class:" + cls.getSimpleName() + ",field:" + entry.getKey());
}
// 根据field,返回set方法,比如setName(str)
Method method = getSetMethod(cls, property);
// 对类实例的指定field赋值
method.invoke(temp, entry.getValue());
}
result.add(temp);
}
return result;
} catch (Exception e) {
throw BizException.of(e);
}
}
插件注解
这一章主要介绍插件里使用的几个自定义注解
- @ToMany,一对多场景
- @ToMiddleTable,多对多场景
- @ToOne,一对一 场景
@ToMany
一般用在List属性上面,如26章里提到的,获取班级里所有学生:@ToMany private List students;
注解要素说明
- self:在对方表中,我方的id;比如在学生表里的 school_class_id;
- depth:其他连个注解也出现这个属性,在级联查询到子表里的时候,根据此配置判断是否要对处理子表里的注解,如果depth=true,则查询子表里的孙子表,一层层往下处理,直到没有级联注解,或者级联查询结果为空。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToMany {
/**
* 在对方表中,我方的id
*/
String self();
/**
* 是否对当前字段执行深度级联查询,
* 场景:当前对象是级联查询的结果,再次对当前对象的属性进行级联查询
*/
boolean depth() default false;
}
@ToOne
如26章里提到的,获取班级里的班主任:
// 班主任对象
@ToOne(target="class_charge_id",depth=false)
private Teather classCharge;
注解要素说明
- target:在我方表中,对方的id;比如在班级表里的 class_charge_id字段;
- depth:与ToMany注解用法相同;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToOne {
/**
* 在我方表中,对方的id
*/
String target();
/**
* 是否对当前字段执行深度级联查询,
* 场景:当前对象是级联查询的结果,再次对当前对象的属性进行级联查询
*/
boolean depth() default false;
}
@ToMiddleTable
此注解面向中间表,类似JPA里的@ManyToMany,不过我们这个插件没有动态生成中间表,所以需要开发者提前在数据库中创建中间表。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToMiddleTable {
/**
* 中间表
*/
String table();
/**
* 关联表中,我方id
*/
String self();
/**
* 关联表中,对方id
*/
String target();
/**
* 是否对当前字段执行深度级联查询,
* 场景:当前对象是级联查询的结果,再次对当前对象的属性进行级联查询
*/
boolean depth() default false;
}
如何使用
toOne和toMany比较好理解,就不单独说明了
先看一个标准的中间表
代码里,直接针对中间表的表名和字段进行设定
@ToMiddleTable(table = "tb_role_permission", self = "role_id", target = "permission_id",depth=false)
private List<TbPermission> permissions;
示例
在第二十六章里,我们举例了班级、学生和老师的三个POJO类,里面使用了 @ToOne和@ToMany,但是注解参数没有写明,这里做一个补全。
class SchoolClass{
private int id;
// 名称
private String name;
// 班主任
private int classChargeId;
// 班级学生列表
@ToMany(self="school_class_id",depth=false)
private List<Student> students;
// 班主任对象
@ToOne(target="class_charge_id",depth=false)
private Teather classCharge;
}