内容


使用 Flex3 开发 OLAP 应用

Comments

概要

联机分析处理(Online Analytical Processing,下文简称 OLAP)是一种共享多维信息、针对特定问题的联机数据访问和分析的快速软件技术。OLAP 专门设计用于支持复杂的分析操作,侧重对决策人员和高层管理人员的决策支持,可以根据分析人员的要求快速、灵活地进行大数据量的复杂查询处理。本文将介绍如果利用 Flex 技术将这些复杂的查询处理结果美观,便捷地展现出来。

软件环境要求

  • Flash Player 9 或更高版本
  • Adobe Flex SDK 3.0 或更高版本
  • Adobe Flex SDK Builder 3 或更高版本

Flex 中的分析组件

在传统的 OLAP 分析组件开发中,各商业智能公司均有自己的一套产品实现,从设计和使用上均不具有开放性。而在 Flex SDK 中,原生地提供了众多分析组件,如饼图,柱状图等各种图表控件,并于 Flex3 中加入了 OLAPDataGrid,这使得基于 Flex 开发 OLAP 应用更加便捷。另外,由于 Flex 类库的良好扩展性,使得定制化的开发成为可能,进而满足 OLAP 分析中种类繁多的用户需求。

图表控件的使用

使用 Flex 中的组件通常有两种方式,一种是直接在 .mxml 文件的类 xml 标签中通过属性来指定这个组件的展现方式,这是最直观的方式。而另一种则将显示与逻辑分离,在 .mxml 文件中只是声明了将使用的组件,而具体的获得数据过程与数据展现过程则在相对应的 .as (actionscript) 中用编程的方式实现。本文将采用第二种方式进行描述:

  • 首先新建一个 OLAPAppInFlex 的 Flex application。
  • 接着用 ViewStack 组件将我们所要展示的三种图表控件粘合在一起,通过点击 ViewStack 的 tab 页来查看不同的展示效果。
清单 1. OLAPAppInFlex.mxml
<mx:Script source="include/Chart.as" />
……
 <mx:ViewStack id="viewStack" width="100%" height="75%" creationComplete="showData()">
  <mx:HBox id="columnBox" name="ColumnChart" width="100%"> 
    <mx:ColumnChart id="columnChart" showDataTips="true" width="100%" height="100%"/>
    <mx:Panel id="columnLegendPanel" horizontalScrollPolicy="off"> 
      <mx:Box maxHeight="250" horizontalScrollPolicy="off"> 
        <mx:Legend id="columnLegend" width="100%" /> 
      </mx:Box> 
    </mx:Panel> 
  </mx:HBox> 

  <mx:HBox id="pieBox" name="PieChart" width="100%">
  </mx:HBox> 

  <mx:HBox id="lineBox" name="LineChart" width="100%"> 
    <mx:LineChart id="lineChart" showDataTips="true" width="100%" height="100%" />
    <mx:Panel id="lineLegendPanel" horizontalScrollPolicy="off"> 
      <mx:Box maxHeight="250" horizontalScrollPolicy="off"> 
        <mx:Legend id="lineLegend" width="100%" /> 
      </mx:Box> 
    </mx:Panel> 
  </mx:HBox> 
 </mx:ViewStack>

在上述代码中第一行引入了 Chart.as,并在此文件中实现了图形的展现逻辑。

而在介绍 Chart.as 如何实现图形的展现逻辑前,先介绍两个一般性的问题:

  • 数据的来源与格式

一般说来,在 Flex UI 上展现的数据都是首先通过服务器端处理后返回的,而 server 和 client 通讯的方式可能是 webservice,httpservice,也可能是序列化好的二进制对象。本文为了清晰地说明 Flex UI 端的功能,故将与服务器通讯(即 OLAP 服务器)这一部分省去,并采用经典 OLAP 示例中最常用的二维数据:产品维,时间维,以及相对应的度量值:销售额来进行阐述。如清单 2 所示。

清单 2. 本文使用数据示例
 <dataset> 
 <dim1label>Product Department Level</dim1label> 
 <dim2label>Year Level</dim2label> 
 <item> 
  <dim1>FURNITURE</dim1> 
  <dim2>2009</dim2> 
  <measure>4019.08</measure> 
 </item> 
 <item> 
  <dim1>FURNITURE</dim1> 
  <dim2>2008</dim2> 
  <measure>2461.49</measure> 
 </item> 
 <item> 
  <dim1>FURNITURE</dim1> 
  <dim2>2006</dim2> 
  <measure>2188.57</measure> 
 </item> 
 ... 
 </dataset>
  • 数据的解析

