Mondrian入门



众所周知,Mondrian是一个开源OLAP引擎。国内很多BI产品都会在此基础上开发。不过网上的资料比较老旧,而且给出例子多是基于jPivot表现层的,jPivot已经多年没有更新了,部署起来也比较麻烦。最新的Mondrian3.6的下载已经将jPivot移除了,如果想学习官方demo的可以去下载3.5的。

我觉得更基础轻量的例子会比较适合入门,下面我就将我最先学习Mondrian的例子分享给大家。这个Java实例将利用Mondrian提供的OLAP引擎对已建立好的数据立方体XML进行MDX查询。

多维数据建模和MDX语法,网上的资料很多,这里就不再冗述了。

首先是关系数据库的表结构(这里插一句,我用的Derby数据库,Derby数据库会为表、字段名强制加上双引号,刚开始带来了不少麻烦):

--时间维表
CREATE TABLE "dim_time"
(
"time_id" VARCHAR(50) NOT NULL,--id
"time_year" VARCHAR(50),--年
"time_quarter" VARCHAR(50),--季度
"time_month" VARCHAR(50),--月
PRIMARY KEY("time_id")
);

--销售点维表
CREATE TABLE "dim_store"
(
"store_id" VARCHAR(50) NOT NULL,--id
"store_province" VARCHAR(50),--省
"store_city" VARCHAR(50),--市
"store" VARCHAR(50),--商店
PRIMARY KEY("store_id")
);

--销售事实表
CREATE TABLE "fact_sales"
(
"sales_id" VARCHAR(50) NOT NULL,--id
"store_id" VARCHAR(50) NOT NULL,--销售点
"time_id" VARCHAR(50) NOT NULL,--时间
"cost" INTEGER,--成本
"sales" INTEGER,--销量
PRIMARY KEY("sales_id")
);

这是一个非常经典场景,有两个维度分别是时间和地点来描述销售情况。


然后下面是用来描述数据立方体的XML文件(sample.xml)。

<?xml version="1.0" encoding="UTF-8" ?>
<Schema name="Sample">

<!-- 时间维度表 -->
<Dimension name="Time">
	<Hierarchy hasAll="true" allMemberName="所有时间" primaryKey="time_id">
		<Table name="dim_time"/>
		<Level name="年" column="time_year" uniqueMembers="true"/>
		<Level name="季度" column="time_quarter" uniqueMembers="false"/>
		<Level name="月" column="time_month" uniqueMembers="false"/>
	</Hierarchy>
</Dimension>

<!-- 商场维度表 -->
<Dimension name="Store">
	<Hierarchy hasAll="true" allMemberName="所有销售点" primaryKey="store_id">
		<Table name="dim_store"/>
		<Level name="省" column="store_province" uniqueMembers="true"/>
		<Level name="市" column="store_city" uniqueMembers="true"/>
		<Level name="商场" column="store" uniqueMembers="true"/>
	</Hierarchy>
</Dimension>

<!-- 一个数据立方体 -->
<Cube name="销售情况">
	<!-- 事实表 -->
	<Table name="fact_sales"/>
	
	<!-- 维表 -->
	<DimensionUsage name="时间" source="Time" foreignKey="time_id"/>
	<DimensionUsage name="销售点" source="Store" foreignKey="store_id"/>
	
	<!-- 度量 -->
	<Measure name="销售额" column="sales" aggregator="sum" datatype="Integer" formatString="#,##0"/>
	<Measure name="成本" column="cost" aggregator="sum" datatype="Integer" formatString="#,##0"/>
	<Measure name="平均销售额" column="sales" aggregator="avg" datatype="Integer" formatString="#,##0"/>
	<Measure name="平均成本" column="cost" aggregator="avg" datatype="Integer" formatString="#,##0"/>
	<!-- 度量(计算成员) -->
	<CalculatedMember name="利润" dimension="Measures">
		<Formula>[Measures].[销售额] - [Measures].[成本]</Formula>
		<CalculatedMemberProperty name="FORMAT_STRING" value="#,##0"/>
	</CalculatedMember>
	
</Cube>

</Schema>

