データベース プロジェクトをビルドするときにカスタム アクションを実行するビルド共同作成者を作成できます。 このチュートリアルでは、データベース プロジェクトをビルドするときに SQL データベース モデルから統計を出力する ModelStatistics という名前のビルド共同作成者を作成します。 このビルド共同作成者はビルド時にパラメーターを受け取るため、いくつかの追加の手順が必要です。
このチュートリアルでは、次の主要なタスクを実行します。
[前提条件]
このチュートリアルを実行するには、次のコンポーネントが必要です。
SQL Server Data Tools (SSDT) を含み、C# または Visual Basic (VB) の開発をサポートするバージョンの Visual Studio をインストールしている必要があります。
SQL オブジェクトを含む SQL プロジェクトが必要です。
注
このチュートリアルは、SSDT の SQL 機能に既に精通しているユーザーを対象としています。 また、クラス ライブラリの作成方法や、コード エディターを使用してクラスにコードを追加する方法など、Visual Studio の基本的な概念についても理解している必要があります。
貢献者の背景を作成する
ビルド共同作成者は、プロジェクトを表すモデルが生成された後、プロジェクトがディスクに保存される前に、プロジェクトのビルド中に実行されます。 これらは、次のようないくつかのシナリオで使用できます。
モデルの内容を検証し、検証エラーを呼び出し元に報告する。 これは、パラメーターとして OnExecute メソッドに渡されたリストにエラーを追加することで実行できます。
モデル統計の生成とユーザーへのレポート。 次に示す例を示します。
ビルド共同作成者の主要なエントリ ポイントは、OnExecute メソッドです。 BuildContributor から継承するすべてのクラスは、このメソッドを実装する必要があります。 BuildContributorContext オブジェクトがこのメソッドに渡されます。これには、データベースのモデル、ビルド プロパティ、ビルド共同作成者が使用する引数/ファイルなど、ビルドに関連するすべてのデータが含まれます。
TSqlModel とデータベース モデル API
最も便利なオブジェクトは、TSqlModel オブジェクトによって表されるデータベース モデルです。 これは、すべてのテーブル、ビュー、およびその他の要素とその間のリレーションシップを含む、データベースの論理的な表現です。 厳密に型指定されたスキーマがあり、特定の種類の要素に対してクエリを実行し、関心のあるリレーションシップを走査するために使用できます。 この方法の例については、チュートリアル コードを参照してください。
このチュートリアルの共同作成者の例で使用されるコマンドの一部を次に示します。
| クラス | メソッドまたはプロパティ | 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 メソッドをオーバーライドします。
プライベート ヘルパー メソッドをいくつか追加します。
結果のアセンブリをビルドします。
クラス ライブラリ プロジェクトを作成する
MyBuildContributor という名前の Visual Basic または C# クラス ライブラリ プロジェクトを作成します。
"Class1.cs" ファイルの名前を "ModelStatistics.cs" に変更します。
ソリューション エクスプローラーで、プロジェクト ノードを右クリックし、[参照の 追加] を選択します。
System.ComponentModel.Composition エントリを選択し、[OK] を選択します。
必要な SQL 参照を追加する: プロジェクト ノードを右クリックし、[ 参照の追加] を選択します。 [ 参照 ] ボタンを選択します。
C:\Program Files (x86)\Microsoft SQL Server\110\DAC\Binフォルダーに移動します。 Microsoft.SqlServer.Dac.dll、 Microsoft.SqlServer.Dac.Extensions.dll、および Microsoft.Data.Tools.Schema.Sql.dll エントリを選択し、[OK] を選択します。次に、クラスにコードを追加します。
ModelStatistics クラスを定義する
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)); } } }次に、クラス ライブラリをビルドします。
アセンブリに署名してビルドする
[ プロジェクト ] メニューの [MyBuildContributor のプロパティ] を選択します。
[ 署名 ] タブを選択します。
[ アセンブリに署名する] を選択します。
[
キー ファイルの厳密な名前を選択 ]で、[ New ] を選択します。 [ 厳密な名前キーの作成 ] ダイアログ ボックスの [ キー ファイル名] に「 MyRefKey」と入力します。
(省略可能)厳密な名前キー ファイルのパスワードを指定できます。
[OK] を選択.
[ファイル] メニューの [すべてを保存] をクリックします。
[ビルド] メニューの [ソリューションのビルド] を選択します。
次に、SQL プロジェクトのビルド時に読み込まれるようにアセンブリをインストールする必要があります。
ビルド共同作成者をインストールする
ビルド共同作成者をインストールするには、アセンブリと関連付けられている .pdb ファイルを Extensions フォルダーにコピーする必要があります。
MyBuildContributor アセンブリをインストールする
次に、アセンブリ情報を Extensions ディレクトリにコピーします。 Visual Studio が起動すると、
%ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensionsディレクトリとサブディレクトリ内のすべての拡張機能が識別され、使用できるようになります。MyBuildContributor.dll アセンブリ ファイルを出力ディレクトリから
%ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensionsディレクトリにコピーします。注
既定では、コンパイルされた
.dllファイルのパスは YourSolutionPath\YourProjectPath\bin\Debug または YourSolutionPath\YourProjectPath\bin\Release です。
ビルド寄稿者を起動またはテストする
ビルド共同作成者を実行またはテストするには、次のタスクを実行する必要があります。
ビルドする
.sqlprojファイルにプロパティを追加します。MSBuild を使用してデータベース プロジェクトをビルドし、適切なパラメーターを指定します。
SQL プロジェクト (.sqlproj) ファイルにプロパティを追加する
実行する共同作成者の ID を指定するには、常に SQL プロジェクト ファイルを更新する必要があります。 さらに、このビルド共同作成者は MSBuild からコマンド ライン パラメーターを受け入れるので、ユーザーが MSBuild を介してこれらのパラメーターを渡せるように SQL プロジェクトを変更する必要があります。
2 つの方法のいずれかでこれを行うことができます。
.sqlprojファイルを手動で変更して、必要な引数を追加できます。 多数のプロジェクトでビルド共同作成者を再利用しない場合は、これを行うことを選択できます。 このオプションを選択した場合は、ファイル内の最初のインポート ノードの後に次のステートメントを.sqlprojファイルに追加します。<PropertyGroup> <BuildContributors> $(BuildContributors);ExampleContributors.ModelStatistics </BuildContributors> <ContributorArguments Condition="'$(Configuration)' == 'Debug'"> $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name; </ContributorArguments> </PropertyGroup>2 番目の方法は、必要な共同作成者引数を含むターゲット ファイルを作成することです。 これは、既定値が含まれるため、複数のプロジェクトで同じ共同作成者を使用している場合に便利です。
この場合は、MSBuild 拡張機能パスにターゲット ファイルを作成します。
%ProgramFiles%\MSBuildに移動します。ターゲット ファイルが格納されている新しいフォルダー "MyContributors" を作成します。
このディレクトリ内に新しいファイル "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>共同作成者を実行するプロジェクトの
.sqlprojファイル内で、.sqlprojImport Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /< ノードの後に次の >ステートメントを追加して、ターゲット ファイルをインポートします。<Import Project="$(MSBuildExtensionsPath)\MyContributors\MyContributors.targets " />
これらの方法のいずれかに従った後は、MSBuild を使用してコマンド ライン ビルドのパラメーターを渡すことができます。
注
共同作成者 ID を指定するには、常に "BuildContributors" プロパティを更新する必要があります。 これは、共同作成者ソース ファイルの "ExportBuildContributor" 属性で使用される ID と同じです。 これを行わないと、プロジェクトのビルド時にコントリビューターのコードが動作しません。 "ContributorArguments" プロパティは、共同作成者の実行に必要な引数がある場合にのみ更新する必要があります。
SQL プロジェクトをビルドする
MSBuild を使用してデータベース プロジェクトを再構築し、統計を生成する
Visual Studio でプロジェクトを右クリックし、[ リビルド] を選択します。 これによりプロジェクトが再構築され、生成されたモデル統計が表示され、出力がビルド出力に含まれて ModelStatistics.xmlに保存されます。 XML ファイルを表示するには、ソリューション エクスプローラーで [すべてのファイルを表示 ] を選択する必要がある場合があります。
Visual Studio コマンド プロンプトを開きます。 [スタート ] メニュー の [すべてのプログラム] を選択し、[ Microsoft Visual Studio <Visual Studio バージョン>を選択し、 Visual Studio ツールを選択してから、 Visual Studio コマンド プロンプト (<Visual Studio バージョン>) を選択します。
コマンド プロンプトで、SQL プロジェクトを含むフォルダーに移動します。
コマンド プロンプトで、次のコマンドを入力します。
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: 502ModelStatistics.xml を開き、内容を確認します。
報告された結果も XML ファイルに保存されます。