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





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