共用方式為


逐步解說:擴充資料庫專案建置以產生模型統計資料

您可以建立組建參與者,以在建置資料庫專案時執行自定義動作。 在本逐步解說中,您會建立名為 ModelStatistics 的組建參與者,以在建置資料庫專案時從 SQL 資料庫模型輸出統計數據。 因為此組建參與者會在您組建時採用參數,所以需要一些額外的步驟。

在本逐步解說中,您會完成下列主要工作:

先決條件

您需要下列組件才能完成本步驟解說:

  • 您必須已安裝包含 SQL Server Data Tools (SSDT) 並支援 C# 或 Visual Basic (VB) 開發的 Visual Studio 版本。

  • 您必須有包含 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”) 結合,以產生標識符。 您會看到如何使用此命名空間來指定您的貢獻者應在引導式教程中之後執行。

建立建置貢獻者

若要建立建置參與者,您必須執行下列步驟:

  • 建立類別庫專案,並新增必要的參考。

  • 定義名為 ModelStatistics 的類別,其繼承自 BuildContributor

  • 覆寫 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. 在 [File] \(檔案\) 功能表上,選取 [Save All] \(全部儲存\)

  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 項目檔,以指定您想要執行的參與者識別碼。 此外,由於此組建參與者接受 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」 屬性,以指定參與者標識碼。 這與您參與者來源檔案中的 「ExportBuildContributor」 屬性中使用的標識符相同。 若未包含此項,您的貢獻元件在建置專案時不會運行。 只有在您有供參與者執行所需的參數時,才需更新「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 檔案。