Tutorial: Ampliación de la compilación del proyecto de base de datos para generar estadísticas de modelo

Puede crear un colaborador de compilación para realizar acciones personalizadas al compilar un proyecto de base de datos. En este tutorial, creará un colaborador de compilación denominado ModelStatistics que genera estadísticas del modelo de base de datos SQL al compilar un proyecto de base de datos. Dado que este colaborador de compilación toma parámetros al compilar, se requieren algunos pasos adicionales.

En este tutorial, se realizan las siguientes tareas principales:

Prerrequisitos

Necesitará los componentes siguientes para completar este tutorial:

  • Debe haber instalado una versión de Visual Studio que incluya SQL Server Data Tools (SSDT) y admita el desarrollo de C# o Visual Basic (VB).

  • Debe tener un proyecto SQL que contenga objetos SQL.

Nota:

Este tutorial está diseñado para los usuarios que ya están familiarizados con las características sql de SSDT. También se espera que esté familiarizado con los conceptos básicos de Visual Studio, como cómo crear una biblioteca de clases y cómo usar el editor de código para agregar código a una clase.

Desarrollar el fondo de los colaboradores

Los colaboradores de compilación se ejecutan durante la compilación del proyecto, después de generar el modelo que representa el proyecto, pero antes de guardarlo en el disco. Se pueden usar para varios escenarios, como:

  • Validar el contenido del modelo y notificar errores de validación al autor de la llamada. Esto se puede hacer agregando errores a una lista pasada como parámetro al método OnExecute.

  • Generación de estadísticas de modelo e informes al usuario. Este es el ejemplo que se muestra aquí.

El punto de entrada principal para colaboradores de compilación es el método OnExecute. Todas las clases que heredan de BuildContributor deben implementar este método. Se pasa un objeto BuildContributorContext a este método: contiene todos los datos pertinentes para la compilación, como un modelo de la base de datos, las propiedades de compilación y los argumentos o archivos que usarán los colaboradores de compilación.

TSqlModel y la API de modelo de base de datos

El objeto más útil es el modelo de base de datos, representado por un objeto TSqlModel. Se trata de una representación lógica de una base de datos, incluidas todas las tablas, la vista y otros elementos, además de las relaciones entre ellas. Hay un esquema fuertemente tipado que puede utilizarse para consultar elementos de tipos específicos y explorar relaciones interesantes. Verá ejemplos de cómo se usa en el código de tutorial.

Estos son algunos de los comandos que usa el colaborador de ejemplo en este tutorial:

Class Método o propiedad Description
TSqlModel GetObjects() Consulta el modelo para objetos y es el punto de entrada principal de la API del modelo. Solo se pueden consultar tipos de nivel superior, como Tabla o Vista: los tipos como Columnas solo se pueden encontrar mediante el recorrido del modelo. Si no se especifica ningún filtro ModelTypeClass, se devuelven todos los tipos de nivel superior.
TSqlObject ObtenerInstanciasDeRelacionesReferenciadas() Busca relaciones con los elementos a los que hace referencia el TSqlObject actual. Por ejemplo, para una tabla, devuelve objetos como las columnas de la tabla. En este caso, se puede usar un filtro ModelRelationshipClass para especificar relaciones exactas que se van a consultar (por ejemplo, mediante el filtro Table.Columns se aseguraría de que solo se devolvieron las columnas).

Hay varios métodos similares, como GetReferencingRelationshipInstances, GetChildren y GetParent. Consulte la documentación de api para obtener más información.

Identificación única del colaborador

Durante el proceso de compilación, los colaboradores personalizados se cargan desde un directorio de extensión estándar. Los colaboradores de compilación se identifican mediante un atributo ExportBuildContributor . Este atributo es necesario para que se puedan detectar colaboradores. Este atributo debe tener un aspecto similar al código siguiente:

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

