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行创建校验器(Validator)。
- 第2行根据Validator转换所有的UnresolvedFunCall到ResolvedFunCall。
- 第3行针对当前Query创建求值器(Evaluator)。
- 第4行根据求值器,校验器创建表达式编译器(ExpCompiler)。
- 第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)
方法时:
- QueryAxis会调用Validator的
void validate(QueryAxis axis)
方法 - Validator反过来调用QueryAxis的
public void resolve(Validator validator)
方法 - QueryAxis又会调用Validator的
public Exp validate(Exp exp, boolean scalar)
方法 - 接着Validator又会调用UnresolvedFunCall的
public Exp accept(Validator validator)
方法。 - 最后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
}
它的执行有一下几步:
- 创建表达式参数
- 解析表达式为指定的函数定义(FunDef)
- 创建已解析的函数调用(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
}
- 第一步获取参数类型
- 第二步获取返回类型,这里要注意的是,对于(cube,dimension, hierarchy, level, member)的返回类型,我们需要指定到某个具体的返回类型中。假设返回一个level类型,我们要知道这个level是属于哪个hierarchy,哪个dimension。所以FunBase默认情况下是取的第一个参数的返回类型进行填充。对于某些函数来说,并不是第一个参数决定返回类型,所以需要自己实现。
- 根据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行判断表达式是否是切片轴,如果是,进入2,3步。
- 标准化表达式,即将表达式封装成集表达式({}表达式)。
- 对表达式执行前面1.1的解析过程,将UnresolvedFunCall转换成ResolvedFunCall。
- 最后使用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的实现),直到返回结果为止。