Zum Inhalt springen

Erweiterungen: Funktionen und Formatter

Mondrian bietet Schema-Autoren mehrere Erweiterungspunkte, um anzupassen, wie MDX-Funktionen arbeiten, wie Zellwerte angezeigt werden und wie Member-Beschriftungen sprachübergreifend erscheinen. Diese Seite behandelt die Erweiterungspunkte, die Sie am wahrscheinlichsten im Alltag verwenden werden.

Benutzerdefinierte Funktionen

Eine benutzerdefinierte Funktion (User-Defined Function, UDF) erlaubt es Ihnen, eine eigene Java- (oder Skript-)Funktion aus jedem MDX-Ausdruck aufzurufen. Mondrian ruft sie während der Abfrageauswertung genau wie eine integrierte Funktion auf.

Eine Java-UDF schreiben

Ihre Klasse muss einen öffentlichen parameterlosen Konstruktor haben und das Interface mondrian.spi.UserDefinedFunction implementieren. Das folgende Beispiel addiert eins zu seinem numerischen Argument:

package com.example;
import mondrian.olap.*;
import mondrian.olap.type.*;
import mondrian.spi.UserDefinedFunction;
/**
* A simple user-defined function which adds one to its argument.
*/
public class PlusOneUdf implements UserDefinedFunction {
// public constructor
public PlusOneUdf() {
}
public String getName() {
return "PlusOne";
}
public String getDescription() {
return "Returns its argument plus one";
}
public Syntax getSyntax() {
return Syntax.Function;
}
public Type getReturnType(Type[] parameterTypes) {
return new NumericType();
}
public Type[] getParameterTypes() {
return new Type[] { new NumericType() };
}
public Object execute(Evaluator evaluator, Exp[] arguments) {
final Object argValue = arguments[0].evaluateScalar(evaluator);
if (argValue instanceof Number) {
return new Double(((Number) argValue).doubleValue() + 1);
} else {
// Argument might be a RuntimeException indicating that
// the cache does not yet have the required cell value.
// The function will be called again when the cache is loaded.
return null;
}
}
public String[] getReservedWords() {
return null;
}
}

Eine UDF in Ihrem Schema registrieren

Deklarieren Sie die UDF innerhalb Ihres <Schema>-Elements nach Ihren Cubes:

<Schema ...>
...
<UserDefinedFunction name="PlusOne" className="com.example.PlusOneUdf"/>
</Schema>

Die UDF in MDX verwenden

Einmal registriert, können Sie die Funktion aus jeder MDX-Anweisung aufrufen:

WITH MEMBER [Measures].[Unit Sales Plus One]
AS 'PlusOne([Measures].[Unit Sales])'
SELECT
{[Measures].[Unit Sales]} ON COLUMNS,
{[Gender].MEMBERS} ON ROWS
FROM [Sales]

Eine Klasse über mehrere Funktionsnamen teilen

Wenn Sie Ihrer Klasse einen öffentlichen Konstruktor geben, der ein einzelnes String-Argument akzeptiert, übergibt Mondrian den Funktionsnamen an diesen Konstruktor. So kann eine Klasse mehrere registrierte UDFs bedienen:

public class PlusOrMinusOneUdf implements UserDefinedFunction {
private final String name;
private final boolean isPlus;
public PlusOrMinusOneUdf(String name) {
this.name = name;
if (name.equals("PlusOne")) {
isPlus = true;
} else if (name.equals("MinusOne")) {
isPlus = false;
} else {
throw new IllegalArgumentException("Unexpected name " + name);
}
}
public String getName() { return name; }
// ... getDescription, getSyntax, getReturnType, getParameterTypes as before ...
public Object execute(Evaluator evaluator, Exp[] arguments) {
final Object argValue = arguments[0].evaluateScalar(evaluator);
if (argValue instanceof Number) {
if (isPlus) {
return new Double(((Number) argValue).doubleValue() + 1);
} else {
return new Double(((Number) argValue).doubleValue() - 1);
}
} else {
return null;
}
}
public String[] getReservedWords() { return null; }
}

