mondrian 源码解读(番外篇)-MDX函数的执行过程详解

MDX函数的执行过程详解

笔者注:由于将来公司可能会有对函数的二次开发需求,所以为公司写了函数的执行过程,以供将来使用。

一,Query对象的创建

在Query对象中,最主要的是resolve()方法,这里涉及到了2个非常重要的功能。

  • 第一,轴(查询轴和切片轴)上表达式(Exp)的转换。

    主要是将表达式从UnresolvedFunCall转换为ResolvedFunCall。

  • 第二,轴上的计算器(Calc)的创建。

    主要是针对各个轴如何创建计算器。

下面是的resolve()方法解释:

 public void resolve() {
    final Validator validator = createValidator(); //1
    resolve(validator); // resolve self and children ,2
    // Create a dummy result so we can use its evaluator
    final Evaluator evaluator = Util.createEvaluator(this); //3
    ExpCompiler compiler =
            createCompiler(
                    evaluator, validator, Collections.singletonList(resultStyle)); //4
    compile(compiler);  //5
}
  1. 第1行创建校验器(Validator)。
  2. 第2行根据Validator转换所有的UnresolvedFunCall到ResolvedFunCall。
  3. 第3行针对当前Query创建求值器(Evaluator)。
  4. 第4行根据求值器,校验器创建表达式编译器(ExpCompiler)。
  5. 第5行根据表达式编译器编译所有的轴,并创建轴上的计算器。

首先我们看Validator接口:

public interface Validator {
    Exp  validate(Exp exp, boolean scalar);
    void validate(QueryAxis axis);
    void validate(Formula formula);
    ...
}

很显然,这是一个访问者模式,针对不同的对象进行不同的操作。看完Validator接口后,我们看resolve()方法的时序图,如下:

时序图

其他功能例如表达式的查找,度量成员的查找,公式(formulas)的成员(member)或者集(set)的创建创建等也是在这个过程中执行的,这里就不一一介绍了。下面将对主要的2点做详细的介绍。


1.1轴上表达式(Exp)的转换

我们看上图,当执行Query的validate(axis)方法时:

  1. QueryAxis会调用Validator的void validate(QueryAxis axis)方法
  2. Validator反过来调用QueryAxis的public void resolve(Validator validator)方法
  3. QueryAxis又会调用Validator的public Exp validate(Exp exp, boolean scalar)方法
  4. 接着Validator又会调用UnresolvedFunCall的public Exp accept(Validator validator)方法。
  5. 最后UnresolvedFunCall的accept(Validator validator)会创建一个ResolvedFunCall对象返回到QueryAxis的exp上。

现在我们看UnresolvedFunCall的accept(Validator validator)方法,看它是如何执行的。

//UnresolvedFunCall
public Exp accept(Validator validator) {
    Exp[] newArgs = new Exp[args.length]; //1
    FunDef funDef =
            FunUtil.resolveFunArgs(
                    validator, null, args, newArgs, name, syntax);  //2
    return funDef.createCall(validator, newArgs);  //3
}

它的执行有一下几步:

  1. 创建表达式参数
  2. 解析表达式为指定的函数定义(FunDef)
  3. 创建已解析的函数调用(ResolvedFunCall)

这里有2个点需要关注,第2和第3。我们看看它是如何执行的。


1.1.1解析表达式为指定的函数定义(FunDef)

下面是FunUtil的resolveFunArgs方法。

//FunUtil
public static FunDef resolveFunArgs(
        Validator validator,
        FunDef funDef,
        Exp[] args,
        Exp[] newArgs,
        String name,
        Syntax syntax)
{
    for (int i = 0; i < args.length; i++) {
        newArgs[i] = validator.validate(args[i], false);  //2
    }
    if (funDef == null || validator.alwaysResolveFunDef()) {
        funDef = validator.getDef(newArgs, name, syntax);  //5
    }
    return funDef;
}

我们看第2行newArgs[i] = validator.validate(args[i], false),这一行对参数进行了解析,最后返回解析后的表达式赋值给新参数。注意这里的参数可以是维度,成员,ID,函数或者其他的表达式。
第5行 funDef = validator.getDef(newArgs, name, syntax),即通过新参数,表达式名称,和语义(Syntax)来构建一个FunDef,最后返回这个FunDef。

