연습: 데이터베이스 프로젝트 빌드를 확장하여 모델 통계 생성
데이터베이스 프로젝트를 빌드할 때 사용자 지정 작업을 수행하는 빌드 기여자를 만들 수 있습니다. 이 연습에서는 데이터베이스 프로젝트를 빌드할 때 SQL 데이터베이스 모델에서 통계를 출력하는 ModelStatistics라는 이름의 빌드 기여자를 만드는 방법을 알아봅니다. 이 빌드 기여자는 빌드할 때 매개 변수를 사용하므로 몇 가지 추가 단계가 필요합니다.
이 연습에서 수행하는 주요 작업은 다음과 같습니다.
필수 조건
이 연습을 완료하려면 다음과 같은 구성 요소가 필요합니다.
SQL Server Data Tools(SSDT)가 포함되고 C# 또는 VB 개발이 지원되는 Visual Studio 버전이 설치되어 있어야 합니다.
SQL 개체가 포함된 SQL 프로젝트가 있어야 합니다.
참고 항목
이 연습은 SSDT의 SQL 기능에 이미 익숙한 사용자를 위한 것입니다. 또한 클래스 라이브러리를 만드는 방법 및 코드 편집기를 사용하여 클래스에 코드를 추가하는 방법과 같은 기본 Visual Studio 개념을 잘 알고 있어야 합니다.
빌드 기여자 배경
빌드 기여자 프로젝트를 나타내는 모델이 생성된 후이지만 프로젝트가 디스크에 저장되기는 전인 프로젝트 빌드 중에 실행됩니다. 다음과 같은 여러 시나리오에 사용할 수 있습니다.
모델 콘텐츠 유효성 검사 및 유효성 검사를 호출자에게 보고. OnExecute 메서드에 매개 변수로 전달된 목록에 오류를 추가하여 이 작업을 수행할 수 있습니다.
모델 통계 생성 및 사용자에게 보고 이는 여기 보이는 예제입니다.
빌드 기여자 기본 진입점은 OnExecute 메서드입니다. BuildContributor에서 상속되는 모든 클래스는 이 메서드를 구현해야 합니다. BuildContributorContext 개체가 이 메서드에 전달됩니다. 여기에는 빌드 기여자 사용할 데이터베이스 모델, 빌드 속성 및 인수/파일과 같은 빌드에 대한 모든 관련 데이터가 포함됩니다.
TSqlModel 및 데이터베이스 모델 API
가장 유용한 개체는 TSqlModel 개체로 대표되는 데이터베이스 모델입니다. 이는 모든 테이블, 뷰 및 기타 요소와 해당 요소 간의 관계를 포함하는 데이터베이스의 논리적인 진술입니다. 특정 유형의 요소를 쿼리하고 필요한 관계를 트래버스하는 데에는 강력한 형식의 스키마를 사용할 수 있습니다. 연습 코드에서 이 방법을 사용하는 방법에 대한 예제를 확인합니다.
다음은 이 연습에서 기여자 예제에서 사용하는 몇 가지 명령입니다.
클래스 | 메서드/속성 | 설명 |
---|---|---|
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 또는 Visual C# 클래스 라이브러리 프로젝트를 만듭니다.
파일 이름을 "Class1.cs"에서 "ModelStatistics.cs"로 바꿉니다.
솔루션 탐색기에서 프로젝트 노드를 마우스 오른쪽 단추로 클릭한 다음 참조 추가를 클릭합니다.
System.ComponentModel.Composition의 선택 후 확인을 클릭합니다.
필요한 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 항목을 선택한 다음 확인을 클릭합니다.
다음으로 클래스에 코드를 추가하기 시작합니다.
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. /// Will only run 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 the following: /// /// <PropertyGroup> /// <ContributorArguments Condition="'$(Configuration)' == 'Debug'"> /// $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy="name"; /// </ContributorArguments> /// <PropertyGroup> /// /// This will generate 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 will later // be serialized. 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 will later be 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 will be 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 속성을 클릭합니다.
시그니처 탭을 클릭합니다.
어셈블리에 서명을 클릭합니다.
강력한 이름 키 파일 선택에서 <새로 만들기>를 클릭합니다.
강력한 이름 키 만들기 대화 상자에서 키 파일 이름에 MyRefKey를 입력합니다.
(선택 사항)강력한 이름 키 파일의 암호를 지정할 수 있습니다.
확인을 클릭합니다.
파일 메뉴에서 모두 저장을 클릭합니다.
빌드 메뉴에서 솔루션 빌드를 클릭합니다.
그런 다음 SQL 프로젝트를 빌드할 때 로드되도록 어셈블리를 설치해야 합니다.
빌드 참가자 설치
빌드 참가자를 설치하려면 어셈블리 및 연관된 .pdb 파일을 Extensions 폴더에 복사해야 합니다.
MyBuildContributor 어셈블리를 설치하려면
다음으로 어셈블리 정보를 확장 디렉터리에 복사합니다. Visual Studio가 시작되면 %Program Files%\Microsoft SQL Server\110\DAC\Bin\Extensions 디렉터리 및 하위 디렉터리에서 확장을 식별하고 이를 사용할 수 있도록 설정합니다.
출력 디렉터리에서 %Program Files%\Microsoft SQL Server\110\DAC\Bin\Extensions 디렉터리로 MyBuildContributor.dll 어셈블리 파일을 복사합니다.
참고 항목
기본적으로 컴파일된 .dll 파일의 경로는 YourSolutionPath\YourProjectPath\bin\Debug 또는 YourSolutionPath\YourProjectPath\bin\Release입니다.
빌드 기여자 실행 또는 테스트
빌드 기여자를 실행 또는 테스트하려면 다음 작업을 수행해야 합니다.
빌드하려는 .sqlproj 파일에 속성을 추가합니다.
MSBuild를 사용하여 데이터베이스 프로젝트를 빌드하고 적절한 매개 변수를 제공합니다.
SQL 프로젝트(.sqlproj) 파일에 속성 추가
실행하려는 기여자 ID를 지정하려면 항상 SQL 프로젝트 파일을 업데이트해야 합니다. 또한 이 빌드 기여자는 MSBuild의 명령줄 매개 변수를 허용하므로 사용자가 MSBuild를 통해 해당 매개 변수를 전달할 수 있도록 SQL 프로젝트를 수정해야 합니다.
이 작업은 다음 두 가지 방법 중 한 가지로 수행할 수 있습니다.
.sqlproj 파일을 수동으로 수정해서 필요한 인수를 추가할 수 있습니다. 빌드 기여자를 여러 프로젝트에서 다시 사용하지 않으려는 경우 이 작업을 수행할 수 있습니다. 이 옵션을 선택할 경우, .sqlproj 파일에서 파일의 첫 번째 가져오기 노드 다음에 다음 문을 추가합니다.
<PropertyGroup> <BuildContributors> $(BuildContributors);ExampleContributors.ModelStatistics </BuildContributors> <ContributorArguments Condition="'$(Configuration)' == 'Debug'"> $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name; </ContributorArguments> </PropertyGroup>
두 번째 방법은 필요한 기여자 인수를 포함하는 대상 파일을 만드는 것입니다. 이는 기본값을 포함하므로 여러 프로젝트에 동일한 기여자를 사용하는 경우에 유용합니다.
이 경우 MSBuild 확장 경로에 대상 파일을 만듭니다.
%Program Files%\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 파일 내에서 <Import Project=“$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets” /> 노드 뒤에 다음 문을 추가하여 대상 파일을 가져옵니다.
<Import Project="$(MSBuildExtensionsPath)\MyContributors\MyContributors.targets " />
이러한 방법 중 하나를 수행한 후에는 MSBuild를 사용하여 명령줄 빌드에 대한 매개 변수를 전달할 수 있습니다.
참고 항목
기여자 ID를 지정하려면 항상 "BuildContributors" 속성을 업데이트해야 합니다. 이 ID는 기여자 원본 파일에서 "ExportBuildContributor" 특성에 사용된 것과 동일한 ID입니다. 이 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:Rebuild 대신 /t:Build를 사용할 수 있습니다.
출력 안에서 다음 같은 빌드 정보를 확인할 수 있습니다.
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
ModelStatistics.xml를 열고 내용을 검사합니다.
보고된 결과는 XML 파일에도 저장됩니다.
다음 단계
출력 XML 파일의 처리를 수행하기 위한 추가 도구를 만들 수도 있습니다. 이는 빌드 기여자의 한 가지 예제에 불과합니다. 예를 들어 빌드의 일부로 데이터 사전 파일을 출력하는 빌드 기여자를 만들 수 있습니다.
참고 항목
빌드 및 배포 기여자를 사용하여 데이터베이스 빌드 및 배포 사용자 지정
연습: 데이터베이스 프로젝트 배포를 확장하여 배포 계획 분석