本文中所展示的数据均采用标准的 xml 格式。

原因之一是 Flex API 有着异常简洁而强大的 xml 处理能力,很多组件亦可以直接接收 xml 数据作为其 dataprovider。在本文中,所有数据均用 Flex 的 XML API 进行解析。

原因之二是 IBM 的 DB2 系列产品从 V9 之后对 xml 有原生的支持,这样从数据库到 server,再到 client,甚至不需要对额外的转换操作,均能统一成标准的 xml 格式。

接下来我们介绍 Flex 中的 Column Chart 与 Pie Chart 组件对于数据的展示。

Column Chart

Column Chart 即通常所说的柱状图,其数据组成方式分为 X 轴和 Y 轴,分别展示待显示数据的某种维度信息。对于本文的例子来说,X 轴是从 xml 数据中提取的时间维信息(清单 3 中所示的 dim2list),而 Y 轴是从 xml 数据中提取的对应各种产品维的销售值。

清单 3. 准备维度信息
 dim1list = new Array(); 
 dim2list = new Array(); 

 // 取出清单 2 中的不同维度信息 ( 产品维 ),形成 dim1list,以便之后对 dim1list 进行
 // 遍历得到具体的度量值 ( 销售值 ) 
 for each(var dim1val:String in chartXML.item.dim1) { 
    if (dim1list.indexOf(dim1val) < 0) 
        dim1list.push(dim1val); 
 } 

 // 取出清单 2 中的不同维度信息 ( 时间维 ),形成 dim2list 
 // 如果需要在 X 轴上显示产品维信息,则简单地将 dim1 与 dim2 互换即可达到效果
 for each(var dim2val:String in chartXML.item.dim2) { 
    if (dim2list.indexOf(dim2val) < 0) 
        dim2list.push(dim2val); 
 }

而每一个柱状图集合对应于一个 ColumnSeries,在此例子中,我们遍历接收到的 xml 数据,构建了多个 ColumnSeries,并对每个 ColumnSeries 设置其相应的产品维信息作为 dataProvider。最后,每个 CategoryAxis 是数据按照 X 轴维度展示的依据。

清单 4. 创建 Column Chart
 columnChart.series = new Array(); 

 // 对 dim1list 进行遍历,并分别建立对应的 series 
 for each (var s:String in dim1list) { 
    // 得到遍历时该维度的度量值集合
    var localXML:XMLList = chartXML.item.(dim1==s);

    // 创建该维度对应的 series 并设置相应属性
    var localSeries:ColumnSeries = new ColumnSeries();localSeries.dataProvider = localXML;localSeries.yField = "measure";localSeries.xField = "dim2";

    // 设置提示信息
    localSeries.displayName = s; 

    // 将该维度 series 置入柱状图中
    columnChart.series.push(localSeries); 
 } 

 // 横 (X) 轴是另一种维度信息 ( 此例中是时间维 ) 
 var hAxis:CategoryAxis = new CategoryAxis(); 

 hAxis.dataProvider = dim2list; 
 hAxis.title = chartXML.dim2label; 
 columnChart.horizontalAxis = hAxis; 
 ... 
 }
图 1. Column Chart
图 1. Column Chart
图 1. Column Chart

Column Chart 的扩展

针对 Column Chart,亦可以简单的作如下扩展:

  • 将 X 轴和 Y 轴互换——这可以通过变换 xml 数据遍历的方式达到。
  • 柱状图上显示数据的方式——此处柱状图上显示的数据值均为 xml 数据的返回值,而在某些需求下显示的为 MAX,MIN,AVG 或 SUM 值。这种情况下,我们可以用两种方式达到效果:一是在本地对得到的数据进行处理,二是重新发出请求至服务器端,并返回想要的数据。
  • 在柱状图上添加上钻,下钻操作——这可以通过对柱状图以编程的方式扩展其 item renderer 实现,具体逻辑可参考后文对 OLAPDataGrid 的扩展。

Pie Chart