下面,我们先要了解什么是函数定义(FunDef)。看一下它的接口:

public interface FunDef {

    Syntax getSyntax(); //函数语义,例如函数,前缀,中缀,后缀等。

    String getName();  //函数名称

    int getReturnCategory();  //返回类型

    int[] getParameterCategories();  //参数类型

    Calc compileCall(ResolvedFunCall call, ExpCompiler compiler);    //创建求值器

    Exp createCall(Validator validator, Exp[] args);    //创建表达式,一般来说是ResolvedFunCall

}

很明显,FunDef描述了函数的特征:语义,名称,返回类型,参数类型。还有2个接口,compileCall定义如何创建求值器,createCall定义了如何创建表达式。

了解清楚了FunDef后,我们再看如何获取FunDef,即第5行的validator.getDef(newArgs, name, syntax)。我们把主干抽出来,代码如下:

//Validator实现
public FunDef getDef(
    Exp[] args,
    String funName,
    Syntax syntax)
{
    //...

    List<Resolver> resolvers = funTable.getResolvers(funName, syntax);  //1

    final List<Resolver.Conversion> conversionList =
        new ArrayList<Resolver.Conversion>();
    List<FunDef> matchDefs = new ArrayList<FunDef>();
    for (Resolver resolver : resolvers) {
        FunDef def = resolver.resolve(args, this, conversionList); //2
        if (def != null) {
            //....
            matchDefs.add(def);
            //...
        }
    }

    switch (matchDefs.size()) {
    case 0:
        throw new RuntimeException("No Function Matches Signature>>>"+
            signature);
    case 1:
        break;
    default:
        throw new RuntimeException("MoreThanOneFunctionMatchesSignature"+
                signature+
                buf.toString());
    }

    // ...

    return matchDef;
}

这里首先做的是funTable通过名称(funName)和语义(syntax)来获取解析器(Resolver)列表,这应该是这里最重要的一部分了。然后再根据获取到的解析器列表来解析参数(args)获取到具体的FunDef。这刚好与FunDef的描述相符合,即语义,名称,参数类型决定了某个具体的函数。注意,要理解的是FunDef还有个返回类型,为什么这里不需要呢这?就像java的方法一样,方法可以同名,不同的参数,单返回类型必须只有一种,不可能定义出重载返回类型的方法出来。

所以了解了这些以后,我们继续看第1处的 funTable.getResolvers(funName, syntax);和第2处的resolver.resolve(args, this, conversionList)就很简单了。这里不就详细解释了,详情请看如何定义一个函数部分。

1.1.2创建已解析的函数调用(ResolvedFunCall)

创建完FunDef后,我们再回到UnresolvedFunCall的accept(Validator validator)方法。
我们看ResolvedFunCall是如何被创建的。由于大部分FunDef继承的FunDefBase,我们只需要看FunDefBase中的createCall(Validator validator, Exp[] args)方法就可以了。

//FunDefBase
public Exp createCall(Validator validator, Exp[] args) {
    int[] categories = getParameterCategories();  //1
    final Type type = getResultType(validator, args);  //2
    if (type == null) {
        throw new RuntimeException("could not derive type");
    }
    return new ResolvedFunCall(this, args, type);  //3
}
  1. 第一步获取参数类型
  2. 第二步获取返回类型,这里要注意的是,对于(cube,dimension, hierarchy, level, member)的返回类型,我们需要指定到某个具体的返回类型中。假设返回一个level类型,我们要知道这个level是属于哪个hierarchy,哪个dimension。所以FunBase默认情况下是取的第一个参数的返回类型进行填充。对于某些函数来说,并不是第一个参数决定返回类型,所以需要自己实现。
  3. 根据this,args,type创建ResolvedFunCall。

最后将解析的ResolvedFunCall返回给QueryAxis,这样,轴(查询轴和切片轴)上表达式(Exp)的转换就完成了。

1.2轴上的计算器(Calc)的创建

