水煮MyBatis(二七)- 级联插件【查询组件和自定义注解】

127 阅读5分钟

前言

   这个章节里,我们介绍级联插件的一个重要部分 ->【查询组件和自定义注解】。在mybatis的设定里,如果Mapper要执行一个sql,要么使用xml与Mapper进行映射;要么使用@Select等注解提供sql语句。但是在级联插件里,我们需要执行的查询语句,无论是待查询的表,还是查询条件,都是动态生成的,无法提前指定。
   所以就有了这个查询组件,组件的核心功能是动态生成select类型的sql,执行完成后,返回指定ORM类型的数据。

组件介绍

查询组件由两部分构成

  1. SqlMapper + @SelectProvider,执行查询;
  2. 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比较好理解,就不单独说明了
先看一个标准的中间表
image.png

代码里,直接针对中间表的表名和字段进行设定

    @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;

}