Registrieren Sie beide Namen, die auf dieselbe Klasse zeigen:

<Schema ...>
...
<UserDefinedFunction name="PlusOne" className="com.example.PlusOrMinusOneUdf"/>
<UserDefinedFunction name="MinusOne" className="com.example.PlusOrMinusOneUdf"/>
</Schema>

Automatische Erkennung über den Service-Provider-Mechanismus

Sie können Ihre UDF-Implementierungen in einer JAR-Datei verpacken und eine Datei META-INF/services/mondrian.spi.UserDefinedFunction mit einem voll qualifizierten Klassennamen pro Zeile einfügen. Mondrian entdeckt diese automatisch und macht sie für jedes in dieser JVM geladene Schema verfügbar – ohne schema-spezifische Deklaration.

Skript-UDFs (JavaScript)

Für einfache Logik, die Sie nicht kompilieren möchten, betten Sie die Implementierung in ein <Script>-Kind von <UserDefinedFunction> ein. Die folgenden Funktionen müssen im Skript vorhanden sein:

  • getParameterTypes()
  • getReturnType(parameterTypes)
  • execute(evaluator, arguments)

getName(), getDescription(), getReservedWords() und getSyntax() sind optional und haben sinnvolle Standardwerte basierend auf dem name-Attribut.

Hier ist eine Fakultätsfunktion in JavaScript:

<UserDefinedFunction name="Factorial">
<Script language="JavaScript">
function getParameterTypes() {
return new Array(new mondrian.olap.type.NumericType());
}
function getReturnType(parameterTypes) {
return new mondrian.olap.type.NumericType();
}
function execute(evaluator, arguments) {
var n = arguments[0].evaluateScalar(evaluator);
return factorial(n);
}
function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
</Script>
</UserDefinedFunction>

Cell Formatter

Ein Cell Formatter überschreibt, wie Mondrian den rohen Wert einer Measure-Zelle zur Anzeige formatiert. Er steuert, was Cell.getFormattedValue() zurückgibt. Ihre Klasse muss mondrian.spi.CellFormatter implementieren.

Einen Formatter an eine Measure anhängen

<Measure name="Revenue">
<CellFormatter className="com.example.MyCellFormatter"/>
</Measure>

Skript-basierter Cell Formatter

<Measure name="Revenue">
<CellFormatter>
<Script language="JavaScript">
var s = value.toString();
while (s.length() &lt; 20) {
s = "0" + s;
}
return s;
</Script>
</CellFormatter>
</Measure>

Das Skript erhält eine Variable value, die dem rohen Zellwert entspricht. Das Fragment kann mehrere Anweisungen enthalten, muss aber mit einer return-Anweisung enden.

Formatter auf einem berechneten Member

<CellFormatter> funktioniert auch auf <CalculatedMember>-Elementen:

calculated_members:
- name: "Double Sales"
dimension: "Measures"
formula: "[Measures].[Unit Sales] * 2"
cell_formatter:
script:
body: "var s = value.toString();\nwhile (s.length() < 20) { s = \"0\" +\
\ s; }\nreturn s;"

Formatter über CELL_FORMATTER-Property (MDX)

Für berechnete Measures, die in der WITH MEMBER-Klausel einer MDX-Abfrage definiert werden, setzen Sie die Property CELL_FORMATTER:

WITH MEMBER [Measures].[Foo]
AS '[Measures].[Unit Sales] * 2',
CELL_FORMATTER='com.example.MyCellFormatter'
SELECT {[Measures].[Unit Sales], [Measures].[Foo]} ON COLUMNS,
{[Store].Children} ON ROWS
FROM [Sales]

Für einen Skript-basierten Formatter inline in MDX verwenden Sie CELL_FORMATTER_SCRIPT und CELL_FORMATTER_SCRIPT_LANGUAGE:

WITH MEMBER [Measures].[Foo]
AS '[Measures].[Unit Sales] * 2',
CELL_FORMATTER_SCRIPT_LANGUAGE='JavaScript',
CELL_FORMATTER_SCRIPT='var s = value.toString(); while (s.length() < 20) s = "0" + s; return s;'
SELECT {[Measures].[Unit Sales], [Measures].[Foo]} ON COLUMNS,
{[Store].Children} ON ROWS
FROM [Sales]

