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

使用 DacFx 公共模型和 System.Reflection 参数化 SSDT 数据库项目 .dacpac 文件中的架构名称

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2016年2月26日

CPOL

6分钟阅读

viewsIcon

20979

downloadIcon

217

如何在部署前以编程方式更改数据库项目中的模式名称

引言

Visual Studio 的数据库项目(也称为 SQL Server Data Tools,SSDT)是一个极其有用的数据库开发工具。它允许在项目构建期间进行代码验证、语法和引用完整性检查、数据库版本控制等等。

但是,当然,它也有其局限性。例如,在对象名称中使用变量的用处非常有限。如果您使用项目引用,则可以在 4 部分对象名称中的服务器名称和数据库名称中使用变量。

CREATE VIEW Sales.[FactEvent] AS
SELECT *
FROM
[$(SourceServer)].[$(SalesDatabase)].[dbo].[FactEvent]

但是您不能在架构名称、表名称或视图名称等中使用变量。这总是会产生错误 71502“未解析的对象引用”。

幸运的是,有一种方法可以克服这个限制。在接下来的文章中,我将解释如何编辑 SSDT 创建的数据库包(*.dacpac 文件),如何在其中重命名架构,并将更新后的包部署到 SQL Server。

我们将使用 DacFx 公共模型解析数据库包,并将其内部的数据库模型视为对象层次结构;我们将使用反射来导航和修改此层次结构。

所有代码均用 C# 编写。

背景:DacFx 公共模型

DacFx 公共模型是 Data-tier Application Framework 的 API。Data-tier Application Framework 只是 SSDT 内部使用的对象模型的另一个名称。在此模型中,每个表、视图、存储过程、SELECT 语句等都表示为复杂层次结构中的一个对象。例如,DacFx 公共模型中简单的 SELECT 语句“select * from OldSchema.t1;”在 Visual Studio 调试器中看起来是这样的:

https://codeproject.org.cn/KB/Articles/1080452/DacFx_simple_select_2.png

每个 SQL 语句都会生成不同的对象层次结构。我们需要一种通用的方法来递归地遍历它,查找具有特定架构名称的对象,并对其进行更新。如果层次结构中有多个此类对象(例如,多个表的 JOINUNION),我们需要找到所有这些对象。我们正在寻找的对象可能嵌套在彼此内部:由表创建的视图、执行另一个存储过程的存储过程等。我们需要一种可靠的方法来解决所有这些情况。为此,我们将使用 System.Reflection

背景:System.Reflection

反射是一个 API,它允许程序操作自身的元数据。例如,反射允许我们分析类实例,获取其所有 public 属性和方法的列表,更改特定名称属性的值,调用方法等。

private void DoSomething(dynamic obj)
{
   foreach (var property in obj.GetType().
      GetProperties(BindingFlags.Public | BindingFlags.Instance))
   {
      // Recursively analyse object model to find specific objects
   }
}

使用反射导航 DacFx 模型

我们将使用以下算法来导航 DacFx 模型:

  1. 如果当前对象具有是 DacFx 对象的 public 属性,则递归地为每个属性运行此算法。
  2. 如果当前对象是集合,则对集合中的每个项运行此算法。
  3. 如果当前对象具有架构,并且其名称等于 oldSchema,则将其更新为 newSchema
  4. 如果当前对象是硬编码文本(例如“IF OBJECT_ID (N'OldSchema.ufn_SalesByStore', N'IF') IS NOT NULL”)
    • 将文本分割为以点分隔的单词。
    • 对于每个单词,如果它等于 oldSchema,则将其更新为 newSchema
var propertyValue = index == -1 ? 
property.GetValue(obj) : property.GetValue(obj, new object[] { index });
//If we have a collection, then iterate through its elements.
bool isCollection = false;
if (property.PropertyType.GetInterface("System.Collections.IEnumerable") != null)
   isCollection = true;
