演练:将数据库项目构建扩展为生成模型统计信息

可以在生成数据库项目时创建一个构建贡献者,以执行自定义操作。 在本演练中,你将创建一个名为 ModelStatistics 的生成参与者,用于在生成数据库项目时从 SQL 数据库模型输出统计信息。 由于此生成参与者在生成时采用参数,因此需要执行一些额外的步骤。

在本演练中,你将完成以下主要任务:

先决条件

你需要满足以下条件才能完成本演练:

  • 必须已安装包含 SQL Server Data Tools (SSDT) 的 Visual Studio 版本,并支持 C# 或 Visual Basic (VB) 开发。

  • 必须具有包含 SQL 对象的 SQL 项目。

注释

本演练适用于已熟悉 SSDT 的 SQL 功能的用户。 你还希望熟悉基本的 Visual Studio 概念,例如如何创建类库,以及如何使用代码编辑器将代码添加到类。

生成参与者背景

生成参与者是在生成项目期间运行,在生成表示项目的模型之后,但在将项目保存到磁盘之前。 它们可用于多种方案,例如:

  • 验证模型内容并向调用方报告验证错误。 这可以通过将错误添加到作为参数传递给 OnExecute 方法的列表来完成。

  • 生成模型统计信息并向用户报告。 这是此处所示的示例。

生成参与者的主要入口点是 OnExecute 方法。 继承自 BuildContributor 的所有类都必须实现此方法。 BuildContributorContext 对象传递给此方法 - 其中包含生成的所有相关数据,例如数据库模型、生成属性和生成参与者要使用的参数/文件。

TSqlModel 和数据库模型 API

最有用的对象是数据库模型,由 TSqlModel 对象表示。 这是数据库的逻辑表示形式,包括所有表、视图和其他元素,以及它们之间的关系。 有一个强类型架构,可用于查询特定类型的元素并遍历有趣的关系。 你将看到演练代码中如何使用此功能的示例。

下面是本演练中示例参与者使用的一些命令:

Class 方法或属性 Description
TSqlModel GetObjects() 查询对象的模型,并且是模型 API 的主要入口点。 只有顶级类型(如表或视图)可以查询 - 只能通过遍历模型找到列等类型。 如果未指定 ModelTypeClass 筛选器,则返回所有顶级类型。
TSqlObject GetReferencedRelationshipInstances() 查找与当前 TSqlObject 引用的元素的关系。 例如,对于表,这将返回表的列等对象。 在这种情况下,ModelRelationshipClass 筛选器可用于指定查询的确切关系(例如使用 Table.Columns 筛选器可确保只返回列)。

有多种类似的方法,例如 GetReferencingRelationshipInstances、GetChildren 和 GetParent。 有关详细信息,请参阅 API 文档。

唯一标识参与者

在构建过程中,自定义参与者将从标准扩展目录加载。 生成参与者由 ExportBuildContributor 属性标识。 此属性是必需的,以便可以发现参与者。 此属性应类似于以下代码:

[ExportBuildContributor("ExampleContributors.ModelStatistics", "1.0.0.0")]

在这种情况下,属性的第一个参数应该是唯一标识符 - 用于标识项目文件中的参与者。 最佳做法是将库的命名空间(在本演练中,“ExampleContributors”)与类名(在本演练中为“ModelStatistics”)合并以生成标识符。 你可以看到如何使用此命名空间来指定贡献者应在演练步骤的后面部分运行。

创建构建贡献者

若要创建构建贡献者,必须执行以下步骤:

  • 创建类库项目并添加所需的引用。

  • 定义从 BuildContributor 继承的名为 ModelStatistics 的类。

  • 重写 OnExecute 方法。

  • 添加一些专用帮助程序方法。

  • 构建生成的程序集。

创建类库项目

  1. 创建名为 MyBuildContributor 的 Visual Basic 或 C# 类库项目。

  2. 将文件“Class1.cs”重命名为“ModelStatistics.cs”。

  3. 在解决方案资源管理器中,右键单击项目节点,然后选择“ 添加引用”。

  4. 选择 System.ComponentModel.Composition 条目,然后选择“ 确定”。

  5. 添加所需的 SQL 引用:右键单击项目节点,然后选择 “添加引用”。 选择 “浏览 ”按钮。 导航到 C:\Program Files (x86)\Microsoft SQL Server\110\DAC\Bin 文件夹。 选择 Microsoft.SqlServer.Dac.dllMicrosoft.SqlServer.Dac.Extensions.dllMicrosoft.Data.Tools.Schema.Sql.dll 条目,然后选择“ 确定”。

    接下来,开始向类添加代码。

