Compartilhar via


Passo a passo: estender o build do projeto de banco de dados para gerar estatísticas de modelo

Você pode criar um colaborador de build para executar ações personalizadas ao criar um projeto de banco de dados. Neste passo a passo, você cria um colaborador de build chamado ModelStatistics que gera estatísticas do modelo de banco de dados SQL ao criar um projeto de banco de dados. Como esse colaborador de build usa parâmetros quando você cria, algumas etapas extras são necessárias.

Neste passo a passo, você realiza as seguintes tarefas principais:

Pré-requisitos

Você precisará dos seguintes componentes para concluir este passo a passo:

  • Você deve ter instalado uma versão do Visual Studio que inclua o SSDT (SQL Server Data Tools) e dê suporte ao desenvolvimento do C# ou do Visual Basic (VB).

  • Você deve ter um projeto SQL que contenha objetos SQL.

Observação

Este passo a passo destina-se a usuários que já estão familiarizados com os recursos sql do SSDT. Espera-se também que você esteja familiarizado com conceitos básicos do Visual Studio, como criar uma biblioteca de classes e como usar o editor de código para adicionar código a uma classe.

Criar histórico do colaborador

Os contribuintes de compilação são executados durante o build do projeto, depois que o modelo que representa o projeto tenha sido gerado, mas antes que o projeto seja salvo em disco. Eles podem ser usados para vários cenários, como:

  • Validando o conteúdo do modelo e relatando erros de validação para o chamador. Isso pode ser feito adicionando erros a uma lista passada como um parâmetro ao método OnExecute.

  • Gerando estatísticas de modelo e relatórios para o usuário. Este é o exemplo mostrado aqui.

O principal ponto de entrada para colaboradores de build é o método OnExecute. Todas as classes herdadas do BuildContributor devem implementar esse método. Um objeto BuildContributorContext é passado para esse método – ele contém todos os dados relevantes para o build, como um modelo do banco de dados, propriedades de build e argumentos/arquivos a serem usados por colaboradores de build.

TSqlModel e a API do modelo de banco de dados

O objeto mais útil é o modelo de banco de dados, representado por um objeto TSqlModel. Essa é uma representação lógica de um banco de dados, incluindo todas as tabelas, exibição e outros elementos, além das relações entre elas. Há um esquema fortemente tipado que pode ser usado para consultar tipos específicos de elementos e percorrer relações interessantes. Você verá exemplos de como isso é usado no código passo a passo.

Aqui estão alguns dos comandos usados pelo colaborador de exemplo neste passo a passo:

Class Método ou propriedade Description
TSqlModel GetObjects() Consulta o modelo de objetos e é o ponto de entrada principal para a API do modelo. Somente tipos de nível superior, como Tabela ou Exibição, podem ser consultados - tipos como Colunas só podem ser encontrados atravessando o modelo. Se nenhum filtro ModelTypeClass for especificado, todos os tipos de nível superior serão retornados.
TSqlObject GetReferencedRelationshipInstances() Localiza relações com elementos referenciados pelo TSqlObject atual. Por exemplo, para uma tabela, isso retorna objetos como as colunas da Tabela. Nesse caso, um filtro ModelRelationshipClass pode ser usado para especificar relações exatas para consultar (por exemplo, usar o filtro Table.Columns garantiria que apenas colunas fossem retornadas).

Há vários métodos semelhantes, como GetReferencingRelationshipInstances, GetChildren e GetParent. Consulte a documentação da API para obter mais informações.

Identificando exclusivamente seu colaborador

Durante o processo de build, os contribuidores personalizados são carregados de um diretório padrão de extensões. Os Colaboradores de Build são identificados por um atributo ExportBuildContributor . Esse atributo é necessário para que os colaboradores possam ser descobertos. Esse atributo deve ser semelhante ao seguinte código:

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

Nesse caso, o primeiro parâmetro para o atributo deve ser um identificador exclusivo - isso é usado para identificar seu colaborador em arquivos de projeto. A melhor prática é combinar o namespace da biblioteca (neste passo a passo, "ExampleContributors") com o nome da classe (neste passo a passo, "ModelStatistics") para produzir o identificador. Você vai ver como este namespace é utilizado para especificar que seu contribuinte deve ser executado mais tarde no tutorial.

Criar um colaborador de build

Para criar um colaborador de build, você deve executar as seguintes tarefas:

  • Crie um projeto de biblioteca de classes e adicione referências necessárias.

  • Defina uma classe chamada ModelStatistics que herda do BuildContributor.

  • Sobrescreva o método OnExecute.

  • Adicione alguns métodos auxiliares privados.

  • Monte o assembly resultante.

Criar um projeto de biblioteca de classes

  1. Crie um projeto de biblioteca de classes do Visual Basic ou C# chamado MyBuildContributor.

  2. Renomeie o arquivo "Class1.cs" como "ModelStatistics.cs".

  3. No Gerenciador de Soluções, clique com o botão direito do mouse no nó do projeto e selecione Adicionar Referência.

  4. Selecione a entrada System.ComponentModel.Composition e selecione OK.

  5. Adicionar referências de SQL necessárias: clique com o botão direito do mouse no nó do projeto e selecione Adicionar Referência. Selecione o botão Procurar . Navegue até a pasta C:\Program Files (x86)\Microsoft SQL Server\110\DAC\Bin. Escolha as entradas Microsoft.SqlServer.Dac.dll, Microsoft.SqlServer.Dac.Extensions.dlle Microsoft.Data.Tools.Schema.Sql.dll e selecione OK.

    Em seguida, você começa a adicionar código à classe.