En este caso, el primer parámetro del atributo debe ser un identificador único: se usa para identificar al colaborador en los archivos del proyecto. El procedimiento recomendado es combinar el espacio de nombres de la biblioteca (en este tutorial, "ExampleContributors") con el nombre de clase (en este tutorial, "ModelStatistics") para generar el identificador. Verá cómo se usa este espacio de nombres para especificar que tu colaborador debe ejecutarse más adelante en la guía paso a paso.

Creación de un colaborador de compilación

Para crear un colaborador de compilación, debe realizar las siguientes tareas:

  • Cree un proyecto de biblioteca de clases y agregue referencias necesarias.

  • Defina una clase denominada ModelStatistics que herede de BuildContributor.

  • Invalide el método OnExecute.

  • Agregue algunos métodos auxiliares privados.

  • Compile el ensamblado resultante.

Creación de un proyecto de biblioteca de clases

  1. Cree un proyecto de biblioteca de clases de Visual Basic o C# denominado MyBuildContributor.

  2. Cambie el nombre del archivo "Class1.cs" a "ModelStatistics.cs".

  3. En el Explorador de soluciones, haga clic con el botón derecho en el nodo del proyecto y seleccione Agregar referencia.

  4. Seleccione la entrada System.ComponentModel.Composition y, a continuación, seleccione Aceptar.

  5. Agregar referencias SQL necesarias: haga clic con el botón derecho en el nodo del proyecto y seleccione Agregar referencia. Seleccione el botón Examinar . Vaya a la carpeta C:\Program Files (x86)\Microsoft SQL Server\110\DAC\Bin. Elija el Microsoft.SqlServer.Dac.dll, Microsoft.SqlServer.Dac.Extensions.dlly Microsoft.Data.Tools.Schema.Sql.dll entradas y, a continuación, seleccione Aceptar.

    A continuación, empezará a agregar código a la clase .

Definir la clase ModelStatistics

  1. La clase ModelStatistics procesa el modelo de base de datos pasado al método OnExecute y genera un informe XML que detalla el contenido del modelo.

    En el editor de código, actualice el archivo ModelStatistics.cs para que coincida con el código siguiente:

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

    A continuación, compile la biblioteca de clases.

Firmar y construir el ensamblaje

  1. En el menú Proyecto, seleccione Propiedades de MyBuildContributor.

  2. Seleccione la pestaña Firma .

  3. Seleccione Firmar el ensamblado.

  4. En Elegir un archivo de clave de nombre seguro, seleccione <Nuevo>.

  5. En el cuadro de diálogo Crear clave de nombre seguro , en Nombre de archivo de clave, escriba MyRefKey.

  6. (opcional) Puede especificar una contraseña para el archivo de clave de nombre seguro.

  7. Selecciona Aceptar.

  8. En el menú Archivo, seleccione Guardar todo.

  9. En el menú Compilar, seleccione Compilar solución.

    A continuación, debe instalar el ensamblado para que se cargue al compilar proyectos SQL.

Instalación de un colaborador de compilación

Para instalar un colaborador de compilación, debe copiar el ensamblado y el archivo asociado .pdb a la carpeta Extensiones.

Instalación del ensamblado MyBuildContributor

  1. A continuación, copie la información del ensamblado en el directorio de Extensiones. Cuando se inicia Visual Studio, identifica las extensiones del %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions directorio y los subdirectorios y los pone a disposición para su uso.

  2. Copie el archivo de ensamblado MyBuildContributor.dll del directorio de salida en el %ProgramFiles%\Microsoft SQL Server\110\DAC\Bin\Extensions directorio .

    Nota:

    De forma predeterminada, la ruta de acceso del archivo compilado .dll es YourSolutionPath\YourProjectPath\bin\Debug o YourSolutionPath\YourProjectPath\bin\Release.

Ejecuta o prueba tu colaborador de compilación

Para ejecutar o probar el colaborador de compilación, debe realizar las siguientes tareas:

  • Agregue propiedades al .sqlproj archivo que planea compilar.

  • Compile el proyecto de base de datos mediante MSBuild y proporcione los parámetros adecuados.