跳到最上面,接下来我们看如何创建轴上的计算器(Calc)。
第3,4,5步是我们比较关心的。第3,4步比较简单,这里就不展开了。
我们看第5步,在compile(compiler)方法内部,调用了轴上的compile(ExpCompiler compiler, ResultStyle resultStyle)方法。我们看方法的具体实现:

public Calc compile(ExpCompiler compiler, ResultStyle resultStyle) {
    Exp exp = this.exp;
    if (axisOrdinal.isFilter()) {              //1
        exp = normalizeSlicerExpression(exp);   //2
        exp = exp.accept(compiler.getValidator());  //3
    }
    switch (resultStyle) {
        case LIST:
            return compiler.compileList(exp, false);
        case MUTABLE_LIST:
            return compiler.compileList(exp, true);
        case ITERABLE:
            return compiler.compileIter(exp);  //4
        default:
            throw Util.unexpected(resultStyle);
    }
}
  1. 第1行判断表达式是否是切片轴,如果是,进入2,3步。
  2. 标准化表达式,即将表达式封装成集表达式({}表达式)。
  3. 对表达式执行前面1.1的解析过程,将UnresolvedFunCall转换成ResolvedFunCall。
  4. 最后使用compiler编译迭代,返回计算器。

接下来我们看它是如何编译的。我们看compiler.compileIter(exp)这个方法。

public IterCalc compileIter(Exp exp) {
    IterCalc calc =
        (IterCalc) compileAs(exp, null, ResultStyle.ITERABLE_ONLY);
    if (calc == null) {
        calc = (IterCalc) compileAs(exp, null, ResultStyle.ANY_ONLY);
        assert calc != null;
    }
    return calc;
}

这个方法比较简答,先用ResultStyle.ITERABLE_ONLY编译一次,如果为空,继续用ResultStyle.ANY_ONLY编译一次。

我们继续看compileAs方法:

public Calc compileAs(
    Exp exp,
    Type resultType,
    List<ResultStyle> preferredResultTypes)
{
    int substitutions = 0;
    List<ResultStyle> save = this.resultStyles;
    try {
        this.resultStyles = preferredResultTypes;
        if (resultType != null && resultType != exp.getType()) {  //1
            if (resultType instanceof MemberType) {
                return compileMember(exp);                        //2
            } else if (resultType instanceof LevelType) {
                return compileLevel(exp);
            } else if (resultType instanceof HierarchyType) {
                return compileHierarchy(exp);
            } else if (resultType instanceof DimensionType) {
                return compileDimension(exp);
            } else if (resultType instanceof ScalarType) {
                return compileScalar(exp, false);
            }
        }                                                       //3
        final Calc calc = compile(exp);                         //4
        if (substitutions > 0) {
            final IterCalc iterCalc = (IterCalc) calc;
            if (iterCalc == null) {
                this.resultStyles =
                    Collections.singletonList(ResultStyle.ITERABLE);
                return compile(exp);
            } else {
                return iterCalc;
            }
        }
        return calc;
    } finally {
        this.resultStyles = save;
    }
}

根据结果集类型,使用不同的编译方式。我们看代码块1-3之间的,它的主要作用是因为表达式的类型跟返回的结果集类型不同,所以要特殊对待。例如,我们看第2行,如果表达式的类型的MemberType,但是返回类型是HierarchyType,这时我们就需要将其转换为类型了。

对于返回结果集合表达式类似相同的情况,在第4行中,compile(exp)调用了ResolvedFunCall的exp.accept(this),最后调用funDef.compileCall(this, compiler)方法,创建了一个计算器(Calc)。注意这里的compileCall是需要在自己在函数中实现的。

二,函数的执行过程。

函数的执行是在RolapResult结果集中进行的。具体的方法是executeAxis( Evaluator evaluator, QueryAxis queryAxis, Calc axisCalc, boolean construct, AxisMemberList axisMembers)
方法。我们可以看到方法中有一段代码

final TupleIterable iterable =
          ((IterCalc) axisCalc).evaluateIterable(evaluator);

为什么要是一个IterCalc呢,留给大家思考。这里通过执行IterCalc的evaluateIterable()方法,来完成整个轴的执行过程。具体的过程是根据轴的不同调用不同的计算器,在计算器里层继续调用参数的计算器(参考各个FunDef的实现),直到返回结果为止。