这里也是简单说下,里面定义了两个维表和一个事实表,然后定义了几个度量,分别用求和(sum)和平均值(avg)进行聚集,最后定义了一个计算成员(CalculatedMember),可以通过MDX片段计算出一个度量。


接下来就是Java实例了。

        // 数据库连接信息,这里用的derby
        String dbName = "sample";
        String driver = "org.apache.derby.jdbc.EmbeddedDriver";
        String url = "jdbc:derby:" + dbName;
        String userName = "sa";
        String password = "sa";
        
        // 立方体定义文件
        String xmlFile = "sample.xml";
        
        // mdx查询
        String mdxStr = "select {[Measures].[销售额],[Measures].[利润]} on columns,"
                + "{[季度].Members} ON rows" 
                + " from 销售情况 " 
                + "where [商场].[支店01]";
        
        // 建立连接
        PropertyList connectInfo = new PropertyList();
        connectInfo.put("Provider", "mondrian");
        connectInfo.put("JdbcDrivers", driver);
        connectInfo.put("Jdbc", url);
        connectInfo.put("JdbcUser", userName);
        connectInfo.put("JdbcPassword", password);
        connectInfo.put("Catalog", xmlFile);
        mondrian.olap.Connection conn = mondrian.olap.DriverManager
                .getConnection(connectInfo, null);
        
        // 执行查询
        mondrian.olap.Query query = conn.parseQuery(mdxStr);
        mondrian.olap.Result result = conn.execute(query);

        // 输出结果
        PrintWriter pw = new PrintWriter(System.out);
        result.print(pw);
        pw.flush();
大部分情况上面的代码可以工作良好,但是这里执行之后出现了以下错误。
Caused by: mondrian.olap.MondrianException: Mondrian Error:MDX cube '销售情况' not found
	at mondrian.resource.MondrianResource$_Def0.ex(MondrianResource.java:969)
	at mondrian.olap.Util.lookupCube(Util.java:1053)
	at mondrian.olap.Query.<init>(Query.java:161)
	at mondrian.olap.Parser$FactoryImpl.makeQuery(Parser.java:927)
	at mondrian.parser.MdxParserImpl.selectStatement(MdxParserImpl.java:1241)
	at mondrian.parser.MdxParserImpl.statement(MdxParserImpl.java:1074)
	at mondrian.parser.MdxParserImpl.statementEof(MdxParserImpl.java:188)
	at mondrian.parser.JavaccParserValidatorImpl.parseInternal(JavaccParserValidatorImpl.java:57)
	at mondrian.olap.ConnectionBase.parseStatement(ConnectionBase.java:96)
	... 3 more
