此内容由人工智能翻译,尚未经过人工编辑审核。图像和图表保持其原始语言。
要点速览
- Tabular Editor 2 的 CLI 常用于 CI/CD 管道(例如 Azure DevOps、GitHub Actions)中,用于自动化执行最佳实践检查、架构校验或部署模型等任务。
- TE2 CLI 可用于执行 C# Script:既可以一次性执行,也可以作为部署流程的一部分执行。这为自动化管道提供了很高的灵活性:只需做少量改动,就能校验或部署表格模型元数据。
- 在现代表格模型特性方面,TE2 CLI 存在一些限制;但它支持执行自定义 C# Script,让你可以绕开其中不少约束。
- 你可以通过将 M 表达式转换为旧版 SQL 分区来编写架构检查脚本,使内置的架构检查器能够检测不匹配。
- 你也可以编写脚本,动态更新数据源引用或替换服务器名称,确保模型在部署时指向正确的环境;还可以编写脚本来更改角色成员。
Tabular Editor 2 的 CLI 常用于 CI/CD 管道(Azure DevOps、GitHub Actions 等)中,以执行某些与表格模型相关的操作,例如运行最佳实践分析、执行架构检查,或将模型部署到目标服务器。 这个 CLI 设计于 2016 年,当时 Power BI 甚至 Azure Analysis Services 都还未普及。不幸的是,这意味着在后来引入的表格模型特性方面,尤其是 Power BI 引入的隐式数据源方面,CLI 显得有些过时。 好消息是,TE2 CLI 可以用来执行自定义 C# Script,这让 CLI 变成了一款非常灵活的工具,使我们能够绕开其原生用法中的一些限制。
注意本文中,我们使用术语 tabular model(表格模型,或有时简称为 model)。 在其他文章中,你可能会看到 Dataset、语义模型,甚至 database(当讨论已部署到 Analysis Services 实例或 Power BI Workspace 中的模型时)等其他术语,但它们指的都是同一个东西。 |
本文将介绍可作为 TE2 CLI 一部分执行的 C# Script,用于执行各种检查以及对模型元数据进行修改。 具体包括:
- 架构检查:我们用 C# Script 来绕过原生 TE2 CLI -SCHEMACHECK 选项的限制
- 更新数据源引用:我们用 C# Script 在部署前动态更改数据源的连接设置。
- 添加角色成员:我们用 C# Script 在部署前动态添加安全角色成员。
架构检查
架构检查适合在构建验证过程中执行,例如提交 Pull Request 时,用于确保表格模型中导入的列与源表或视图的架构(列名和数据类型)一致。 如果源端删除或重命名了某一列,但表格模型中对应的列没有同步更新,那么后续刷新模型时很可能会报错。
在 SQL Server 2016 Analysis Services 上,我们对于表分区只有一种选择:旧版(SQL)。 没有 M 查询、Structured数据源、隐式数据源等。 每个表都有一个或多个旧版分区,分区中包含一条 SQL 查询;该查询会针对数据源执行,并返回要进入该分区的数据集。 这意味着,我们可以在 Tabular Editor 中通过非常直接的方式执行架构检查:打开到同一源的连接,执行分区中指定的 SQL 查询(甚至可以设置 CommandBehavior.SchemaOnly 标志来表明我们不关心数据——只关心架构),然后将得到的架构与模型中已导入的列进行对比。
Tabular Editor 2 CLI 也提供了专门的开关用于执行架构检查:-SC / -SCHEMACHECK,可在命令行中完成同样的操作。 默认情况下,架构差异信息会以“每列一行”的形式输出。 使用 -G / -GITHUB 或 -V / -VSTS 开关,你可以让 Tabular Editor 以 GitHub Actions 或 Azure DevOps 管道可识别为警告/错误的方式输出架构差异。
Tabular Editor 2 CLI 中的架构检查,使用 GitHub Actions 风格的日志输出。
遗憾的是,如今大多数 表格模型 并不会再使用带有 SQL 分区的旧版数据源来定义进入模型的数据。 相反,随着 Power BI 的普及,隐式数据源已成为常态,这意味着数据源和查询通过 M 表达式来定义。 这意味着要执行架构检查,就必须以某种方式解析 M 表达式:提取数据源的详细信息及其数据源对象(表/视图),并弄清 M 表达式中指定的各类数据转换会带来什么影响。 而这还没有考虑到:M 语言和 Power BI 支持数百种不同的数据源,而不仅仅是关系型数据库。 这显然远远超出了 Tabular Editor 2 的能力范围——它只是一个开源的业余爱好项目。 相比之下,Tabular Editor 3 的确包含一个 M 解析器,但这对依赖 TE2 CLI 的 CI/CD 管道并没有帮助。
需要说明的是:原生 TE2 CLI 的 -SCHEMACHECK 功能只支持旧版数据源,且分区必须指定 SQL 查询。
C# Script 来解围
不过,如上所述,我们确实有一个变通办法:可以把 C# Script 作为 TE2 CLI 的一部分来执行。 下面我们会用一个 C# Script,将模型中的所有 M 表达式转换为旧版 SQL 分区,而这类分区确实受 TE2 内置架构检查功能支持。
不过,要让该脚本可用,我们需要做出一些假设:
- 单一 SQL 源:表格模型只从单一的 SQL 类型数据源导入数据(例如本地 SQL Server 数据库、Azure SQL Database 或 Synapse SQL Pool)。 理论上,这个脚本也适用于支持 OLE DB 连接的其他关系型数据源,但尚未测试。
- 无转换:模型中的 M 表达式不包含数据转换。 数据会按 1:1 从 SQL 源中的表/视图导入——这也是最佳实践(M 表达式中允许使用筛选器,但任何 M 转换产生的架构必须与表/视图的原始架构一致)。
- 访问与身份验证:运行 TE2 CLI 的机器在网络/环境上能够访问该 SQL 数据源。 另外,由于该脚本将作为自动化管道的一部分执行,必须提供一个具有对 SQL 数据库必要读取权限的服务主体 Service Principal 或 SQL 用户名/密码账户。
脚本分为三部分:
- 创建一个(旧版)数据源,用于保存 Tabular Editor 连接数据源所需的连接信息。
- 遍历所有通过 M 表达式定义数据内容的表(这可能是包含一个或多个 M 分区的表,或带有增量刷新刷新策略的表),并将其分区替换为一个旧版(SQL)分区。
- 执行架构检查。
在第 2 部分中,我们使用正则表达式(regex)从 M 表达式中提取架构以及表/视图名称。 这假设分区上的 M 表达式如下所示——如果上述假设成立,通常就是这种形式:
let
Source = Sql.Databases(Server),
AdventureWorksDW = Source{[Name="AdventureWorksDW"]}[Data],
dbo_Customer = AdventureWorksDW{[Schema="dbo",Item="Customer"]}[Data]
in
dbo_Customer
具体来说,我们用 regex 获取第 4 行中的 Schema 和 Item 值,本例中分别是“dbo”和“Customer”(顺便一提,感谢 ChatGPT 帮我写这个 RegEx!)。
下面是完整脚本。 在脚本顶部为 4 个变量填入你自己的值(server、database、服务主体 application ID 和 secret)后,你可以直接在 Tabular Editor 2 的 UI 中运行脚本,架构检查的结果会显示出来,效果类似于上面的截图。 运行脚本后,按下 CTRL+Z,即可撤销脚本对模型元数据所做的修改。
重要由于脚本会更改模型元数据,运行完脚本后请务必不要保存模型(如果你确实要保存,至少先备份原始模型元数据)。 如果通过 CLI 运行脚本,不要指定 -D 开关(否则会在脚本修改完成后将模型元数据保存/部署)。 我们只希望脚本临时修改模型元数据,以便执行架构检查。 如果你希望在同一个 CI/CD 管道中部署模型,请在单独的一次 TE2 CLI 调用中完成该操作。 |
using System.Text.RegularExpressions;
var server = "te3synapsetest-ondemand.sql.azuresynapse.net";
var database = "AdventureWorksDW";
var applicationId = "e449df3a-957e-40ff-802f-e3fa75e43be1";
var secret = "<your-service-principal-secret-key>";
// Part 1: Create legacy data source
var source = Model.AddDataSource();
source.Provider = "System.Data.OleDb";
source.ConnectionString = string.Format("Provider=MSOLEDBSQL;Data Source={0};Initial Catalog={1};Authentication=ActiveDirectoryServicePrincipal;User ID={2};Password={3}",
server, database, applicationId, secret);
// Part 2: Swap partitions
string mExpression;
foreach(var table in Model.Tables)
{
// Loop through all tables that have at least one M partition, or an M Source Expression:
var partition = table.Partitions.FirstOrDefault() as MPartition;
if (partition != null) mExpression = partition.MExpression;
else if (!string.IsNullOrEmpty(table.SourceExpression)) mExpression = table.SourceExpression;
else continue;
// Extract the schema and name of the source table/view from the M Expression:
string pattern = @"\[Schema=""(?<schema>.*?)"",Item=""(?<item>.*?)""\]";
Match match = Regex.Match(mExpression, pattern);
var sourceObjectName = string.Format("[{0}].[{1}]", match.Groups["schema"], match.Groups["item"]);
// Create a single legacy SQL partition with a "SELECT * FROM <sourceObjectName>" query:
var legacyPartition = table.AddPartition(query: "SELECT * FROM " + sourceObjectName);
legacyPartition.DataSource = source;
// Delete all other partitions:
foreach(var p in table.Partitions.Where(p => p != legacyPartition).ToList()) p.Delete();
}
// Part 3: Perform schema check
SchemaCheck(Model);
如果你更倾向于使用 SQL 用户名/密码进行身份验证,请将脚本的前 12 行替换为以下内容:
using System.Text.RegularExpressions;
var server = "te3synapsetest-ondemand.sql.azuresynapse.net,1433";
var database = "AdventureWorksDW";
var userId = "<your-sql-username>";
var password = "<your-sql-password>";
// Part 1: Create legacy data source
var source = Model.AddDataSource();
source.Provider = "System.Data.SqlClient";
source.ConnectionString = string.Format("Server={0};Initial Catalog={1};Persist Security Info=False;User ID={2};Password={3};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
server, database, userId, password);
...
也可以使用访问令牌进行身份验证:
using System.Text.RegularExpressions;
var server = "te3synapsetest-ondemand.sql.azuresynapse.net";
var database = "AdventureWorksDW";
var accessToken = "<your-access-token>";
// Part 1: Create legacy data source
var source = Model.AddDataSource();
source.Provider = "System.Data.OleDb";
source.ConnectionString = string.Format("Provider=MSOLEDBSQL;Data Source={0};Initial Catalog={1};Access Token={2}",
server, database, accessToken);
...
如果在 CI/CD 管道中运行脚本,你可能希望从环境变量读取连接信息,而不是在脚本中硬编码这些值。 好在,用 C# 实现这一点相当简单:
using System.Text.RegularExpressions;
var server = Environment.GetEnvironmentVariable("server");
var database = Environment.GetEnvironmentVariable("database");
var applicationId = Environment.GetEnvironmentVariable("applicationid");
var secret = Environment.GetEnvironmentVariable("secret");
// Part 1: Create legacy data source
...
要在命令行中执行 C# Script,请使用下面所示的语法。 这里假设你的模型元数据位于“c:\path\to\model.bim”,并且你要执行的脚本位于“c:\path\to\schemacheck.cs”。 注意,你可以在 CLI 中指定多个脚本文件,让它们按顺序依次执行。
start /wait TabularEditor.exe "c:\path\to\model.bim" -S "c:\path\to\schemacheck.cs" -G
start /wait TabularEditor.exe "c:\path\to\model.bim" -S "c:\path\to\schemacheck.cs" -V
注意使用 TE2 CLI 打开 表格模型元数据时,输入文件既可以是如上所示的 model.bim 文件,也可以是 Database.json(Tabular Editor 文件夹结构)、model.tmdl(TMDL 文件夹结构),或是一个包含单个 Dataset 定义的 .pbip 文件夹。 |
从下面的截图可以看到,架构检查是通过脚本执行的,就像指定了 -SC / -SCHEMACHECK 开关一样,但额外的好处是:它也适用于隐式数据源。 同样的技巧也适用于使用 Structured数据源 的模型,前提与上述相同。
更新数据源引用
以自动化方式部署模型时,经常需要更新模型内部的数据源引用。 例如,源代码管理中的模型元数据可能指向开发或测试数据源;而当我们将模型部署到生产环境时,还需要确保模型引用的是生产数据源。
对于传统(provider)数据源,TE2 CLI 提供了通过 CLI 直接更新数据源连接字符串的原生功能。 但对于结构化数据源或隐式数据源来说,这还不够,因为它们不会显式指定连接字符串。
好在,可以用 C# Script 轻松绕过这一限制。 而且这种方法很灵活,因为 C# Script 可以访问完整的 Tabular Object Model。 例如,如果你使用共享的 M 表达式(也称为 参数,在 Power BI Desktop 中)来保存服务器名称或地址,那么修改该 M 表达式只需一行代码:
var server = "te3synapsetest-ondemand.sql.azuresynapse.net";
// This assumes the existence of a Shared M Expression with the name "Server":
Model.Expressions["Server"].Expression = string.Format("\"{0}\" meta [IsParameterQuery=true, Type=\"Text\", IsParameterQueryRequired=true]", server);
或者,如果你的每个 M 分区都引用了某个服务器名称,你可以写一个简短的循环,将它们全部替换:
foreach(var partition in Model.AllPartitions.OfType<MPartition>())
{
partition.Expression = partition.Expression.Replace("\"old-server-name\"", "\"new-server-name\"");
}
再或者,对于传统或 Structured数据源,直接编辑数据源属性即可:
(Model.DataSources["MyDataSource"] as StructuredDataSource).Server = "new-server-address";
与上面相同,我们可以在脚本的任意位置使用 Environment.GetEnvironmentVariable("<environment-variable-name>"),从环境变量读取文本字符串,而不是在脚本里硬编码这些值。
要在模型部署前即时执行脚本,请使用以下命令语法:
start /wait TabularEditor.exe "c:\path\to\model.bim" -S "c:\path\to\script.cs" -D <target-server> <target-database> -O -C
<target-server> 是 SSAS 或 Azure AS 服务器名称,或 Power BI 的 XMLA endpoint。 它也可以是一个完整的 MSOLAP 连接字符串,包括服务主体凭据,这在无人值守执行 CLI 时是必需的。
<target-database> 是要部署的数据库 (Dataset) 名称。 使用 -O 明确表示允许覆盖已存在的数据库。
对于 provider(传统)或 Structured数据源,务必像上面那样在部署命令中指定 -C 开关。 否则,包含(已更新)属性的数据源不会被部署。 查看命令行文档,以更详细地了解各个开关的作用。
添加角色成员
另一个常见需求是:根据模型部署到的环境不同,为模型中的安全角色分配不同的角色成员。 一种做法是在部署模型的命令中不指定 -R / -ROLES 和 -M / -MEMBERS 开关,这样会保持所有角色/角色成员原样不变。 但这要求在部署目标上已经存在一个已部署的数据库,并且其中的角色/角色成员已正确配置。 有时,在部署流水线中分配角色成员会更实用。
同样,用 C# 脚本也很容易实现。 下面示例演示如何将一个 Entra ID 组(以前称为“AD 组”)添加到某个角色:
var roleName = "Reader";
var groupId = "2a73590e-cf03-4db8-a8b3-7e639c0ca4de";
var tenantId = "ddec87fe-da10-4d12-990a-770ca3eb6226";
var memberName = string.Format("obj:{0}@{1}", groupId, tenantId);
Model.Roles[roleName].AddExternalMember(memberName);
要添加某个特定用户,请在脚本中提供其 UserPrincipalName:
var memberName = "daniel@test.com"
Model.Roles[roleName].AddExternalMember(memberName);
当然,这种方式可以按你的需求做多种定制,例如使用环境变量,或从文件、数据库等位置读取用户列表。 发挥空间很大。
通过 CLI 部署并同时运行这个脚本时,记得同时指定 -R 和 -M 开关,确保新添加的角色成员也会被部署。
start /wait TabularEditor.exe "c:\path\to\model.bim" -S "c:\path\to\script.cs" -D <target-server> <target-database> -O -R -M
重要-R 和 -M 开关会更新所有角色及角色成员的元数据,使其与源文件/文件夹结构中的内容保持一致(包括脚本所做的任何修改)。 如果你的目标数据库中已有你希望保留的角色/角色成员,就不应按上面所示方式进行部署。 你可以改为通过 CLI 打开现有模型(将服务器和数据库名称作为前两个参数),用 -S 开关运行脚本,然后使用不带额外参数的 -D 将更改保存回数据库: |
结论
本文介绍了 TE2 CLI 如何用来执行 C# Script:既可以一次性执行,也可以作为部署流程的一部分。 这让你可以非常灵活地搭建自动化流水线,在对表格模型元数据进行小幅修改的同时完成验证或部署——在企业场景中这类需求很常见,例如需要同时面对多个环境等。
希望你喜欢这篇文章! 你可以在下方评论区提问,或提出你希望补充的脚本建议。