在构建 PieChart 时,我们在 .mxml 文件中仅定义了

<mx:HBox id="pieBox" name="PieChart" width="100%"></mx:HBox>

而关于 PieChart 的一切细节,在 actionscript 中动态生成。PieChart 与 ColumnChart 在构建上有很大的不同,首先它没有所谓的 CategoryAxis(因为饼图并没有 X 轴和 Y 轴),而是用类似于极坐标的方式来展现和定位数据。如果我们尝试把多个饼图(每个以 PieSeries 的方式)在一个组件中展示出来时,Flex 的默认行为为多个饼图以嵌套的方式进行呈现。如图 2 所示。

图 2. Pie Chart – default style
图 2. Pie Chart – default style

Pie Chart 的扩展

对于饼图,我们更多情况中并不需要对某一维度进行比较,而饼图本身从设计上也并不能很好的展示出这种比较。所以我们更希望多个饼图以彼此不影响的方式进行呈现。

这就需要借助 actionscript 中对 Flex 组件动态构建的功能。我们知道,Flex 的类库和组件都是以面向对象的方式设计的,任何组件都可以看成一个对象,其孩子元素以树的方式逐级构建出这个组件的展现。在这里,我们可以将 pieBox 作为一个根元素,并根据传来的 xml 数据,动态地构建相应数量的 panel,而在每个 panel 中,相应添加一个 PieChart。

清单 5. 扩展 pieChart
 private function createPieChart():void { 
    pieBox.removeAllChildren(); 
    var length:int = dim1list.length; 
    for each (var s:String in dim2list) { 
        var pieChartPanel:Panel = new Panel(); 
    
        var localXML:XMLList = chartXML.item.(dim2==s);
        // 与清单 1 在 mxml 中设置默认的 columnChartcreate 不同,此处是在 actionscript 中
        // 以面向对象的编程方式创建 pieChart,但他们的效果是一样的
        var pieChart:PieChart = new PieChart();
        pieChart.showDataTips = true; 
        // 创建饼图对应的 series 
        var localSeries:PieSeries = new PieSeries();localSeries.dataProvider = localXML;localSeries.field = "measure";localSeries.nameField = "dim1";
        localSeries.displayName = s; 
    
        pieChart.series.push(localSeries); 
    
        pieChartPanel.title = s; 
        // 为了此处扩展需要,将一个 pieChart 置于一个 pieChartPanel 中
        pieChartPanel.addChild(pieChart); 
        // 每次遍历将创建完的 pieChart 置于 pieBox 中
        pieBox.addChild(pieChartPanel); 
    } 

    var piePanel:Panel = new Panel(); 
    piePanel.horizontalScrollPolicy = "off"; 
    piePanel.title = chartXML.dim1label; 

    // legend 是图 3 右边的指示板
    var pieLegendBox:Box = new Box(); 
    pieLegendBox.horizontalScrollPolicy = "off"; 
    pieLegendBox.maxHeight = 250; 
    var pieLegend:Legend = new Legend(); 
    pieLegend.dataProvider = pieChart; 

    pieLegendBox.addChild(pieLegend); 
    piePanel.addChild(pieLegendBox); 
    pieBox.addChild(piePanel); 
 }
图 3. Pie Chart – customized style
图 3. Pie Chart – customized style
图 3. Pie Chart – customized style

OLAPDataGrid 的使用

在 Flex3 之前,大多数的表格都是基于 DataGrid 的,其以多列布局显示给定的数据集。而在 Flex3 中,添加了两个类似于 DataGrid 的控件:AdvancedDataGrid 和 OLAPDataGrid。其中 OLAPDataGrid 组件的出现,大大加速了 OLAP 应用中常见的多表头数据集的开发。接下来我们重点介绍 OLAPDataGrid 的用法及相应扩展。

在 .mxml 中定义 OLAPDataGrid 组件的代码如下:

<mx:OLAPDataGrid id="myOLAPDG" width="100%" height="100%"></mx:OLAPDataGrid>