只有当XML中出现中文时才会出现这样的错误,这很明显是字符编码的问题,通过log可以发现MDX已经被正常解析并没有出现乱码,由此可以推测出问题出在数据立方体XML的解析上,但是Mondrian并没有提供连接字符集的设置方法。还好它是一个开源项目,代码并不多,很快就能找到解析XML的地方,在mondrian.olap.Util.java中有这么一个方法readVirtualFileAsString,如下:
    public static String readVirtualFileAsString(
        String catalogUrl)
        throws IOException
    {
        InputStream in = readVirtualFile(catalogUrl);
        try {
            final byte[] bytes = Util.readFully(in, 1024);
            final char[] chars = new char[bytes.length];
            for (int i = 0; i < chars.length; i++) {
                chars[i] = (char) bytes[i];
            }
            return new String(chars);
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

我们可以看到它将xml文件字节流中的每一个字节转换为字符之后才构造xml字符串,难怪即使将xml的编码格式改成GBK都没辙,这遇到中文不乱码才怪了。这种处理方式我也想不通,反正老外从来也不考虑我们中国程序员的感受。

如果不想修改它的源码的话,这里有一种解决办法。创建连接本来就有两种方式,一种就是上面用到的指定xml文件的路径(Catalog),还有一种就是直接传入xml文件的内容(CatalogContent),我们用这种方式就不怕乱码了。

首先仿造上面的readVirtualFileAsString新建一个getCatalogContent方法

    private static String getCatalogContent() throws Exception {
        InputStream inputStream = mondrian.olap.Util.readVirtualFile(xmlFile);

        try {
            final byte[] bytes = Util.readFully(inputStream, 1024);

            // 下面是mondrian原来的实现,由于byte被强制转化为char,汉字全为乱码,故将这段处理处理掉。
            // final char[] chars = new char[bytes.length];
            // for (int i = 0; i < chars.length; i++) {
            // chars[i] = (char) bytes[i];
            // }

            return new String(bytes, "UTF-8");
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

然后对数据连接方式稍作修改

        // 建立连接
        PropertyList connectInfo = new PropertyList();
        connectInfo.put("Provider", "mondrian");
        connectInfo.put("JdbcDrivers", driver);
        connectInfo.put("Jdbc", url);
        connectInfo.put("JdbcUser", userName);
        connectInfo.put("JdbcPassword", password);
        // mondrian默认解析xml的方法不带字符集,中文会有乱码,故自行取得CatalogContent
        // connectInfo.put("Catalog", xmlFile);
        connectInfo.put("CatalogContent", getCatalogContent());
        mondrian.olap.Connection conn = mondrian.olap.DriverManager
                .getConnection(connectInfo, null);


执行成功,结果如下:

Axis #0:
{[销售点].[湖北省].[武汉市].[支店01]}
Axis #1:
{[Measures].[销售额]}
{[Measures].[利润]}
Axis #2:
{[时间].[2012].[Q1]}
{[时间].[2012].[Q2]}
{[时间].[2012].[Q3]}
{[时间].[2012].[Q4]}
{[时间].[2013].[Q1]}
{[时间].[2013].[Q2]}
{[时间].[2013].[Q3]}
{[时间].[2013].[Q4]}
Row #0: 330,000
Row #0: -40,000
Row #1: 330,000
Row #1: -30,000
Row #2: 460,000
Row #2: 60,000
Row #3: 470,000
Row #3: 130,000
Row #4: 440,000
Row #4: 110,000
Row #5: 370,000
Row #5: -10,000
Row #6: 480,000
Row #6: 130,000
Row #7: 400,000
Row #7: 30,000


写到这里,其实还有一个问题,上面利用到的conn.execute(query)方法其实是已经过时了的,查看doc会发现这个方法将在4.0版本后被移除,官方推荐利用olap4j的实现。olap4j是一个OLAP的API,现在已成了标准,下面就是olap4j的实现方式。

        // 建立连接
        String strUrl = "jdbc:mondrian:";
        strUrl += "Jdbc=" + url;
        strUrl += ";JdbcUser=" + userName;
        strUrl += ";JdbcPassword=" + password;
        // strUrl += ";Catalog=" + xmlFile;
        strUrl += ";CatalogContent=" + getCatalogContent();
        Class.forName("mondrian.olap4j.MondrianOlap4jDriver");
        Connection olap4jConn = DriverManager.getConnection(strUrl);
        OlapConnection olapConn = olap4jConn.unwrap(OlapConnection.class);
        
        // 执行查询
        OlapStatement statement = olapConn.createStatement();
        CellSet cellSet = statement.executeOlapQuery(mdxStr);

        // 输出结果
        CellSetFormatter formatter = new RectangularCellSetFormatter(false);
        formatter.format(cellSet, new PrintWriter(System.out, true));

执行结果:

|           | 销售额     | 利润      |
+------+----+---------+---------+
| 2012 | Q1 | 330,000 | -40,000 |
|      | Q2 | 330,000 | -30,000 |
|      | Q3 | 460,000 |  60,000 |
|      | Q4 | 470,000 | 130,000 |
| 2013 | Q1 | 440,000 | 110,000 |
|      | Q2 | 370,000 | -10,000 |
|      | Q3 | 480,000 | 130,000 |
|      | Q4 | 400,000 |  30,000 |

比Mondrian的更直观点。


以上就是我刚开始学习Mondrian是制作的例子,完整工程请到此下载:http://download.csdn.net/detail/chch87/7210135



猜你喜欢

转载自blog.csdn.net/chch87/article/details/23953731