Agregar propiedades al archivo del proyecto SQL (.sqlproj)

Siempre debe actualizar el archivo de proyecto de SQL para especificar el ID de los colaboradores que desea incluir. Además, dado que este colaborador de compilación acepta parámetros de línea de comandos de MSBuild, debe modificar el proyecto SQL para permitir que los usuarios pasen esos parámetros a través de MSBuild.

Puede hacerlo de una de las maneras siguientes:

  • Puede modificar manualmente el .sqlproj archivo para agregar los argumentos necesarios. Puede optar por hacerlo si no pretende reutilizar el colaborador de compilación en un gran número de proyectos. Si elige esta opción, agregue las siguientes instrucciones al .sqlproj archivo después del primer nodo Import del archivo.

    <PropertyGroup>
        <BuildContributors>
            $(BuildContributors);ExampleContributors.ModelStatistics
        </BuildContributors>
        <ContributorArguments Condition="'$(Configuration)' == 'Debug'">
            $(ContributorArguments);ModelStatistics.GenerateModelStatistics=true;ModelStatistics.SortModelStatisticsBy=name;
        </ContributorArguments>
    </PropertyGroup>
    
  • El segundo método consiste en crear un archivo de destino que contenga los argumentos de colaborador necesarios. Esto resulta útil si usa el mismo colaborador para varios proyectos, ya que incluye los valores predeterminados.

    En este caso, cree un archivo targets en la ruta de extensiones de MSBuild.

    1. Navegue a %ProgramFiles%\MSBuild.

    2. Cree una nueva carpeta "MyContributors" donde se almacenan los archivos de destino.

    3. Cree un nuevo archivo "MyContributors.targets" dentro de este directorio, agregue el texto siguiente y, a continuación, guarde el archivo:

      <?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 del archivo .sqlproj para cualquier proyecto en el que desee ejecutar scripts adicionales, importe el archivo de targets agregando la siguiente instrucción al archivo .sqlproj después del nodo <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> en el archivo:

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

Una vez que haya seguido uno de estos enfoques, puede usar MSBuild para pasar los parámetros de las compilaciones de línea de comandos.

Nota:

Siempre debe actualizar la propiedad "BuildContributors" para especificar su ID de colaborador. Este es el mismo identificador que se usa en el atributo "ExportBuildContributor" del archivo de origen del colaborador. Sin esto, el componente no se ejecuta al compilar el proyecto. La propiedad "ContributorArguments" debe actualizarse solo si cuenta con los argumentos necesarios para ejecutar la participación.

Compilación del proyecto de SQL

Recompilación del proyecto de base de datos mediante MSBuild y generación de estadísticas

  1. En Visual Studio, haga clic con el botón derecho en el proyecto y seleccione Recompilar. Esto recompila el proyecto y debería ver las estadísticas del modelo generadas, con la salida incluida en la salida de compilación y guardada en ModelStatistics.xml. Es posible que tenga que elegir Mostrar todos los archivos en el Explorador de soluciones para ver el archivo XML.

  2. Abra el símbolo del sistema de Visual Studio: en el menú Inicio, seleccione Todos los programas, luego seleccione Microsoft Visual Studio<Versión>, seleccione Herramientas de Visual Studio y, finalmente, seleccione Símbolo del sistema de Visual Studio (<Versión de Visual Studio>).

  3. En la línea de comandos, navegue al directorio que contiene su proyecto SQL.

  4. En el símbolo del sistema, escriba el siguiente comando:

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

    Reemplace MyDatabaseProject por el nombre del proyecto de base de datos que desea compilar. Si había cambiado el proyecto después de compilarlo por última vez, podría usar /t:Build en lugar de /t:Rebuild.

    Dentro de la salida debería ver información de compilación como en el ejemplo siguiente:

    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 y examine el contenido.

    Los resultados notificados también se conservan en el archivo XML.