而具体的构建 OLAPDataGrid 的动作,同样我们也放在 actionscript 中进行。数据立方体模型作为 OLAP 应用中的模型核心,在 Flex 的 OLAPDataGrid 组件中亦如此。

  1. 首先我们需要在逻辑定义 cube 模型的各维度信息,本例子就是清单 1 中所示的产品维和时间维。
  2. 接着我们就可以构建这个 cube。OLAPCube 的 elements 属性包含了所有需要显示的维度,所以此例中将两维信息(产品,时间)以 OLAPDimension 加入其中,而度量值(销售额)则以 OLAPMeasure 的方式加入其中。对于产品维和时间维需要用 OLAPDimension 来设置相应的 OLAPAttribute 与 OLAPHierarchy,销售额维则采用 OLAPMeasure 来设置。
    清单 6. createCube
     private function createMyCube():void { 
        myCube = new OLAPCube(); 
        myCube.addEventListener(CubeEvent.CUBE_COMPLETE, runQuery); 
    
        // 创建 dim1 维度
        var dim1:OLAPDimension = new OLAPDimension("Dim1"); 
    
        // 对于 dim1 创建其 attributes 
        var attr1:OLAPAttribute = new OLAPAttribute(chartXML.dim1label); 
        attr1.dataField = "dim1"; 
        attr1.displayName = chartXML.dim1label; 
    
        dim1.attributes = new ArrayCollection([ attr1 ]); 
    
        // 在维上创建用户定义的 hierarchy 
        var dim1Hierarchy:OLAPHierarchy = new OLAPHierarchy("Dim1"); 
    
        // 在 hierarchy 上定义 level 
        var level1:OLAPLevel = new OLAPLevel(); 
        level1.attributeName = chartXML.dim1label; 
    
        dim1Hierarchy.levels = new ArrayCollection([ level1 ]); 
    
        // 将创建好的 hierarchy 设置在 dim1 上
        dim1.hierarchies = new ArrayCollection([ dim1Hierarchy ]); 
    
        // 创建 dim2 维度
        var dim2:OLAPDimension = new OLAPDimension("Dim2"); 
    
        // 对于 dim2 创建其 attributes 
        var attr2:OLAPAttribute = new OLAPAttribute(chartXML.dim2label); 
        attr2.dataField = "dim2"; 
        //attr2.displayName = chartXML.dim2label; 
    
        dim2.attributes = new ArrayCollection([ attr2 ]); 
    
        // 在维上创建用户定义的 hierarchy 
        var dim2Hierarchy:OLAPHierarchy = new OLAPHierarchy("Dim2"); 
    
        // 在 hierarchy 上定义 level 
        var level2:OLAPLevel = new OLAPLevel(); 
        level2.attributeName = chartXML.dim2label ; 
    
        dim2Hierarchy.levels = new ArrayCollection([ level2 ]); 
    
        // 将创建好的 hierarchy 设置在 dim2 上
        dim2.hierarchies = new ArrayCollection([ dim2Hierarchy ]); 
    
        // 创建 measure 度量信息
        var measure:OLAPMeasure = new OLAPMeasure("Measure"); 
        measure.dataField = "measure"; 
        measure.aggregator = "SUM"; 
    
        // dim1 维度,dim2 维度以及 measure 度量信息构成了整个 cube 的所有元素
        myCube.elements = [ dim1, dim2,  measure ]; 
     }
  3. 然后我们创建 OLAPQuery 来查询。myCube.addEventListener(CubeEvent.CUBE_COMPLETE, runQuery); 在这句话中,用到了 Flex 中常用的回调机制——当 cube 模型建立完成后,会自动调用 runQuery 方法来执行默认的查询。在 runQuery 方法中,接收 CubeEvent 的 target 作为其 IOLAPCube,然后构建一个 IOLAPQuery 并继而执行之。

    在每个 IOLAPQuery 中,均有 ROW_AXIS 与 COLUMN_AXIS 两个维度。并继而对横轴和纵轴分别添加相应的 OLAPSet 信息以填充具体的数据信息。

    清单 7. runQuery
     private function runQuery(cubeEvent:CubeEvent):void { 
        // 得到清单 6 创建好的 cube,cubeEvent 通过回调的方式将其返回
        var cube:IOLAPCube = IOLAPCube(cubeEvent.currentTarget); 
        // 创建查询实例
        var query:IOLAPQuery = getQuery(cube); 
        // 执行查询
        var token:AsyncToken = cube.execute(query); 
        // 设置回调函数来处理返回结果及错误信息
        token.addResponder(new AsyncResponder(showResult, showFault)); 
     }
    清单 8. getQuery
     private function getQuery(cube:IOLAPCube):IOLAPQuery { 
        // 创建 OLAPQuery 实例来表示一次查询
        var query:OLAPQuery = new OLAPQuery; 
    
        // 得到此查询的横轴
        var rowQueryAxis:IOLAPQueryAxis = query.getAxis(OLAPQuery.ROW_AXIS); 
        // 创建对应于横轴的 OLAPSet 
        var productSet:OLAPSet = new OLAPSet; 
        // 从 cube 中将关于产品维的聚集数据加入此 OLAPSet 中
        productSet.addElements( 
            cube.findDimension("Dim1").findAttribute(chartXML.dim1label).children); 
        rowQueryAxis.addSet(productSet); 
    
        // 得到此查询的纵轴
        var colQueryAxis:IOLAPQueryAxis = query.getAxis(OLAPQuery.COLUMN_AXIS); 
        // 创建对应于纵轴的 OLAPSet 
        var quarterSet:OLAPSet= new OLAPSet; 
        // 从 cube 中将关于时间维的聚集数据加入此 OLAPSet 中
        quarterSet.addElements( 
            cube.findDimension("Dim2").findAttribute(chartXML.dim2label).children); 
        colQueryAxis.addSet(quarterSet); 
    
        return query; 
     }
  4. 最后用 OLAPDataGrid 来显示查询结果。一般来说我们可以在 .mxml 文件可以设置类似 <mx:xxx></mx:xxx> 的方式来构建 OLAPDataGrid,但在本例中,所有 OLAPDataGrid 的子元素构建过程均通过 actionscript 来完成。
    清单 9. showResult
     private function showResult(result:Object, token:Object):void { 
        if (!result) { 
            Alert.show("No results from query."); 
            return; 
        } 
    
        myOLAPDG.dataProvider= result as OLAPResult; 
     }
    图 4. OLAPDataGrid
    图 4. OLAPDataGrid
    图 4. OLAPDataGrid