Member Formatter

Ein Member Formatter überschreibt, wie Mondrian die Beschriftung eines Members formatiert. Er steuert, was Member.getCaption() zurückgibt. Ihre Klasse muss mondrian.spi.MemberFormatter implementieren.

Einen Formatter an eine Ebene anhängen

<Level name="Store Name" column="store_name">
<MemberFormatter className="com.example.MyMemberFormatter"/>
</Level>

Skript-basierter Member Formatter

<Level name="Store Name" column="store_name">
<MemberFormatter>
<Script language="JavaScript">
return member.getName().toUpperCase();
</Script>
</MemberFormatter>
</Level>

Das Skript erhält eine Variable member – das Member-Objekt, das formatiert wird. Das Fragment muss mit einer return-Anweisung enden.


Property Formatter

Ein Property Formatter überschreibt, wie Mondrian den Wert einer Member-Property formatiert. Er steuert, was Property.getPropertyFormattedValue() zurückgibt. Ihre Klasse muss mondrian.spi.PropertyFormatter implementieren.

Einen Formatter an eine Property anhängen

In Mondrian 4 lebt das <Property>-Element innerhalb einer <Attribute>-Definition:

<Attribute name="My Attribute" keyColumn="attributeColumn" uniqueMembers="true">
<Property name="My Property" column="propColumn">
<PropertyFormatter className="com.example.MyPropertyFormatter"/>
</Property>
</Attribute>

Skript-basierter Property Formatter

<Attribute name="Store Name" keyColumn="store_name">
<Property name="Store Type" column="store_type">
<PropertyFormatter>
<Script language="JavaScript">
return member.getName().toUpperCase();
</Script>
</PropertyFormatter>
</Property>
</Attribute>

Das Skript erhält die Variablen member, propertyName und propertyValue. Das Fragment muss mit einer return-Anweisung enden.


Ihr Schema lokalisieren

Mondrian erlaubt es Ihnen, Ihr Schema in mehreren Sprachen anzubieten, ohne separate Schema-Dateien zu pflegen. Der Ansatz kombiniert zwei Dinge: sprachspezifische Beschriftungs- und Beschreibungswerte, die als <Annotation>-Elemente gespeichert werden, und die Klasse LocalizingDynamicSchemaProcessor, die das Schema-XML für jede Sprache zur Laufzeit umschreibt.

<Annotation> für sprachspezifische Metadaten

Jedes Schema-Objekt (<Schema>, <Cube>, <Dimension>, <Hierarchy>, <Level>, <Measure>, benannte Sets usw.) unterstützt einen <Annotations>-Kindblock. Die Mondrian-Konvention ist, punktqualifizierte Annotationsnamen für Sprachmetadaten zu verwenden:

cubes:
Sales:
caption: "Sales"
annotations:
caption.de_DE: "Verkaufen"
caption.fr_FR: "Ventes"
description.fr_FR: "Cube des ventes"
description.de_AT: "Cube den Verkaufen"

Client-Tools, die diese Konvention verstehen, können dann Beschriftungen in der Sprache des Benutzers anzeigen. Das YAML-Schema-Format unterstützt diese Annotationen direkt unter dem annotations-Schlüssel jedes Elements – siehe YAML-Schema-Referenz für die vollständige Syntax.

Substitution per Properties-Datei mit LocalizingDynamicSchemaProcessor

Für eine vollständige serverseitige Sprach-Substitution schreiben Sie Ihr Schema mit %{key}-Platzhaltern in den Attributen caption, description, allMemberCaption und measuresCaption:

<Schema name="FoodMart" measuresCaption="%{foodmart.measures.caption}">
<Dimension name="Store" caption="%{foodmart.dimension.store.caption}"
description="%{foodmart.dimension.store.description}">
<Hierarchy hasAll="true"
allMemberName="All Stores"
allMemberCaption="%{foodmart.dimension.store.allmember.caption}"
primaryKey="store_id">
<Table name="store"/>
<Level name="Store Country" column="store_country" uniqueMembers="true"
caption="%{foodmart.dimension.store.country.caption}"
description="%{foodmart.dimension.store.country.description}"/>
<Level name="Store State" column="store_state" uniqueMembers="true"
caption="%{foodmart.dimension.store.state.caption}"/>
<Level name="Store City" column="store_city" uniqueMembers="false"
caption="%{foodmart.dimension.store.city.caption}"/>
</Hierarchy>
</Dimension>
<Cube name="Sales" caption="%{foodmart.cube.sales.caption}"
description="%{foodmart.cube.sales.description}">
...
<MeasureGroup table="sales_fact_1997">
<Measures>
<Measure name="Unit Sales" column="unit_sales"
caption="%{foodmart.cube.sales.measure.unitsales.caption}"
description="%{foodmart.cube.sales.measure.unitsales.description}"/>
</Measures>
</MeasureGroup>
</Cube>
</Schema>

Stellen Sie eine Standard-Properties-Datei (locale.properties) und eine pro unterstützter Sprache (locale_fr.properties, locale_de.properties usw.) bereit. Jede Datei ordnet die Platzhalterschlüssel übersetzten Strings zu:

# locale.properties — default (English)
foodmart.measures.caption=Measures
foodmart.dimension.store.caption=Store
foodmart.dimension.store.country.caption=Store Country
foodmart.dimension.store.state.caption=Store State
foodmart.dimension.store.city.caption=Store City
foodmart.dimension.store.allmember.caption=All Stores
foodmart.cube.sales.caption=Sales
foodmart.cube.sales.measure.unitsales.caption=Unit Sales
# locale_hu.properties — Hungarian
foodmart.measures.caption=Hungarian Measures
foodmart.dimension.store.caption=Áruház
foodmart.dimension.store.country.caption=Ország
foodmart.dimension.store.state.caption=Állam/Megye
foodmart.dimension.store.city.caption=Város
foodmart.dimension.store.allmember.caption=Minden Áruház
foodmart.cube.sales.caption=Forgalom
foodmart.cube.sales.measure.unitsales.caption=Eladott db

Der LocalizingDynamicSchemaProcessor wird über den Mondrian-Connection-String aktiviert. In Saiku Cloud wird dies auf der Datenquellen-Ebene durch Ihren Administrator konfiguriert. Wenn Sie eine Sprach-Substitution auf Schema-Ebene benötigen, wenden Sie sich an den Support.


Fortgeschritten: Selbstgehostete Erweiterungspunkte

Die drei folgenden Erweiterungspunkte gelten nur für selbstgehostete oder eingebettete Mondrian-Deployments. Sie erfordern Konfiguration auf Deployment-Ebene (Connection-Strings, web.xml-Servlet-Deklarationen) und werden typischerweise nicht benötigt, wenn Sie Saiku Cloud verwenden.

  • Dynamic Schema Processor (mondrian.spi.DynamicSchemaProcessor): Fängt das Schema-Laden ab, um das Schema-XML bei jeder ROLAP-Verbindung zu filtern oder zu generieren. Wird verwendet, um sprach- oder mandantenspezifische Inhalte zur Ladezeit einzuspielen.
  • Data Source Change Listener (mondrian.spi.DataSourceChangeListener): Klinkt sich in Mondrians Cache-Invalidierungspfad ein. Beim Aufruf signalisiert er, ob sich die zugrundeliegenden Dimensions- oder Faktendaten geändert haben, und löst eine Cache-Leerung aus.
  • Dynamic Datasource XMLA Servlet (mondrian.xmla.impl.DynamicDatasourceXmlaServlet): Ein alternatives XMLA-Servlet, das datasources.xml bei jeder Client-Anfrage neu lädt und Katalog-Caches selektiv für geänderte Einträge leert.

Wenn Sie Mondrian eingebettet betreiben und eines dieser SPIs implementieren müssen, konsultieren Sie das Mondrian-Javadoc für mondrian.spi.


Adaptiert aus dem Mondrian-Projekt-Schema-Guide (EPL v1.0).