65.9K
CodeProject 正在变化。 阅读更多。
Home

分析服务 MDX 查询查看器 Web 客户端

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020年1月5日

CPOL

7分钟阅读

viewsIcon

7582

downloadIcon

185

客户端和一个 WebAPI,用于对 SSAS 执行 MDX 查询,并在一个透视网格中显示结果单元集,其中轴以分层和分组的方式整齐排列。

引言

我们希望能够执行 MDX 查询,就像我们在 SQL Server Management Studio 中所做的那样,但在浏览器中,并且无需直接访问 SSAS。我们希望将结果单元集显示在一个赏心悦目的网格中,其中选定的行和列被很好地组合在一起。

源代码

源代码可在 GitHub 上找到:https://github.com/goatdata/mdxviewer,如果您有任何改进或错误修复,请随时提交 pull request。

背景

将 CellSet Adodm MDX 查询结果集序列化为 JSON 的基础上,本文将创建一个 Angular 8+ CLI、TypeScript 客户端应用程序,该应用程序将 MDX 查询发送到在 .Net 4.7 中创建的 WebAPI,WebAPI 又对 SSAS(SQL Server Analysis Service)执行 MDX 查询,将结果单元集转换为 JSON,并将其返回给客户端。客户端应用程序随后在一个网格中呈现单元集数据,其中包含现代、美观和功能齐全的网格所应具备的所有功能。

我们希望它快速、美观且对最终用户有意义。因此,我们将锦上添花,添加 MDX 代码高亮文本编辑器(Ace Editor)和现成的顶级网格(agGrid Community)。

拓扑

这里有三个部分在发挥作用——自然是 Web 客户端,它只是一堆在浏览器中运行的 JavaScript。服务器端是 WebAPI 控制器和 SQL Server Analysis Service。

  1. 用户使用我们漂亮的 Ace Editor 输入 MDX 查询
  2. 按下执行按钮后,Viewer/Grid 组件将 MDX 文本传递给 Angular mdxapi 服务。它的作用仅仅是向 WebAPI 发送请求,别无他求。
  3. WebAPI 有一个名为 MDX Controller 的控制器,其中有一个 Query 方法,我们将 MDX 查询文本作为 Post payload 发送给它
  4. Query 方法连接到 SSAS 并将我们的 MDX 查询作为 CellSet 执行
  5. 在读取单元集时,我们将其转换为 JSON
  6. 读取完成后,我们将 JSON 作为字符串响应返回给客户端
  7. 在这里我们需要做一些魔术。agGrid 需要特定格式的数据,因此我们必须将其组织成适合 agGrid 的格式。我们简单地将整个 JSON 馈送给 CellSet.ts 类,该类反过来进行一些内部魔术,并允许我们将 agGrid 直接绑定到 AgGridData(),将列定义绑定到 AgGridColDefs()。*尽管我们实际上为此目的分配了变量,以防止在消化过程中重新解析单元集。*
  8. 就是这样

代码演练

服务器端 WebAPI

让我们先看看服务器端代码。我们不打算深入探讨 WebAPI 项目结构,它是一个非常标准的结构。我们有一个 MDXController 控制器,其中有一个简单的 HttpPost 操作 Query。我们需要它是一个 POST,因为我们的 payload 将作为 JSON 对象在主体中传入。

[HttpPost]
[ActionName("Query")]
public HttpResponseMessage Query(MdxQuery MdxQuery)
{
    try
    {
       string output = "";
       using (AdomdConnection conn = new AdomdConnection(Helpers.SettingHelper.ConnectionString()))
       {
           conn.Open();
           AdomdCommand cmd = new AdomdCommand(MdxQuery.Query, conn);
           CellSet cs = cmd.ExecuteCellSet();
           output = Helpers.CellSetToJson.MdxToJson.MdxToJsonConvert(cs, false);
           conn.Close();
       }
       var response = this.Request.CreateResponse(HttpStatusCode.OK);
       response.Content = new StringContent(output, System.Text.Encoding.UTF8, "application/json");
       return response;
    }
    catch (Exception ex) {
        return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex.Message);
    }
}

这里没什么特别的,我们只是打开与 SSAS 的 AdodmConnection,使用我们的 MDX 创建一个命令并执行 CellSet。值得一提的一点是,我们确实需要 CellSet 结果才能正确地向用户呈现结果。另一个有趣的观察是 MdxToJson 类,它有一个非常有用的方法(是的,是的,它是静态的,我知道……随便啦),这个方法只是读取单元集数据并将其转换为 JSON。它在我的上一篇文章将 CellSet Adodm MDX 查询结果集序列化为 JSON 中有描述,但一个显著的区别是我们如何序列化 Tuple 的 Members 属性。为此我们有一个专用的 Member **MemberJsonConverter**。原因是 Member 类的 Parent 属性。如果我们查看 Microsoft.AnalysisServices.AdomdClient 中 CellSet 的 **Member.Parent** 方法,我们将看到以下内容

 public Member Parent
    {
      get
      {
        if ((Member) null == this.parent)
        {
          if (this.baseData.CubeName == null)
            throw new NotSupportedException(SR.NotSupportedByProvider);
          if (this.Connection == null)
            throw new NotSupportedException(SR.NotSupportedWhenConnectionMissing);
          AdomdUtils.CheckConnectionOpened(this.Connection);
          ListDictionary restrictions = new ListDictionary();
          restrictions.Add((object) CubeCollectionInternal.cubeNameRest, (object) this.baseData.CubeName);
          AdomdUtils.AddCubeSourceRestrictionIfApplicable(this.Connection, restrictions);
          restrictions.Add((object) "MEMBER_UNIQUE_NAME", (object) this.UniqueName);
          restrictions.Add((object) "TREE_OP", (object) 4);
          DataRowCollection rows = AdomdUtils.GetRows(this.Connection, "MDSCHEMA_MEMBERS", restrictions);
          if (rows.Count > 0)
            this.parent = new Member(this.Connection, rows[0], (Level) null, (Member) null, MemberOrigin.Metadata, this.baseData.CubeName, (Tuple) null, -1, this.baseData.Catalog, this.baseData.SessionID);
        }
        return this.parent;
      }
    }