string currentPropertyName = property.Name;
string currentPropertyType = property.PropertyType.Name;
string currentPropertyTypeNamespace = property.PropertyType.Namespace;
if (currentPropertyTypeNamespace == propertyTypeNamespace || 
	currentPropertyType == "String" || isCollection)
{
   if (isCollection && !(propertyValue is string) && propertyValue != null)
      foreach (var item in propertyValue)
         Set(item, oldValue, newValue, propertyTypeNamespace,
               propertyHierarchy + "." + currentPropertyName);
   //If field name and type matches, then set.
   else
      if (currentPropertyName == "Value" && 
      currentPropertyType == "String" && propertyValue == oldValue
            && (propertyHierarchy.EndsWith("MultiPartIdentifier.Item")
            || propertyHierarchy.EndsWith("SchemaIdentifier")
            || propertyHierarchy.EndsWith("CreateSchemaStatement.Name")))
         property.SetValue(obj, newValue);
      else
         if (currentPropertyName == "Value" && currentPropertyType == "String"
               && propertyHierarchy.EndsWith("Predicate.Expression.Parameters"))
            ProcessStringLiteral(propertyValue, property, obj, oldValue, newValue);
         else
            if (currentPropertyTypeNamespace == propertyTypeNamespace)
               Set(propertyValue, oldValue, newValue, propertyTypeNamespace, 
               propertyHierarchy + "." + currentPropertyName);
}

为什么 Visual Studio Deployment Contributor 不起作用

我最初的想法是创建一个 Visual Studio Deployment Contributor – 一个自定义的 VS 插件,用于补充部署过程。当 SSDT 生成部署脚本时,Deployment Contributor 可以在脚本执行于服务器之前对其进行修改。可以使用相同的方法(DacFx + Reflection)来解析和修改脚本,更改架构名称。

但是这种方法存在一个关键问题。部署脚本基于源 SSDT 数据库模型和目标数据库模型之间的比较。此比较发生在部署之前,因此会使用未更改的架构名称进行比较。如果目标模型和源模型已具有表 OldSchema.t1 的相同定义,那么此表将根本不会包含在部署脚本中,因此也不会创建表 NewSchema.t1。另一方面,如果目标中不存在表 OldSchema.t1,但存在表 NewSchema.t1,则部署尝试将因错误“指定的对象已存在”而失败。

因此,我们需要在部署和模型比较之前修改源模型。我尝试使用 Visual Studio Build Contributor 来做到这一点。与 Deployment Contributor 类似,它补充了项目构建过程。不幸的是,Build Contributor 对 SSDT 数据库模型具有只读访问权限。它无法修改模型 – 只能收集有关模型的信息,例如用于自定义日志记录。

因此,在部署之前修改数据库模型的唯一方法是编辑数据库包文件(*.dacpac),并在 Visual Studio 部署周期之外完全进行。然后可以使用 SqlPackage.exe 命令行实用程序将修改后的包部署到 SQL Server。

解析数据库包文件

.dacpac 文件加载模型非常容易:

TSqlModel modelFromDacpac = TSqlModel.LoadFromDacpac(fileName,
          new ModelLoadOptions(DacSchemaModelStorageType.Memory, loadAsScriptBackedModel: true));

在此处指定选项 loadAsScriptBackedModel: true 很重要。指定此选项后,模型中的每个对象都会加载一个自动生成的 SQL 脚本。方法 TsqlModel.DeleteObjects 以 SQL 脚本名称作为参数;因此,没有脚本的对象无法从模型中删除。

模型加载后,我们可以获取其中用户定义对象的列表。在将每个对象分析为类层次结构之前,我们需要将其从 TSqlObject 转换为 TSqlStatement。没有原生优雅的方法可以做到这一点,所以我们必须使用一个技巧:从 TsqlObject 中提取 SQL 脚本,并在该脚本之上构建 TsqlStatement。为了更新模型,我们将进行反向转换。

可能会有诱惑,只需在此步骤中使用 string.Replace() 等函数更新 SQL 脚本,而不使用 DacFx 类层次结构。但是,如果这样做,我们可能会面临很多意外的错误:某些对象名称可能包含架构名称作为子字符串;某些对象名称可能与架构名称重叠等等。如果我们使用对象模型,我们将能够正确处理所有这些情况。

List<TSqlObject> sourceModelObjects = new List<TSqlObject>();
foreach (TSqlObject tso in model.GetObjects(DacQueryScopes.UserDefined))
    sourceModelObjects.Add(tso);