定义 ModelStatistics 类

  1. ModelStatistics 类处理传递给 OnExecute 方法的数据库模型,并生成和 XML 报告,详细说明模型的内容。

    在代码编辑器中,更新ModelStatistics.cs文件以匹配以下代码:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Xml.Linq;
    using Microsoft.Data.Schema;
    using Microsoft.Data.Schema.Build;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.Sql;
    
    namespace ExampleContributors
    {
    /// <summary>
        /// A BuildContributor that generates statistics about a model and saves this to the output directory.
        /// Only runs if a "GenerateModelStatistics=true" contributor argument is set in the project file, or a targets file.
        /// Statistics can be sorted by "none, "name" or "value", with "none" being the default sort behavior.
        ///
        /// To set contributor arguments in a project file, add:
        ///
        /// <PropertyGroup>
        ///     <ContributorArguments Condition="'$(Configuration)' == 'Debug'">
        /// $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy="name";
        ///     </ContributorArguments>
        /// <PropertyGroup>
        ///
        /// This generates model statistics when building in Debug mode only - remove the condition to generate in all build modes.
        /// </summary>
        [ExportBuildContributor("ExampleContributors.ModelStatistics", "1.0.0.0")]
        public class ModelStatistics : BuildContributor
        {
            public const string GenerateModelStatistics = "ModelStatistics.GenerateModelStatistics";
            public const string SortModelStatisticsBy = "ModelStatistics.SortModelStatisticsBy";
            public const string OutDir = "ModelStatistics.OutDir";
            public const string ModelStatisticsFilename = "ModelStatistics.xml";
            private enum SortBy { None, Name, Value };
            private static Dictionary<string, SortBy> SortByMap = new Dictionary<string, SortBy>(StringComparer.OrdinalIgnoreCase)
            {
                { "none", SortBy.None },
                { "name", SortBy.Name },
                { "value", SortBy.Value },
            };
    
            private SortBy _sortBy = SortBy.None;
    
            /// <summary>
            /// Override the OnExecute method to perform actions when you build a database project.
            /// </summary>
            protected override void OnExecute(BuildContributorContext context, IList<ExtensibilityError> errors)
            {
                // handle related arguments, passed in as part of
                // the context information.
                bool generateModelStatistics;
                ParseArguments(context.Arguments, errors, out generateModelStatistics);
    
                // Only generate statistics if requested to do so
                if (generateModelStatistics)
                {
                    // First, output model-wide information, such
                    // as the type of database schema provider (DSP)
                    // and the collation.
                    StringBuilder statisticsMsg = new StringBuilder();
                    statisticsMsg.AppendLine(" ")
                                 .AppendLine("Model Statistics:")
                                 .AppendLine("===")
                                 .AppendLine(" ");
                    errors.Add(new ExtensibilityError(statisticsMsg.ToString(), Severity.Message));
    
                    var model = context.Model;
    
                    // Start building up the XML that is serialized later
                    var xRoot = new XElement("ModelStatistics");
    
                    SummarizeModelInfo(model, xRoot, errors);
    
                    // First, count the elements that are contained
                    // in this model.
                    IList<TSqlObject> elements = model.GetObjects(DacQueryScopes.UserDefined).ToList();
                    Summarize(elements, element => element.ObjectType.Name, "UserDefinedElements", xRoot, errors);
    
                    // Now, count the elements that are defined in
                    // another model. Examples include built-in types,
                    // roles, filegroups, assemblies, and any
                    // referenced objects from another database.
                    elements = model.GetObjects(DacQueryScopes.BuiltIn | DacQueryScopes.SameDatabase | DacQueryScopes.System).ToList();
                    Summarize(elements, element => element.ObjectType.Name, "OtherElements", xRoot, errors);
    
                    // Now, count the number of each type
                    // of relationship in the model.
                    SurveyRelationships(model, xRoot, errors);
    
                    // Determine where the user wants to save
                    // the serialized XML file.
                    string outDir;
                    if (context.Arguments.TryGetValue(OutDir, out outDir) == false)
                    {
                        outDir = ".";
                    }
                    string filePath = Path.Combine(outDir, ModelStatisticsFilename);
                    // Save the XML file and tell the user
                    // where it was saved.
                    xRoot.Save(filePath);
                    ExtensibilityError resultArg = new ExtensibilityError("Result was saved to " + filePath, Severity.Message);
                    errors.Add(resultArg);
                }
            }
    
            /// <summary>
            /// Examine the arguments provided by the user
            /// to determine if model statistics should be generated
            /// and, if so, how the results should be sorted.
            /// </summary>
            private void ParseArguments(IDictionary<string, string> arguments, IList<ExtensibilityError> errors, out bool generateModelStatistics)
            {
                // By default, we don't generate model statistics
                generateModelStatistics = false;
    
                // see if the user provided the GenerateModelStatistics
                // option and if so, what value was it given.
                string valueString;
                arguments.TryGetValue(GenerateModelStatistics, out valueString);
                if (string.IsNullOrWhiteSpace(valueString) == false)
                {
                    if (bool.TryParse(valueString, out generateModelStatistics) == false)
                    {
                        generateModelStatistics = false;
    
                        // The value was not valid from the end user
                        ExtensibilityError invalidArg = new ExtensibilityError(
                            GenerateModelStatistics + "=" + valueString + " was not valid.  It can be true or false", Severity.Error);
                        errors.Add(invalidArg);
                        return;
                    }
                }
    
                // Only worry about sort order if the user requested
                // that we generate model statistics.
                if (generateModelStatistics)
                {
                    // see if the user provided the sort option and
                    // if so, what value was provided.
                    arguments.TryGetValue(SortModelStatisticsBy, out valueString);
                    if (string.IsNullOrWhiteSpace(valueString) == false)
                    {
                        SortBy sortBy;
                        if (SortByMap.TryGetValue(valueString, out sortBy))
                        {
                            _sortBy = sortBy;
                        }
                        else
                        {
                            // The value was not valid from the end user
                            ExtensibilityError invalidArg = new ExtensibilityError(
                                SortModelStatisticsBy + "=" + valueString + " was not valid.  It can be none, name, or value", Severity.Error);
                            errors.Add(invalidArg);
                        }
                    }
                }
            }
    
            /// <summary>
            /// Retrieve the database schema provider for the
            /// model and the collation of that model.
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private static void SummarizeModelInfo(TSqlModel model, XElement xContainer, IList<ExtensibilityError> errors)
            {
                // use a Dictionary to accumulate the information
                // that is later output.
                var info = new Dictionary<string, string>();
    
                // Two things of interest: the database schema
                // provider for the model, and the language id and
                // case sensitivity of the collation of that
                // model
                info.Add("Version", model.Version.ToString());
    
                TSqlObject options = model.GetObjects(DacQueryScopes.UserDefined, DatabaseOptions.TypeClass).FirstOrDefault();
                if (options != null)
                {
                    info.Add("Collation", options.GetProperty<string>(DatabaseOptions.Collation));
                }
    
                // Output the accumulated information and add it to
                // the XML.
                OutputResult("Basic model info", info, xContainer, errors);
            }
    
            /// <summary>
            /// For a provided list of model elements, count the number
            /// of elements for each class name, sorted as specified
            /// by the user.
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private void Summarize<T>(IList<T> set, Func<T, string> groupValue, string category, XElement xContainer, IList<ExtensibilityError> errors)
            { // Use a Dictionary to keep all summarized information
                var statistics = new Dictionary<string, int>();
    
                // For each element in the provided list,
                // count items based on the specified grouping
                var groups =
                    from item in set
                    group item by groupValue(item) into g
                    select new { g.Key, Count = g.Count() };
    
                // order the groups as requested by the user
                if (this._sortBy == SortBy.Name)
                {
                    groups = groups.OrderBy(group => group.Key);
                }
                else if (this._sortBy == SortBy.Value)
                {
                    groups = groups.OrderBy(group => group.Count);
                }
    
                // build the Dictionary of accumulated statistics
                // that is passed along to the OutputResult method.
                foreach (var item in groups)
                {
                    statistics.Add(item.Key, item.Count);
                }
    
                statistics.Add("subtotal", set.Count);
                statistics.Add("total items", groups.Count());
    
                // output the results, and build up the XML
                OutputResult(category, statistics, xContainer, errors);
            }
    
            /// <summary>
            /// Iterate over all model elements, counting the
            /// styles and types for relationships that reference each
            /// element
            /// Results are output to the console and added to the XML
            /// being constructed.
            /// </summary>
            private static void SurveyRelationships(TSqlModel model, XElement xContainer, IList<ExtensibilityError> errors)
            {
                // get a list that contains all elements in the model
                var elements = model.GetObjects(DacQueryScopes.All);
                // We are interested in all relationships that
                // reference each element.
                var entries =
                    from element in elements
                    from entry in element.GetReferencedRelationshipInstances(DacExternalQueryScopes.All)
                    select entry;
    
                // initialize our counting buckets
                var composing = 0;
                var hierachical = 0;
                var peer = 0;
    
                // process each relationship, adding to the
                // appropriate bucket for style and type.
                foreach (var entry in entries)
                {
                    switch (entry.Relationship.Type)
                    {
                        case RelationshipType.Composing:
                            ++composing;
                            break;
                        case RelationshipType.Hierarchical:
                            ++hierachical;
                            break;
                        case RelationshipType.Peer:
                            ++peer;
                            break;
                        default:
                            break;
                    }
                }
    
                // build a dictionary of data to pass along
                // to the OutputResult method.
                var stat = new Dictionary<string, int>
                {
                    {"Composing", composing},
                    {"Hierarchical", hierachical},
                    {"Peer", peer},
                    {"subtotal", entries.Count()}
                };
    
                OutputResult("Relationships", stat, xContainer, errors);
            }
    
            /// <summary>
            /// Performs the actual output for this contributor,
            /// writing the specified set of statistics, and adding any
            /// output information to the XML being constructed.
            /// </summary>
            private static void OutputResult<T>(string category, Dictionary<string, T> statistics, XElement xContainer, IList<ExtensibilityError> errors)
            {
                var maxLen = statistics.Max(stat => stat.Key.Length) + 2;
                var format = string.Format("{{0, {0}}}: {{1}}", maxLen);
    
                StringBuilder resultMessage = new StringBuilder();
                //List<ExtensibilityError> args = new List<ExtensibilityError>();
                resultMessage.AppendLine(category);
                resultMessage.AppendLine("-----------------");
    
                // Remove any blank spaces from the category name
                var xCategory = new XElement(category.Replace(" ", ""));
                xContainer.Add(xCategory);
    
                foreach (var item in statistics)
                {
                    //Console.WriteLine(format, item.Key, item.Value);
                    var entry = string.Format(format, item.Key, item.Value);
                    resultMessage.AppendLine(entry);
                    // Replace any blank spaces in the element key with
                    // underscores.
                    xCategory.Add(new XElement(item.Key.Replace(' ', '_'), item.Value));
                }
                resultMessage.AppendLine(" ");
                errors.Add(new ExtensibilityError(resultMessage.ToString(), Severity.Message));
            }
        }
    }
    

    接下来,生成类库。