基本上,SSAS 不返回维度或层次结构成员的父级,而是每次访问时都会单独从服务器请求。如果您有 10 或 20 个成员,这还算可以,但是当您有无数个成员时,它会像服用了百忧解的考拉一样慢。相反,我们可以在每个轴的实际 MDX 中请求 **DIMENSION PROPERTIES PARENT_UNIQUE_NAME**,并从成员属性中读取 PARENT_UNIQUE_NAME。哦,是的,我们首先确实需要成员父级,因为这就是我们在网格视图中在 X 轴上构建维度成员的分层布局的方式。

客户端

客户端是 Angular CLI 项目版本 8+。同样,我们不要深入探讨项目结构,因为它是一个非常标准的实现,相反,我们将专注于关键组件,例如如何将 JSON 从单元集转换为适合网格的格式。在我们的案例中,我们使用 agGrid Community。旁注,agGrid 超级强大,速度非常快且美观,Community 版本为我们提供了良好的基础,但对于商业实现,我建议升级到 agGrid Enterprise,因为它解锁了一些很酷的内置透视、自定义排序和单元格格式功能,这些功能非常方便。

CellSet Json 到 agGrid

首先我们需要列定义。MDX 查询将轴的所有成员作为列表的列表(对于那些今年八月将庆祝 Windows 95 25 周年的人来说,是数组的数组)返回。

层次结构本身并未得到特别好的呈现。在上面的示例中,英国是欧洲的成员——一个销售区域维度,它在逻辑上作为元组序数直接跟随其父级出现。我们希望在 UI 上以网格形式向用户表示这种关系

我们在 app/models/CellSet 中创建了一个类,用于方便 CellSet 转换为不同的视图,特别是我们希望提取适用于 agGrid 的列定义。

export class CellSet{    

    public Axes :Axis[];
    public Cells:Array<Array<string>>;

    constructor(private fromRaw: any){
        Object.assign(this,fromRaw);
    }
    
    public AgGridColDefs(): any[]{
        // logic to conver Axis[0]  Tuples and it's Members to agGrid colDefinitions
    }
    
    public AgGridData() : any[]{
       // logic to conver Axis[1] Tuples and it's Members to cell data - more on that later...
    }
}

一旦我们使用从 WebAPI 返回的原始 JSON 实例化了 CellSet 类,我们就会调用 AgGridColDefs() 来执行魔法。简而言之,在这里我们计算出每个维度所有层次结构中的最大级别数,然后对于每个维度成员,我们简单地迭代同一级别上的所有成员,将它们放入层次结构中(我们为此有一个特定类 AxisMember,它只是为了强类型一个成员,让生活更轻松)。此方法的具体逻辑有点超出了本文的范围,如果您想了解它是如何实现的,请逐步调试它——调试会启发您。

好的,列定义简单明了,agGrid 最终内置了列分组,我们在此案例中使用了它。那么 Axis1(Y 轴)行标题我们该怎么办呢,再次看看我们从 MDX 中得到的结果

看,例如,澳元在每一行都重复出现,这很丑。此外,自行车架实际上是配件的成员。无论如何,agGrid 没有行标题,至少社区版本没有。我们需要作弊。我们将像处理列那样,弄清楚哪个成员属于哪个,并根据它在层次结构中的级别来填充单元格值。对于 agGrid 来说,它看起来就像另一个单元格值,我们最终得到类似这样的结果

我们在 CellSet 类中有一个 AgGridData() 方法,它负责处理这类事情,同样,具体的逻辑有点超出本文的范围——如果您对此感兴趣,可以调试它。

一旦我们将原始 CellSet 数据转换为 agGrid 可用的数据,数据就会绑定到网格。这发生在 app/viewer/pivotalgrid/pivotalgrid.component.ts 的 Execute 方法中。

  Execute(){
    this.IsExecuting=true;    
    this.mdxapi.Query(this.query).then(r=>{
      this.cellSet  = new CellSet(r as any);
      this.columnDefs = this.cellSet.AgGridColDefs();
      this.rowData = this.cellSet.AgGridData(); 
      this.IsExecuting=false;
      this.gridApi.hideOverlay();
    }).catch(ex=>{
      this.IsExecuting=false;
      this.gridApi.hideOverlay();
      throw ex;
    });

    this.gridApi.showLoadingOverlay();
  }

接下来是什么

  • 显然,在 WebAPI 上进行一些身份验证/授权是一个好主意。我的下一篇文章将是关于 .Net WebAPI 中的通用会话管理和身份验证,我将把这个项目联系起来。
  • 我经常面临长时间运行的查询导致 SSAS 缓慢甚至崩溃的问题。在当前的实现中,执行按钮会变成 X,如果我们可以取消查询并完全杀死 SSAS 上的 spid,那就太酷了。
  • Ace Editor 具有注入智能感知的非常巧妙的功能。在当前实现中,我修改了 Ace Editor 的定义文件(src\assets\EditorTheme\mode-mdx.js 和 src\assets\EditorTheme\theme-mdx.js)以高亮显示 MDX 函数。如果我们可以拥有某种类似 SQL Server Management Studio 中的对象浏览器,以及用于成员属性路径和函数的智能感知,那就太好了。

 

 

© . All rights reserved.