OLAPDataGrid 的扩展

作为 Flex3 后新添加的组件,OLAPDataGrid 仍有一定的局限性。如一般的 OLAP 操作中均允许客户在图表直接进行点击,以达到数据上钻及下钻的效果。

对 Flex 已有组件扩展需要在其之上创建自定义的渲染器,而这通常有两种方法:定义一个 renderer(item 类型或 header 类型)在 MXML 的组件内部;或定义在另一个类文件中,再把这个类名指定在组件的 itemRenderer 或 headerRenderer 属性里。

对于 OLAPDataGrid 组件而言,虽然扩展了 AdvancedDataGrid 的功能,但最主要目的是为了支持 OLAP 查询结果的显示,也就是说,数据是由 OLAPCube 决定的。这就决定了其渲染器(OLAPDataGrid**Renderer)行为与普通组件的渲染器相比有着诸多限制,如不允许列的拖拽,不允许在表格上进行修改操作,不允许对列数据进行排序。而对于 OLAP 应用最重要的上钻与下钻操作,OLAPDataGrid**Renderer 亦不支持,我们需要继承 AdvancedDataGrid**Renderer 来完成此功能。

  1. 首先在 mxml 文件中给出 OLAPDataGrid 的初始定义。
    清单 9. OLAPDataGrid 定义
     <mx:Canvas width="100%" height="25%"> 
      <mx:OLAPDataGrid id="myOLAPDG" width="100%" height="100%" 
      initialize="configureGrid()"> 
      </mx:OLAPDataGrid> 
     </mx:Canvas>
  2. 通过点击标题栏的方式进行数据的钻取,并使用 header 类型的自定义渲染器来处理上钻,下钻行为。(当然你还可以自定义的添加其他类型的渲染器,如 item 和 groupItem)
    清单 10. configureGrid
     public function configureGrid():void { 
        myOLAPDG.headerRenderer = new ClassFactory(DAXHeadItemReader); 
     }
  3. 接着覆写主 header 以及其 subheader(以递归的方式),并以回调函数的方式处理各种鼠标事件。
    清单 10. 父 header 的覆写函数
     protected override function createChildren():void { 
        super.createChildren(); 
    
        label.addEventListener(MouseEvent.CLICK, headerLabelClicked); 
        label.addEventListener(MouseEvent.MOUSE_OVER, headerLabelMouseOver); 
        label.addEventListener(MouseEvent.MOUSE_OUT, headerLabelMouseOut); 
    
        if (data != null && data.hasOwnProperty("children")) { 
            var children:Array = this.data.children; 
            var child:AdvancedDataGridColumn; 
            for (var i:int = 0; i < children.length; i++) { 
                child = children[i]; 
                child.rendererIsEditor = true; 
                child.headerRenderer = new ClassFactory(DAXSubHeadRender);
            } 
        } 
     }
    清单 11. 上钻回调函数
     private function headerLabelClicked(mouseEvent:MouseEvent):void { 
        var textField:String = mouseEvent.target.getRawText(); 
    
        if (textField == 'Year Level') 
            Alert.show("It's the top now! Drill up unavailable!"); 
        else { 
            // 模拟本应通过 HttpService 与服务端通信返回的数据
            var newXML:XML = …… ; 
            dispatchEvent(new DrillEvent(DrillEvent.DRILL_UP, newXML, true, false)); 
        } 
     }

    Subheader 的构建方式与下钻回调函数与此类似,具体详见附件中代码。

    这样就把一个 Flex 中的标准 OLAPDataGrid 组件用自定义的渲染器进行了扩展。

  4. 接下来我们再来看看具体的钻取逻辑实现。我们可以根据多种方式来判断钻取操作是上钻还是下钻:例如根据点击组件的 textField,或是从后端返回的数据中某字段的值。当上钻或下钻操作无效时,及时地提醒用户。
    清单 12. 下钻回调函数
     private function subheaderLabelMouseClick(mouseEvent:MouseEvent):void { 
        var textField:String = mouseEvent.target.getRawText(); 
    
        if (textField == 'Day Level') 
            Alert.show("It's the bottom now! Drill down unavailable!"); 
        else { 
            // 模拟本应通过 HttpService 与服务端通信返回的数据
            var newXML:XML; 
            if (textField == 'Year Level') 
                newXML = …… ; 
            else if (textField == 'Quarter Level') 
                newXML = …… ; 
            else if (textField == 'Month Level') 
                newXML = …… ; 
    
            dispatchEvent(new DrillEvent(DrillEvent.DRILL_DOWN, newXML, true, false)); 
     }

    最后看一看具体显示的效果。

  5. 点击子 Header,鼠标移过时的特效在 headerLabelMouseOver() 和 headerLabelMouseOut() 方法中定义 ,如图 5 所示。(可与图 4 不进行任何扩展的 OLAPDataGrid 行为进行比较)
    图 5. 下钻操作 1
    图 5. 下钻操作 1
    图 5. 下钻操作 1

    这时候若点击即完成了下钻操作,将显示季度这个度量的各种数据。如图 6 所示。

    图 6. 下钻操作 2
    图 6. 下钻操作 2
    图 6. 下钻操作 2

    继续点击,数据将会越来越细化,在月之后,最后显示天的数据。而这时若仍进行下钻,则会出现提示用户:下钻操作无法执行。如图 7 所示。

    图 7. 非法下钻操作
    图 7. 非法下钻操作
    图 7. 非法下钻操作
  6. 点击父 Header,则将一步步进行上钻操作,如图 7 的“Day Level”和图 6 的“Quarter Level”。而若在图 5 的基础上点击“Year Level”,也将提示用户:上钻操作无法执行,如图 8 所示。
    图 8. 非法上钻操作
    图 8. 非法上钻操作
    图 8. 非法上钻操作

总结

本篇文章的例子使用 Flex 技术完整地构建了一个纯 OLAP 客户端,而在实际的应用中,OLAP 客户端通常与后台有着大量的数据交互(在例子为了直观阐述,均将上钻下钻请求及数据返回使用硬编码的方式进行模拟),例如我们可以使用 IBM Cubing Service 作为后端的 OLAP 数据处理引擎。

对 IBM Cubing Service 感兴趣的读者,可以移步 http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/index.jsp?topic=/com.ibm.dwe.cubeserv.doc/topics/c_cubingconcepts.html


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=478106
ArticleTitle=使用 Flex3 开发 OLAP 应用
publish-date=03292010