for (int i = 0; i < sourceModelObjects.Count; i++)
{
    Console.WriteLine(DateTime.Now.ToString("T") + 
    " Processing object " + (i + 1).ToString() + " of " + 
       sourceModelObjects.Count.ToString() + ": " + 
       sourceModelObjects[i].ObjectType.Name + " " + sourceModelObjects[i].Name.ToString());
    SourceInformation sourceInfo = sourceModelObjects[i].GetSourceInformation();
    string oldObjectScript = string.Empty;
    sourceModelObjects[i].TryGetScript(out oldObjectScript);
    string newObjectScript = string.Empty;
    try
    {
        TSql120Parser parser = new TSql120Parser(false);
        IList<ParseError> errors;
        TSqlScript fragment = (TSqlScript)parser.Parse(new StringReader(oldObjectScript), out errors);
        TSqlStatement stmt = fragment.Batches[0].Statements[0];
        ReflectionUtils.SubstituteSchemaName(stmt, oldSchema,
                newSchema, "Microsoft.SqlServer.TransactSql.ScriptDom");
        Sql120ScriptGenerator sg = new Sql120ScriptGenerator();
        sg.GenerateScript(fragment, out newObjectScript);
    }
    catch (Exception ex) { }
    if (oldObjectScript != newObjectScript && newObjectScript != string.Empty)
    {
        model.DeleteObjects(sourceInfo.SourceName);
        model.AddObjects(newObjectScript);
    }
}

将更新后的模型保存到文件几乎和加载它一样简单。

using (DacPackage dacPackage = DacPackage.Load(fileName,
                DacSchemaModelStorageType.Memory,
                FileAccess.ReadWrite))
   {
      DacPackageExtensions.UpdateModel(dacPackage, modelFromDacpac, null);
   }

使用 SqlPackage.exe 实用程序部署修改后的 .dacpac 文件

SqlPackage.exe 命令行实用程序是 SQL Server 安装的一部分。它可以在 %Program Files (x86)\Microsoft SQL Server\120\DAC\bin\ 中找到(取决于 SQL Server 的版本)。为了实现部署自动化,我使用了 PowerShell 脚本来修改 .dacpac,然后进行部署。

$SqlPackageLocation = [string] ($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq "SqlPackageLocation" }).value
$DacpacFile = [string] ($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq " DacpacFile" }).value
$Profile = [string] ($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq "Profile" }).value
$SchemaSubstitute = [System.Convert]::ToBoolean
(($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq "SchemaSubstitute" }).value)
$SchemaSubstituteCmdLocation = [string] ($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq "SchemaSubstituteCmdLocation" }).value
$SchemaSubstituteParams = """$DacpacFile"""
foreach ($nodeXML1 in ($Settings.DeploymentSettings.Setting | ? 
{ $_.name -eq "SchemaSubstitutePairs" }).SchemaSubstitutePair)
{
    $SchemaSubstituteParams = $SchemaSubstituteParams + " " + 
    $nodeXML1.OldSchema + " " + $nodeXML1.NewSchema
}
if ($SchemaSubstitute) {
   write-host "Substituting schema with parameters " $SchemaSubstituteParams
   & $SchemaSubstituteCmdLocation $SchemaSubstituteParams
}
echo "Deploy"
& $SqlPackageLocation /Action:Publish /SourceFile:$DacpacFile /Profile:$Profile

此脚本使用的配置文件如下所示:

<DeploymentSettings>
  <Setting name="SqlPackageLocation" 
  value="C:\Program Files (x86)\Microsoft SQL Server\120\DAC\bin\SqlPackage.exe"/>
  <Setting name="DacpacFile" value="Sample.Database.dacpac"/>
  <Setting name="Profile" value="Sample.Database.publish.xml"/>
  <Setting name="SchemaSubstitute" value="True" />
  <Setting name="SchemaSubstituteCmdLocation" value="SchemaSubstituteCmd.exe" />
  <Setting name="SchemaSubstitutePairs">
         <SchemaSubstitutePair OldSchema="dbo" NewSchema="TestSchema" />
  </Setting>
</DeploymentSettings>

结论

此应用程序展示了如何更新 SSDT 创建的数据库包的模型。除了参数化架构名称之外,这项技术还有许多其他潜在的用例:更改表名和列名,动态添加新对象(如索引或默认约束)等。

致谢

© . All rights reserved.