对程序集进行签名和生成

  1. “项目 ”菜单上,选择 “MyBuildContributor 属性”。

  2. 选择“ 签名 ”选项卡。

  3. 选择“ 对程序集进行签名”。

  4. “选择强名称密钥文件”中,选择“ <新建>”。

  5. 在“ 创建强名称密钥 ”对话框中的 “密钥文件名”中,键入 MyRefKey

  6. (可选)可以为强名称密钥文件指定密码。

  7. 选择“确定”

  8. 在“文件”菜单上,单击“全部保存”

  9. 在“生成”菜单上,选择“生成解决方案”

    接下来,必须安装程序集,以便在生成 SQL 项目时加载它。

安装构建贡献者

若要安装构建贡献者,必须将程序集和关联 .pdb 文件复制到 Extensions 文件夹。

安装 MyBuildContributor 程序集

  1. 接下来,将程序集信息复制到 Extensions 目录。 Visual Studio 启动时,它会标识目录和子目录中的任何扩展 %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions ,并使它们可供使用。

  2. MyBuildContributor.dll 程序集文件从输出目录复制到 %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions 该目录。

    注释

    默认情况下,已 .dll 编译文件的路径为 YourSolutionPath\YourProjectPath\bin\Debug 或 YourSolutionPath\YourProjectPath\bin\Release。