Definir a classe ModelStatistics

  1. A classe ModelStatistics processa o modelo de banco de dados passado para o método OnExecute e produz e relatório XML detalhando o conteúdo do modelo.

    No editor de código, atualize o arquivo ModelStatistics.cs para corresponder ao seguinte código:

    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));
            }
        }
    }
    

    Em seguida, você cria a biblioteca de classes.

Assinar e compilar o assembly

  1. No menu Projeto , selecione Propriedades do MyBuildContributor.

  2. Selecione a aba Assinatura.

  3. Selecione Assinar o assembly.

  4. Em Escolher um arquivo de chave de nome forte, selecione <Novo>.

  5. Na caixa de diálogo Criar Chave de Nome Forte , no nome do arquivo Chave, digite MyRefKey.

  6. (opcional) Você pode especificar uma senha para seu arquivo de chave de nome forte.

  7. Selecione OK.

  8. No menu Arquivo, selecione Salvar Tudo.

  9. No menu Build, selecione Compilar Solução.

    Em seguida, você deve instalar o assembly para que ele seja carregado ao compilar projetos SQL.

Instalar um colaborador de build

Para instalar um colaborador de build, você deve copiar o assembly e o arquivo associado .pdb para a pasta Extensões.

Instalar o componente MyBuildContributor

  1. Em seguida, você copia as informações de montagem para o diretório de Extensões. Quando o Visual Studio é iniciado, ele identifica todas as %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions extensões no diretório e nos subdiretórios e as disponibiliza para uso.

  2. Copie o arquivo de assembly MyBuildContributor.dll do diretório de saída para o diretório %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions.

    Observação

    Por padrão, o caminho do arquivo compilado .dll é YourSolutionPath\YourProjectPath\bin\Debug ou YourSolutionPath\YourProjectPath\bin\Release.

Executar ou testar seu colaborador de build

Para executar ou testar o colaborador de build, você deve executar as seguintes tarefas:

  • Adicione propriedades ao .sqlproj arquivo que você planeja compilar.

  • Crie o projeto de banco de dados usando o MSBuild e fornecendo os parâmetros apropriados.

Adicionar propriedades ao arquivo do projeto SQL (.sqlproj)

Você sempre deve atualizar o arquivo de projeto do SQL para especificar a ID dos colaboradores que deseja executar. Além disso, como esse colaborador de build aceita parâmetros de linha de comando do MSBuild, você deve modificar o projeto SQL para permitir que os usuários passem esses parâmetros por meio do MSBuild.

É possível fazer isso de duas formas:

  • Você pode modificar manualmente o .sqlproj arquivo para adicionar os argumentos necessários. Você pode optar por fazer isso se não pretende reutilizar o colaborador de build em um grande número de projetos. Se você escolher essa opção, adicione as seguintes instruções ao arquivo .sqlproj após o primeiro nó Importar.

    <PropertyGroup>
        <BuildContributors>
            $(BuildContributors);ExampleContributors.ModelStatistics
        </BuildContributors>
        <ContributorArguments Condition="'$(Configuration)' == 'Debug'">
            $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name;
        </ContributorArguments>
    </PropertyGroup>
    
  • O segundo método é criar um arquivo de destino contendo os argumentos de colaborador necessários. Isso será útil se você estiver usando o mesmo colaborador para vários projetos, pois ele inclui os valores padrão.

    Nesse caso, crie um arquivo de destino no caminho de extensões do MSBuild:

    1. Navegue até %ProgramFiles%\MSBuild.

    2. Crie uma nova pasta "MyContributors" em que os arquivos de destino são armazenados.

    3. Crie um novo arquivo "MyContributors.targets" dentro deste diretório, adicione o seguinte texto a ele e salve o arquivo:

      <?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. Dentro do .sqlproj arquivo de qualquer projeto que você deseja executar contendo colaboradores, importe o arquivo Targets adicionando a seguinte instrução ao .sqlproj arquivo após o nó <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> no arquivo:

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

Depois de seguir uma dessas abordagens, você pode usar o MSBuild para passar os parâmetros para builds de linha de comando.

Observação

Você sempre deve atualizar a propriedade "BuildContributors" para especificar sua ID de colaborador. Essa é a mesma ID usada no atributo "ExportBuildContributor" no arquivo de origem do colaborador. Sem isso, o componente não é executado ao realizar a compilação do projeto. A propriedade "ContributorArguments" deve ser atualizada somente se você tiver argumentos necessários para executar o componente do colaborador.

Compilar o projeto SQL

Recompilar seu projeto de banco de dados usando o MSBuild e gerar estatísticas

  1. No Visual Studio, clique com o botão direito do mouse em seu projeto e selecione Recompilar. Isso recompila o projeto e você deve ver as estatísticas do modelo geradas, com a saída incluída na saída do build e salva em ModelStatistics.xml. Talvez seja necessário escolher Mostrar todos os Arquivos no Gerenciador de Soluções para ver o arquivo XML.

  2. Abra um prompt de comando do Visual Studio: no menu Iniciar, selecione Todos os Programas, selecione Microsoft Visual Studio <Versão do Visual Studio>, selecione Ferramentas do Visual Studio e, em seguida, selecione Prompt de Comando do Visual Studio (<Versão do Visual Studio>).

  3. No prompt de comando, navegue até a pasta que contém seu projeto SQL.

  4. No prompt de comando, digite o seguinte comando:

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

    Substitua MyDatabaseProject pelo nome do projeto de banco de dados que você deseja criar. Se você tiver alterado o projeto depois de construí-lo pela última vez, poderá usá-lo /t:Build em vez de /t:Rebuild.

    Dentro da saída, você deverá ver informações de build como o seguinte exemplo:

    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. Abra ModelStatistics.xml e examine o conteúdo.

    Os resultados relatados também são persistidos no arquivo XML.