Partilhar via


Passo a passo: Estender a compilação do projeto de banco de dados para gerar estatísticas de modelo

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

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

Pré-requisitos

Você precisa 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 ofereça suporte ao desenvolvimento em C# ou Visual Basic (VB).

  • Você deve ter um projeto SQL que contém objetos SQL.

Observação

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

Criar perfil do colaborador

Os contribuidores de compilação são executados durante a compilação do projeto, depois que o modelo que representa o projeto foi gerado, mas antes que o projeto seja salvo no disco. Eles podem ser usados para vários cenários, tais como:

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

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

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

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. Esta é 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 eles. Existe um esquema fortemente tipado que pode ser usado para consultar tipos específicos de elementos e percorrer relações interessantes. Você vê exemplos de como isso é usado no código passo a passo.

Aqui estão alguns dos comandos usados pelo contribuidor de exemplo nesta explicação passo a passo:

Class Método ou propriedade Description
TSqlModel GetObjects() Consulta o modelo para objetos e é o principal ponto de entrada 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 a serem consultadas (por exemplo, usar o filtro Table.Columns garantiria que apenas colunas fossem retornadas).

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

Identificar exclusivamente o seu colaborador

Durante o processo de compilação, os componentes personalizados são carregados de um diretório de extensão padrão. Os Contribuidores de compilação são identificados por um atributo ExportBuildContributor. Esse atributo é necessário para que os colaboradores possam ser descobertos. Este atributo deve ser semelhante ao seguinte código:

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

Neste caso, o primeiro parâmetro para o atributo deve ser um identificador exclusivo - isso é usado para identificar seu colaborador em arquivos de projeto. A prática recomendada é 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ê vê como esse namespace é usado para especificar que seu colaborador deve ser executado posteriormente no passo a passo.

Criar um contribuidor de compilação

Para criar um colaborador de compilação, você deve executar as seguintes tarefas:

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

  • Defina uma classe chamada ModelStatistics que herda de BuildContributor.

  • Substitua o método OnExecute.

  • Adicione alguns métodos auxiliares privados.

  • Construa o conjunto resultante.

Criar um projeto de biblioteca de classes

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

  2. Renomeie o arquivo "Class1.cs" para "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, em seguida, selecione OK.

  5. Adicionar referências 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, em seguida, 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 um relatório XML detalhando o conteúdo do modelo.

    No editor de códigos, 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.

Assine e construa a montagem

  1. No menu Projeto , selecione Propriedades de MyBuildContributor.

  2. Selecione a guia Assinatura .

  3. Selecione Assinar a montagem.

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

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

  6. (facultativo) 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 Compilação, selecione Compilar Solução.

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

Instalar um colaborador de compilação

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

Instalar a assemblagem MyBuildContributor

  1. Em seguida, copie as informações do assembly para o diretório 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 subdiretórios e as disponibiliza para uso.

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

    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 compilação

Para executar ou testar seu colaborador de compilação, você deve executar as seguintes tarefas:

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

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

Adicionar propriedades ao arquivo de projeto SQL (.sqlproj)

Você sempre deve atualizar o arquivo de projeto SQL para especificar a ID dos colaboradores que deseja executar. Além disso, como esse colaborador de compilação 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 pelo MSBuild.

Você pode fazer isso de duas maneiras:

  • 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 contribuidor de compilação em um grande número de projetos. Caso selecione esta opção, adicione as seguintes declarações ao arquivo .sqlproj após o primeiro nó Import.

    <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 destinos contendo os argumentos de contribuidor necessários. Isso é útil se você estiver usando o mesmo colaborador para vários projetos, pois 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" onde seus 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 ficheiro para qualquer projeto que pretende configurar contribuintes, importe o ficheiro de alvos adicionando a seguinte instrução ao .sqlproj ficheiro após o <nó Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> no ficheiro:

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

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

Observação

Você sempre deve atualizar a propriedade "BuildContributors" para especificar sua ID de colaborador. Este é o mesmo ID utilizado no atributo "ExportBuildContributor" no ficheiro de origem do colaborador. Sem isso, o seu componente não é executado ao compilar o projeto. A propriedade "ContributorArguments" deve ser atualizada somente se você tiver argumentos necessários para que seu colaborador seja executado.

Criar o projeto SQL

Reconstrua seu projeto de banco de dados usando o MSBuild e gere estatísticas

  1. No Visual Studio, clique com o botão direito do mouse em seu projeto e selecione Reconstruir. Isso reconstrói o projeto, e você deve ver as estatísticas do modelo geradas, com a saída incluída na saída da compilação 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 Versão do Microsoft Visual Studio <Visual Studio>, selecione Ferramentas do Visual Studio e 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á usar /t:Build em vez de /t:Rebuild.

    Dentro da saída, você deve ver informações de compilação como o exemplo a seguir:

    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 que foram relatados também são persistidos para o arquivo XML.