运行或测试构建过程中的参与者

若要运行或测试构建参与组件,必须执行以下任务:

  • 将属性添加到您计划构建的 .sqlproj 文件。

  • 使用 MSBuild 生成数据库项目并提供相应的参数。

将属性添加到 SQL 项目 (.sqlproj) 文件

必须始终更新 SQL 项目文件,以指定要运行的参与者的 ID。 此外,由于此生成参与者接受 MSBuild 中的命令行参数,因此必须修改 SQL 项目,使用户能够通过 MSBuild 传递这些参数。

可以通过两种方法执行此操作:

  • 可以手动修改 .sqlproj 文件以添加所需的参数。 如果您不打算在大量项目中重复使用构建贡献者,则可以选择这样做。 如果选择此选项,请在文件中第一个导入节点之后将 .sqlproj 以下语句添加到文件

    <PropertyGroup>
        <BuildContributors>
            $(BuildContributors);ExampleContributors.ModelStatistics
        </BuildContributors>
        <ContributorArguments Condition="'$(Configuration)' == 'Debug'">
            $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name;
        </ContributorArguments>
    </PropertyGroup>
    
  • 第二种方法是创建包含所需参与者参数的目标文件。 如果对多个项目使用相同的参与者,因为这会很有用,因为它包含默认值。

    在这种情况下,请在 MSBuild 扩展路径中创建目标文件:

    1. 请导航至 %ProgramFiles%\MSBuild

    2. 创建存储目标文件的新文件夹“MyContributors”。

    3. 在此目录中创建一个新文件“MyContributors.targets”,向其添加以下文本,然后保存该文件:

      <?xml version="1.0" encoding="utf-8"?>
      
      <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
        <PropertyGroup>
          <BuildContributors>$(BuildContributors);ExampleContributors.ModelStatistics</BuildContributors>
          <ContributorArguments Condition="'$(Configuration)' == 'Debug'">$(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name;</ContributorArguments>
        </PropertyGroup>
      </Project>
      
    4. 在您要运行相关贡献者的任何项目的 .sqlproj 文件中,通过将以下语句添加到 .sqlproj 文件中:<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> 节点之后,导入目标文件。

      <Import Project="$(MSBuildExtensionsPath)\MyContributors\MyContributors.targets " />
      

遵循上述方法之一后,可以使用 MSBuild 传入命令行生成的参数。

注释

必须始终更新“BuildContributors”属性以指定参与者 ID。 在您的参与者源文件中,“ExportBuildContributor”属性使用的就是相同的 ID。 如果没有这个,你的贡献者在生成项目时不会运行。 仅当参与者需要参数才能运行时,才必须更新“ContributorArguments”属性。

生成 SQL 项目

使用 MSBuild 重新生成数据库项目并生成统计信息

  1. 在 Visual Studio 中,右键单击项目并选择“ 重新生成”。 这会重新生成项目,应会看到生成的模型统计信息,其中输出包含在生成输出中,并保存到 ModelStatistics.xml。 可能需要选择“在解决方案资源管理器中 显示所有文件 ”以查看 XML 文件。

  2. 打开 Visual Studio 命令提示符:在“开始”菜单上,选择“所有程序”,选择Microsoft Visual Studio Visual Studio <版本>,选择“Visual Studio 工具”,然后选择“Visual Studio 命令提示符”(<Visual Studio 版本>)。

  3. 在命令提示符下,导航到包含 SQL 项目的文件夹。

  4. 在命令提示符处,键入以下命令:

    MSBuild /t:Rebuild MyDatabaseProject.sqlproj /p:BuildContributors=$(BuildContributors);ExampleContributors.ModelStatistics /p:ContributorArguments=$(ContributorArguments);GenerateModelStatistics=true;SortModelStatisticsBy=name;OutDir=.\;
    

    MyDatabaseProject 替换为要生成的数据库项目的名称。 如果在上次生成项目后更改了该项目,则可以使用 /t:Build 而不是 /t:Rebuild

    在输出中,应会看到生成信息,如以下示例所示:

    Model Statistics:
    ===
    
    Basic model info
    -----------------
        Version: Sql110
      Collation: SQL_Latin1_General_CP1_CI_AS
    
    UserDefinedElements
    -----------------
      DatabaseOptions: 1
             subtotal: 1
          total items: 1
    
    OtherElements
    -----------------
                    Assembly: 1
           BuiltInServerRole: 9
               ClrTypeMethod: 218
      ClrTypeMethodParameter: 197
             ClrTypeProperty: 20
                    Contract: 6
                    DataType: 34
                    Endpoint: 5
                   Filegroup: 1
                 MessageType: 14
                       Queue: 3
                        Role: 10
                      Schema: 13
                     Service: 3
                        User: 4
             UserDefinedType: 3
                    subtotal: 541
                 total items: 16
    
    Relationships
    -----------------
         Composing: 477
      Hierarchical: 6
              Peer: 19
          subtotal: 502
    
  5. 打开 ModelStatistics.xml 并检查内容。

    报告的结果也会保存到 XML 文件中。