martes, 6 de marzo de 2018

Servicios comunes en aplicación de consola .NET Core

Si usamos ASP.NET Core, es fácil que estemos acostumbrados a que los servicios comunes de la plataforma estén allí o al menos, que sea muy fácil incluirlos y consumirlos. Sin embargo, con una aplicación de consola monda y lironda, tenemos un triste Main y puede ser que incluir DI, logging o configuración se haga un poco cuesta arriba (al menos a mí se me hizo).

En cualquier caso, se tiene que poder hacer, porque ya sabemos que una aplicación ASP.NET Core es al fin y al cabo una aplicación de consola (aunque ASP.NET Core es también un framework y hace uso de IoC para facilitar nuestro trabajo). Sin embargo, con File > New > Project… > Console App estamos solos contra el mundo.

Por servicios comunes se entienden DI (bueno, estrictamente hablando un contenedor de IoC), logging y configuración, servicios a los que no debemos renunciar en una aplicación de consola, o al menos no renunciar si no queremos.

Después de escribir el post, me chivaron por twitter que en .NET Core 2.1 habrá importantes cambios en cuanto a crear un host en una aplicación de consola. Puedes leer más información en Having Fun with the .NET Core Generic Host y desde el propio github del equipo de producto.

Yo por mi parte he actualizado el repositorio de github con un ejemplo idéntico al aquí expuesto pero funcionando con el nuevo host genérico, así se pueden comparar ambos. En cualquier caso, el post seguía aquí...

Lo primero que vamos a ver es la configuración.

var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

var builder = new ConfigurationBuilder()
    .SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location))
    .AddJsonFile("appsettings.json", optional: true)
    .AddJsonFile($"appsettings.{environment}.json", optional: true)
    .AddEnvironmentVariables()
    .AddCommandLine(args);

Inicialmente, para establecer la ruta base de la configuración usaba Directory.GetCurrentDirectory(), pero de nuevo en twitter me dijeron que mejor no asumir que el cwd (current working directory) es el mismo directorio que donde esté el ejecutable.

Para que no explote nada, tenemos que agregar en la raíz del proyecto un fichero con el nombre appsettings.json, por ejemplo:

{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Debug"
      }
    }
  },
  "Foo": {
    "Bar": "Baz"
  }
}

Es importante poner “Copy if newer” en “Copy to Output Directory” para que se copie el fichero en el directorio \bin.

Además, toca agregar paquetes porque en una aplicación de consola empezamos con un .csproj más limpio que una patena.

dotnet add package Microsoft.Extensions.Configuration.FileExtensions
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.Extensions.Configuration.CommandLine

Ahora vamos con el logging.

var loggerFactory = new LoggerFactory()
    .AddConsole(configuration.GetSection("Logging:Console"))
    .AddDebug();

Y de nuevo toca agregar los paquetes oportunos.

dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug

La verdad es que en este punto ya podríamos usar tanto la configuración como el logging.

var logger = loggerFactory.CreateLogger<Program>();
logger.LogWarning("Hello World!");

Sin embargo, nos falta hacer DI para poder sacar todo el jugo a nuestra aplicación de consola.

Forzando un poco el ejemplo y para ver después cómo es posible usar patrón de opciones creamos la clase Foo que recogerá la configuración que agregamos antes en el fichero appsettings.json

class Foo
{
    public string Bar { get; set; }
}

Ahora ya sí, creamos el contenedor.

IServiceCollection services = new ServiceCollection();
            
services
    .AddSingleton(loggerFactory)
    .AddLogging();
            
services
    .AddSingleton(configuration)
    .AddOptions()
    .Configure<Foo>(configuration.GetSection("Foo"));

var serviceProvider = services.BuildServiceProvider();

De nuevo, estos son los paquetes que hay que instalar.

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions

En este punto, toda la infraestructura está lista, pero ¿Cómo arrancamos nuestra aplicación de consola? Pues creando una clase App e invocando su método Run (tanto App como Run parecen nombres comúnmente usados pero eres libre de elegir cualquier otro). Lógicamente, nos toca registrar la clase App en el contenedor y resolverla antes de poder usarla.

Todo el código junto y disponible en un repositorio de github

using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false)
                .AddJsonFile($"appsettings.{environment}.json", optional: true)
                .AddEnvironmentVariables()
                .AddCommandLine(args);

            var configuration = builder.Build();

            var loggerFactory = new LoggerFactory()
                .AddConsole(configuration.GetSection("Logging:Console"))
                .AddDebug();

            IServiceCollection services = new ServiceCollection();

            services
                .AddSingleton(loggerFactory)
                .AddLogging();

            services
                .AddSingleton(configuration)
                .AddOptions()
                .Configure<Foo>(configuration.GetSection("Foo"));

            services.AddTransient<App>();

            var serviceProvider = services.BuildServiceProvider();

            var app = (App)serviceProvider.GetService(typeof(App));
            app.Run();
        }
    }

    class Foo
    {
        public string Bar { get; set; }
    }

    class App
    {
        private readonly ILogger<App> _logger;
        private readonly Foo _foo;

        public App(ILogger<App> logger, IOptions<Foo> foo)
        {
            _logger = logger;
            _foo = foo.Value;
        }
        public void Run()
        {
            _logger.LogDebug(_foo.Bar);
        }
    }
}

Un saludo!

lunes, 26 de febrero de 2018

Find vs First vs Single

Hay varios métodos de LINQ que, a priori, parecen sirven al mismo propósito, pero que al fijarnos en detalle vemos son muy distintos, más si cabe si estamos usando LINQ to Entities, me estoy refiriendo a First, FirstOrDefault, Single, SingleOrDefault y Find (método que de hecho sólo está disponible en DbSet<>).

Find es el método que deberíamos casi siempre intentar usar porque es el único que podría evitar un round-trip a la base de datos. Find primero busca en el contexto y si no encuentra nada, es entonces cuando ejecuta una consulta a la base de datos. En caso de ir a base de datos y volverse con las manos vacías, devuelve null. Por contra, Find recibe un params object[] keyValues, con lo que si la clave es compuesta podría no ser muy intuitivo y propenso a errores.

Sólo se me ocurre no usar Find si usamos por ejemplo Include, ya que Include es un método de extensión de IQueryable<> y Find devuelve directamente una entidad.

Por otro lado, First, FirstOrDefault, Single y SingleOrDefault siempre ejecutarán una consulta a la base de datos, aunque la entidad que fueran a materializar estuviera ya disponible en el contexto. Que siempre vaya a la base de datos no significa que devuelva los valores que estén en la base de datos ya que, de estar disponible en el contexto, nos devolverá la instancia previamente materializada, esto es lo que se denomina en terminología de ORMs, IdentityMap.

Otra gran diferencia es que Single lanzará una excepción si encuentra 2 o más registros, así que no hay se debería usar First cuando en realidad se quería usar Single o, mejor aún, Find.

Con el siguiente código se puede ver todo lo expuesto.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new ShopContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
                context.Orders.Add(new Order() { Id = 1, CustomerPurchaseOrder = "foo" });
                context.Orders.Add(new Order() { Id = 2, CustomerPurchaseOrder = "foo" });
                context.SaveChanges();
            }

            using (var context = new ShopContext())
            {
                var order = context.Orders.Find(1);
                //exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[CustomerPurchaseOrder]
                //FROM [Orders] AS [e]
                //WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=1

                order.CustomerPurchaseOrder = "panicoenlaxbox"; // Identity Map

                order = context.Orders.Find(1); // no db query

                Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox

                order = context.Orders.Find(3);                
                //exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[CustomerPurchaseOrder]
                //FROM [Orders] AS [e]
                //WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=3

                Console.WriteLine(order == null); // True

                order = context.Orders.First(o => o.Id == 1);
                //SELECT TOP(1) [o].[Id], [o].[CustomerPurchaseOrder]
                //FROM [Orders] AS [o]
                //WHERE [o].[Id] = 1

                Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox

                try
                {
                    order = context.Orders.First(o => o.Id == 3);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message); // Sequence contains no elements
                }

                order = context.Orders.FirstOrDefault(o => o.Id == 3);
                //SELECT TOP(1) [o].[Id], [o].[CustomerPurchaseOrder]
                //FROM [Orders] AS [o]
                //WHERE [o].[Id] = 2

                Console.WriteLine(order == null); // True

                order = context.Orders.Single(o => o.Id == 1);
                //SELECT TOP(2) [o].[Id], [o].[CustomerPurchaseOrder]
                //FROM [Orders] AS [o]
                //WHERE [o].[Id] = 1

                Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox

                try
                {
                    order = context.Orders.Single(o => o.Id == 3);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message); // Sequence contains no elements
                }

                try
                {
                    order = context.Orders.Single(o => o.CustomerPurchaseOrder == "foo");
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message); // Sequence contains more than one element
                }

                order = context.Orders.SingleOrDefault(o => o.Id == 1);
                //SELECT TOP(2) [o].[Id], [o].[CustomerPurchaseOrder]
                //FROM [Orders] AS [o]
                //WHERE [o].[Id] = 1

                Console.WriteLine(order.CustomerPurchaseOrder); //panicoenlaxbox                
            }
        }
    }

    class ShopContext : DbContext
    {
        public DbSet<Order> Orders { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Shop;Trusted_Connection=True");
            }
            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Order>().Property(o => o.Id).ValueGeneratedNever();
            base.OnModelCreating(modelBuilder);
        }
    }

    internal class Order
    {
        public int Id { get; set; }
        public string CustomerPurchaseOrder { get; set; }
    }
}

Un saludo!

miércoles, 31 de enero de 2018

SQL dinámico con ExpandoObject y Dapper

Crear una consulta SQL con una condición de filtrado dinámico es algo que tarde o temprano sale a la palestra

En la solución que se muestra a continuación, se usa un objeto de tipo ExpandoObject que permite agregar propiedades en tiempo de ejecución. Lo explica muy bien @eiximenis en el post Var, object y dynamic

Si a ExpandoObject le sumamos Dapper, la solución es segura contra SQL injection y flexible en cuanto a su confección

En el ejemplo he procurado que sea vea que podemos tanto agregar propierdades de forma dinámica por el mero hecho de asignar un valor, así como castear el ExpandoObject a un dicccionario para agregar propiedades de forma dinámica

Primero el código SQL para crear una tabla y que el ejemplo funcione

CREATE TABLE [dbo].[Table_1](
	[Id] [int] NOT NULL,
	[TenantId] [int] NOT NULL,
	[C1] [int] NULL,
	[C2] [nvarchar](50) NULL,
	[C3] [datetime2](7) NULL,
 CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)

Ahora el código

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Dynamic;
using System.Linq;
using Dapper;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            QueryExample(new Dictionary<string, object>()
            {
                { "C1", 1 },
                { "C2", "Foo" },
                { "C3", DateTime.Now }
            });
        }

        private static void QueryExample(IDictionary<string, object> criterias)
        {
            const string connectionString = @"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;";

            var sql = "SELECT * FROM Table_1 WHERE TenantId = @TenantId";

            var columnsWhitelist = new[] { "C1", "C2", "C3" };

            foreach (var criteria in criterias)
            {
                if (!columnsWhitelist.Contains(criteria.Key))
                {
                    throw new ArgumentException($"{criteria.Key} is not allowed as column name", nameof(criteria));
                }
                sql += $" AND {criteria.Key} = @{criteria.Key}";
            }

            dynamic parameters = new ExpandoObject();

            parameters.TenantId = 1;

            var dictionary = (IDictionary<string, object>)parameters;
            foreach (var criteria in criterias)
            {
                dictionary.Add(criteria.Key, criteria.Value);
            }

            using (var connection = new SqlConnection(connectionString))
            {
                connection.Query(sql, (ExpandoObject)parameters);
            }

            Console.ReadKey();
        }
    }
}

El SQL que se ejecuta finalmente es el siguiente

exec sp_executesql N'SELECT * FROM Table_1 WHERE TenantId = @TenantId AND C1 = @C1 AND C2 = @C2 AND C3 = @C3',N'@TenantId int,@C1 int,@C2 nvarchar(4000),@C3 datetime',@TenantId=1,@C1=1,@C2=N'Foo',@C3='2018-01-31 18:58:56.823'

Un saludo!

miércoles, 13 de diciembre de 2017

Gestión de errores en SQL Server

Si te toca escribir “algo” de lógica de negocio en T-SQL, y te agobia la gestión de errores (como a mi), espero que después de este post tengamos los 2 las cosas un poco más claras.

La primera opción que para tratar errores es con el estilo old-school, es decir, con @@ERROR, que devuelve un número de error si la última sentencia T-SQL ejecutada dio algún error, devolverá 0 si no hubo ningún error.

Para todos los ejemplos vamos a usar una tabla con una sola columna.

    CREATE TABLE Table1 (Id INT PRIMARY KEY)
    

Usando @@ERROR

    DELETE FROM Table1;
    GO
    INSERT INTO Table1 VALUES (1);
    INSERT INTO Table1 VALUES (1);
    IF @@ERROR <> 0
        PRINT 'There was an error';
    INSERT INTO Table1 VALUES (2);
    GO
    SELECT COUNT(*) FROM Table1;
    

Lo más relevante de este código es que, finalmente, la tabla tiene 2 registros, es decir, a pesar del error de la línea 4, el resto del script se ha seguido ejecutando. Este comportamiento de seguir ejecutando el script es el predeterminado, pero ¿qué pasa si no quiero que sea así? Pues podemos usar XACT_ABORT, además @@ERROR no parece una técnica muy segura.

Si XACT_ABORT es ON, en caso de haber un error, se acaba la ejecución inmediatamente del lote y se revierte, si la hubiera, la transacción explícita. Si es OFF, el valor predeterminado, pues funciona como el anterior script, la ejecución sigue y no se revierte automáticamente ninguna transacción explícita (sólo la implícita que es la propia sentencia).

        SET XACT_ABORT ON;
        DELETE FROM Table1;
        GO
        INSERT INTO Table1 VALUES (1);
        INSERT INTO Table1 VALUES (1);
        INSERT INTO Table1 VALUES (2);
        GO
        SELECT COUNT(*) FROM Table1;    
        

Es decir, la instrucción 6 no se ejecuta porque se aborta la ejecución del lote, por eso finalmente, sólo hay 1 registro en la tabla destino.

Antes de ver como XACT_ABORT ON revierte automáticamente una transacción explícita, es importante conocer la función XACT_STATE. Esta función nos devuelve un valor que indica si hay o no una transacción explícita y en que estado está.

  • 1. Hay transacción.
  • 0. No hay transacción.
  • -1. Hay transacción, pero un error hizo que la transacción no se pueda confirmar. La única operación válida es deshacer toda la transacción.

Las diferencias entre @@TRANCOUNT y XACT_STATE es que @@TRANCOUNT permite saber si hay transacciones anidadas y XACT_STATE permite saber si la transacción es confirmable.

        SET XACT_ABORT ON;
        GO
        DELETE FROM Table1;
        GO
        BEGIN TRAN;
        INSERT INTO Table1 VALUES (1);
        INSERT INTO Table1 VALUES (1);
        INSERT INTO Table1 VALUES (2);
        COMMIT TRAN;
        GO
        PRINT XACT_STATE();
        PRINT @@TRANCOUNT;
        IF XACT_STATE() = 1
            COMMIT TRAN;
        IF XACT_STATE() = -1
            ROLLBACK TRAN;
        GO
        SELECT * FROM Table1;    
        

Como la línea 7 da un error y XACT_ABORT es ON, pasa lo siguiente:

  • Se aborta la ejecución del lote, no se ejecuta la línea 8.
  • Automáticamente se revierte la transacción explícita. Luego XACT_STATE y @@TRANCOUNT pasan a valer 0.

Si comentáramos la línea 7, XACT_STATE valdría 1 y se ejecutaría COMMIT TRAN.

La línea 16 hace ROLLBACK TRAN si por algún motivo la transacción se volvió no confirmable.

De nuevo, antes de seguir es necesario entender otro concepto, como maneja los timeouts de cliente SQL Server. Un timeout de cliente es como si pulsáramos “Cancel Executing Query” en SSMS, el botón Stop, vamos. Por ejemplo, creamos un procedimiento almacenado como el siguiente:

        CREATE PROCEDURE Foo
        AS
        PRINT 'Sergio';
        WAITFOR DELAY '00:00:10';
        PRINT 'panicoenlaxbox';
        END    
        

Si lo ejecutamos y antes de que pasen 10 segundos pulsamos Stop, sólo veremos la salida 'Sergio', es decir, se deja de ejecutar el script y no vemos 'panicoenlaxbox'.

¿Desde una aplicación cliente funcionará igual?

Lo primero es poder ver PRINT en el SQL Server Profiler, esto no es necesario para comprobar esto, pero me parece útil poder ver PRINT en el Profiler, lo he sacado de aquí. Creamos el procedimiento almacenado que hace la magia:

        CREATE PROCEDURE PrintTrace1
        @Text nvarchar(max) 
        AS
        BEGIN
        DECLARE @UserData BINARY(8000) = 0
        DECLARE @UserInfo NVARCHAR(256) = SUBSTRING(@Text,1,256)
        PRINT   @Text
        EXEC sp_trace_generateevent 82, @UserInfo, @UserData
        END    
        

Y modificamos el anterior procedimiento para lo use:

    ALTER PROCEDURE Foo
    AS
    EXEC PrintTrace1 'Sergio';
    WAITFOR DELAY '00:00:10';
    EXEC PrintTrace1 'panicoenlaxbox';    
    

Por último, cuando abramos el Profiler será necesario marcar el evento UserConfigurable:0 para ver la salida de PrintTrace1.

Ahora nuestro código cliente:

    using System;
    using System.Data;
    using System.Data.SqlClient;
    
    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    using (var connection = new SqlConnection(@"Server=(LocalDB)\MSSQLLocalDB;Database=Sergio;Trusted_Connection=True;"))
                    {
                        connection.Open();
                        using (var command = connection.CreateCommand())
                        {
                            command.CommandTimeout = 5;
                            command.CommandType = CommandType.StoredProcedure;
                            command.CommandText = "Foo";
                            command.ExecuteNonQuery();
                        }
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);                
                }
                Console.ReadKey();
            }
        }
    }    
    

Con CommandTimeout 5 y WAITFOR DELAY '00:00:10' el timeout está garantizado, devolviendo el error típico al cliente Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding, y confirmando que igualmente deja de ejecutar el resto de script en el servidor:

clip_image001[4]

¿Y todo esto del timeout por qué? Pues porque ahora vamos a ver TRY…CATCH de SQL Server, manejado estructurado de errores, buena cosa, pero era necesario tener los anteriores conceptos claro para poder hablar sobre ellos.

De TRY…CATCH el ejemplo típico es el siguiente, donde al igual que pasaba con XACT_ABORT ON, ahora cuando se sucede un error dentro del bloque TRY, la ejecución no continua, sino que salta al bloque CATCH.

        DELETE FROM Table1;
        GO
        BEGIN TRY
            BEGIN TRAN;
                INSERT INTO Table1 VALUES (1);
                -- RAISERROR with severity 11-19 will cause execution to jump to the CATCH block.  
                --RAISERROR ('Error raised in TRY block.', -- Message text.  
                --		   16, -- Severity.  
                --		   1 -- State.  
                --		   ); 
                INSERT INTO Table1 VALUES (1);
                INSERT INTO Table1 VALUES (2);
            COMMIT TRAN;
        END TRY
        BEGIN CATCH
                SELECT
                ERROR_NUMBER() AS ErrorNumber
                ,ERROR_SEVERITY() AS ErrorSeverity
                ,ERROR_STATE() AS ErrorState
                ,ERROR_PROCEDURE() AS ErrorProcedure
                ,ERROR_LINE() AS ErrorLine
                ,ERROR_MESSAGE() AS ErrorMessage;
            IF XACT_STATE() <> 0 
            BEGIN
                PRINT 'ROLLBACK TRAN';
                ROLLBACK TRAN;
            END
        END CATCH
        GO
        SELECT * FROM Table1;   
        

¿Cómo se llevará TRY…CATCH con un timeout?

Si hay un error de timeout no se ejecutará el CATCH, es decir, no creas que siempre que si hay un TRY…CATCH el CATCH siempre está asegurado.

Modificando el procedimiento anterior y cancelando la ejecución desde SSMS vemos este comportamiento.

    ALTER PROCEDURE Foo
    AS
        DELETE FROM Table1;
        BEGIN TRY
            BEGIN TRAN;
                INSERT INTO Table1 VALUES (1);
                EXEC PrintTrace1 'waitfor...';			
                WAITFOR DELAY '00:00:10';
                EXEC PrintTrace1 'continue...';
                INSERT INTO Table1 VALUES (1);
                INSERT INTO Table1 VALUES (2);
            COMMIT TRAN;
        END TRY
        BEGIN CATCH
            EXEC PrintTrace1 'catch...';
            IF XACT_STATE() <> 0 
            BEGIN
                PRINT 'ROLLBACK TRAN';
                ROLLBACK TRAN;
            END
        END CATCH 
    

Y ahora la pregunta es: Si no puedo garantizar la ejecución del bloque CATCH, ¿debería activar siempre XACT_ABORT para garantizar que la transacción explícita siempre se rechazara automáticamente? Pues parece que sí, porque quien defiende XACT_ABORT lo hace porque si no está activo, un timeout de cliente podría dejar la conexión con recursos bloqueados hasta que la transacción se cancele o la conexión se cierre, y asumiendo que hay pool de conexiones, un mal código de cliente podría arruinar el servidor, incluso en un comentario del mismo post queda clara la jugada.

Por otro lado, si activamos XACT_ABORT y además hay un TRY…CATCH, el CATCH seguirá ejecutándose, pero XACT_STATE valdrá -1 (la única operación válida es deshacer la transacción) y además @@TRANCOUNT seguirá valiendo lo que valía, es decir, un -1 en XACT_STATE no rechaza automáticamente la transacción explícita.

Y antes de llegar a nuestro snippet definitivo para la gestión de errores, hablemos de transacciones anidadas en SQL Server. Poder se puede:

        BEGIN TRAN
            BEGIN TRAN
            PRINT @@TRANCOUNT --2
            COMMIT TRAN
            PRINT @@TRANCOUNT --1
        COMMIT TRAN
        PRINT @@TRANCOUNT --0    
        

Además, aparece el concepto de salvar una transacción que lo que permite es deshacer partes concretas de una transacción. Cabe mencionar que SAVE TRAN no incrementa @@TRANCOUNT y, por ende, ROLLBACK TRAN <nombre> tampoco lo decrementa.

        BEGIN TRAN
            BEGIN TRAN
            PRINT @@TRANCOUNT --2
            SAVE TRAN st1
                --Do something that can be rolled back
                PRINT @@TRANCOUNT --2
                ROLLBACK TRAN st1
            COMMIT TRAN
            PRINT @@TRANCOUNT --1
        COMMIT TRAN
        PRINT @@TRANCOUNT --0        
        

Un ROLLBACK TRAN (sin nombre) deshace todas las transacciones (anidadas también si las hubiera) y decrementa @@TRANCOUNT a 0. ROLLBACK TRAN <nombre> sólo es válido si <nombre> es un SAVE TRAN o un BEGIN TRAN <nombre> siendo esa transacción la más externa (no siendo anidada).

Un COMMIT TRAN (sin nombre) confirma la transacción actual según su nivel de indentación, aunque es válido un COMMIT TRAN <nombre> refiriéndose tanto a una transacción anidada como a una externa.

Como resumen, con transacciones anidadas podemos o bien rechazar todas las transacciones (ROLLBACK TRAN o ROLLBACK TRAN <nombre_de_la_más_externa>) o bien rechazar partes de una transacción anidada (SAVE TRAN <nombre> y ROLLBACK <nombre>).

Y en este momento, es cuando vemos la plantilla de un procedimiento almacenado que he sacado de este post donde le agregamos XACT_ABORT ON para que un timeout de cliente no nos de guerra.

        CREATE PROCEDURE [ProcedureName]
        AS
        BEGIN
            SET NOCOUNT ON;
            SET XACT_ABORT ON;
            DECLARE @trancount INT;
            SET @trancount = @@TRANCOUNT;
            BEGIN TRY
                IF @trancount = 0
                    BEGIN TRANSACTION;
                ELSE
                    SAVE TRANSACTION ProcedureName;
                -- Do something...	
                
                IF @trancount = 0	
                    COMMIT;
            END TRY
            BEGIN CATCH
                DECLARE @errorNumber INT, @message NVARCHAR(4000), @xact_state INT;
                SELECT @errorNumber = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xact_state = XACT_STATE();
                IF @xact_state = -1
                    ROLLBACK;
                IF @xact_state = 1 AND @trancount = 0
                    ROLLBACK;
                IF @xact_state = 1 AND @trancount > 0
                    ROLLBACK TRANSACTION ProcedureName;
        
                RAISERROR('ProcedureName: %d: %s', 16, 1, @errorNumber, @message) ;
            END CATCH
        END   
        

Ahora sí, podemos escribir un “poco” de lógica de negocio en T-SQL con una estrategia clara de gestión de errores.

viernes, 3 de noviembre de 2017

Crear e inicializar un contexto en EF Core

En EF 6.x, crear e inicializar un contexto tiene magia. Magia en el sentido de que Entity Framework puede decidir automáticamente dónde y con que nombre crear la base de datos. Según el constructor elegido de DbContext, si existe o no una cadena de conexión o un inicializador en un fichero .config, incluso saber que prevalece lo escrito en un fichero .config sobre la configuración por código… en fin, magia, para lo bueno y para lo malo.

En EF Core (actualmente 2.0) se ha prescindido de esa magia, ahora tenemos que ser explícitos sobre la configuración del contexto. Esto parece una buena idea, no creo que haya mucho debate, pero por el contrario arrancar un proyecto requiere saber más sobre como inicializar y trabajar con EF.

Asumiendo que estamos trabajando con una aplicación de consola, la forma recomendada de agregar EF Core a un proyecto es instalar un proveedor.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Después, habrá que agregar el CLI de EF Core y/o los comandos de PowerShell para el PMC (Power Management Console).

Para agregar el CLI de EF Core hay que editar el .csproj a mano.

<ItemGroup>
  <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>

Para agregar los comandos al PMC, podemos hacerlo agregando un paquete con normalidad.

dotnet add package Microsoft.EntityFrameworkCore.Tools

Nuestro .csproj quedaría así:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
  </ItemGroup>
  
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  </ItemGroup>

</Project>
Si estamos en una aplicación de consola también será necesario agregar una referencia a Microsoft.EntityFrameworkCore.Design

Ahora ya estamos preparados para trabajar con EF Core, así que creamos un contexto cualquiera:

class ShopContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
}

internal class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
}

Y al crear nuestra primera migración, la primera en la frente:

dotnet ef migrations add Initial

No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.

El error nos está diciendo que el contexto no está “configurado” con ningún proveedor de base de datos, que no sabe a que bd atacar… que o bien usemos DbContext.OnConfiguring o bien AddDbContext sobre el “proveedor de servicios de la aplicación (el contenedor de dependencias)” y que si usamos este último método no olvidemos crear un constructor que acepte DbContextOptions y que llame a la base.

La primera solución pasa por sobrescribir OnConfiguring

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        optionsBuilder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;");
    }
    base.OnConfiguring(optionsBuilder);
}

“IsConfigured” lo que viene a decir es “si no me han configurado ya, entonces me configuro”, luego veremos que será posible configurar el contexto desde fuera, porque si no lo hiciéramos y quisiéramos cambiar de proveedor o de cadena de conexión tendríamos que meter más código aquí, pero, en cualquier caso, esté o no configurado el contexto está bien saber que tenemos una última oportunidad para hacer lo que queramos con la configuración. Además, hay que tener en cuenta que OnConfiguring se llama siempre para cada instancia creada del contexto.

Ahora ya funciona crear una migración con las herramientas cliente y además podríamos usar el contexto en nuestro código:

            
using (var context = new ShopContext())
{
}

Sin embargo, parece mejor que el contexto sea configurado desde fuera, será más versátil y no habremos hardcodeado nada, probemos la segunda opción.

using System;
using Microsoft.EntityFrameworkCore;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var options = new DbContextOptionsBuilder()
                .UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
                .Options;

            using (var context = new ShopContext(options))
            {
                context.Database.Migrate();
            }
        }
    }


    class ShopContext : DbContext
    {
        public ShopContext(DbContextOptions options) : base(options)
        {
        }
        public DbSet<Order> Orders { get; set; }
    }

    internal class Order
    {
        public int Id { get; set; }
        public DateTime OrderDate { get; set; }
    }
}

Ahora somos capaces de inyectar la configuración al contexto (luego IsConfigured vale true en OnConfiguring).

Tanto DbContextOptions como DbContextOptionsBuilder tiene su versión genérica, que cuando trabajemos más adelante con DI nos servirá si tenemos más de un contexto en nuestra aplicación.

            
var options = new DbContextOptionsBuilder<ShopContext>()
    .UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
    .Options;

public ShopContext(DbContextOptions<ShopContext> options) : base(options)
{
}

Sin embargo, lo que deja de funcionar ahora es el CLI, porque, aunque encuentra el contexto, no tiene un constructor sin parámetros y por ende no se puede instanciar.

Unable to create an object of type 'ShopContext'. Add an implementation of 'IDesignTimeDbContextFactory ' to the project, or see https://go.microsoft.com/fwlink/?linkid=851728 for additional patterns supported at design time.

IDesignTimeDbContextFactory ayuda a las herramientas de cliente de EF a crear un contexto si el mismo no tiene un constructor público sin parámetros.

    
class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ShopContext>
{
    public ShopContext CreateDbContext(string[] args)
    {
        var options = new DbContextOptionsBuilder<ShopContext>()
            .UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
            .Options;
        return new ShopContext(options);
    }
} 

Esta clase sólo será usada por las herramientas cliente, esto es importante, es tiempo de diseño, nada tiene que ver con tiempo de ejecución.

Ahora ya funcionan las migraciones.

Aunque con lo visto hasta aquí se podría funcionar, no estamos haciendo uso del contenedor de dependencias.

Para hacerlo en una aplicación de consola.

        
static void Main(string[] args)
{
    var options = new DbContextOptionsBuilder()
        .UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;")
        .Options;

    IServiceCollection services = new ServiceCollection();
    services
        .AddSingleton(options)
        .AddScoped<ShopContext>();

    ServiceProvider serviceProvider = services.BuildServiceProvider();

        using (var context = serviceProvider.GetService<ShopContext>())
    {
        context.Database.EnsureCreated();
    }
}

Y si queremos simplificar el registro de servicios, tenemos el método de extensión AddDbContext, que registra el contexto como Scoped y nos da una lamba que se llamará la primera vez que se resuelva el contexto y que devolverá un DbContextOptions, que se registrará como Singleton.

        
static void Main(string[] args)
{
    IServiceCollection services = new ServiceCollection();
    services.AddDbContext<ShopContext>(builder =>
    {
        builder.UseSqlServer(@"Server=(LocalDB)\MSSQLLocalDB;Database=Example;Trusted_Connection=True;");
    });

    ServiceProvider serviceProvider = services.BuildServiceProvider();

    using (var context = serviceProvider.GetService<ShopContext>())
    {
        context.Database.EnsureCreated();
    }
}

En realidad, AddDbContext registra muchos más servicios con su llamada a AddCoreServices

Si por el contrario nuestra aplicación es web la cosa cambia.

De serie (y asumiendo que estamos usando Visual Studio) viene con el paquete Microsoft.AspNetCore.All Microsoft.AspNetCore.App que ya incluye los paquetes Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools y Microsoft.EntityFrameworkCore.Design, es decir, para tener lo mismo que en el ejemplo anterior sólo tendríamos que agregar el CLI de EF (el de PMC ya viene de serie) y también el CLI de EF (en ASP.NET Core 2.1, dotnet ef es un comando de serie)

Además, ahora estas herramientas encontrarán el método BuildWebHost

    
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }

Es importante saber que se busca el método BuildWebHost con ese nombre exacto, cualquier otro nombre no valdrá.

Si las herramientas cliente encuentran BuildWebHost lo ejecutarán y también ejecutarán Startup.Configure y Startup.ConfigureServices (porque aquí es donde habremos llamado a AddDbContext y registrado los servicios).

La consecuencia directa de esto es que el código de inicialización (migración, seed, etc) que tuviéramos en Startup.Configure ya no debería estar allí. Se recomienda moverlo al método Main.

public static void Main(string[] args)
{
    // Construir IWebHost
    var host = BuildWebHost(args);

    // Inicialización
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        try
        {
            var context = services.GetRequiredService<ShopContext>();
            // Hacer algo útil...
        }
        catch (Exception ex)
        {
            ILogger<Program> logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred while seeding the database.");
        }
    }

    // Ejecutar IWebHost
    host.Run();
}

En este gist de Unai Zorrila hay un ejemplo más molón

Como dato curioso (o no tan curioso y sí peligroso), si pasara que tenemos tanto BuildWebHost como IDesignTimeDbContextFactory, se ejecutarían ambos, aunque prevalecería IDesignTimeDbContextFactory.

Y para terminar, además del método extensor AddDbContext, en EF Core 2 han metido el nuevo método de extensión AddDbContextPool que usa un pool de contextos para mejorar el rendimiento, aunque tiene ciertas limitaciones.

¡Un saludo!

martes, 19 de septiembre de 2017

Merge vs Rebase

Cuando se trabaja en equipo todo el mundo acepta con naturalidad la necesidad de un coding standard o similar para buscar la mayor legibilidad, consistencia y mantenibilidad del código. Igualmente, y en relación al control de código fuente, debería haber un consenso sobre que flujo de trabajo y tipo de estrategia de merge seguir.

Trabajando con git, tarde o temprano hay que elegir, merge o rebase, o para ser más exactos, estrategia de merge de tipo fast-forward o tipo recursive.

Cualquier opción es válida y ambas tienen ventajas y desventajas.

En mi caso, intentaré explicar que es merge y rebase.

TL;DR

Merge con estrategia recursive hace explícita la integración de ramas y además mantiene contiguos todos los commits de una característica, por el contrario, dificulta la legibilidad del repositorio y podemos acabar fácilmente con un guitar hero http://devhumor.com/media/i-fucked-up-git-so-bad-it-turned-into-guitar-hero

Por otro lado, rebase mantiene una historia lineal del proyecto, pero está sujeto a más situaciones comprometidas, es más propenso a meter la pata. La regla de oro es no hacer rebase sobre commits públicos, o al menos no hacerlo sobre commits sobre los que cualquier miembro del equipo haya basado su trabajo. Además, con rebase perdemos la trazabilidad de cuando se integró una rama.

En mi caso apuesto por merge, forzando incluso con --no-ff para evitar una estrategia fast-forward y así ser totalmente explícito de cuando se integró una rama. Sin embargo, para descargar cambios del repositorio remoto me parece adecuado usar git pull --rebase para evitar ciertos commits extra de merge que no aportan valor al repositorio.

Por último, el rebase de tipo clean-up es gratis en local y parece una buena práctica ¿Por qué no usarlo?

Para los ejemplos, asumo que no tienes guardado nada de valor en el directorio C:\Temp y deberían poderse seguir de principio a final si no te cambias de directorio. Además, y aunque normalmente los ejemplos se ejecutan por comandos, también mencionaré de vez en cuando a SourceTree porque es el cliente de git que uso habitualmente.

Para hacer nuestro primer merge:

mkdir C:\Temp
cd C:\Temp
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git checkout -b develop
echo F3 > F3.txt
git add .
git commit -m "C3"
git checkout master
echo F4 > F4.txt
git add .
git commit -m "C4"

Aquí la historia se ha bifurcado porque se han hecho commits en distintas ramas desde un mismo commit base.

clip_image001

Para integrar los cambios de develop en master tenemos 2 opciones: merge o rebase.

Los mantras oficiales (grábatelos a fuego) son:

  • merge “me voy a y cojo de”
  • rebase “estoy en y rebaso a”

Por ahora veremos merge y más adelante rebase

git checkout master
git merge develop                
clip_image002

Ahora el commit de merge tiene 2 padres. El primero es el commit de la rama donde se estaba (master) y el segundo es el commit de la rama que se integró (C3 de develop). El orden es importante porque después (y si fuera necesario) podríamos ver sólo los commits de una rama (excluyendo los que vinieron por integración de otras ramas) con git log --oneline --first-parent. Otra opción muy socorrida es git log --oneline --no-merge que muestra el log sin ningún commit de merge.

Para probar otro tipo de merge, tenemos que volver a la situación anterior, como si no hubiéramos hecho el merge. Para ello bastaría con retroceder la rama actual un commit con git reset --hard master^, el problema es que en Windows hay que poner dos acentos circunflejos porque hay que escapar ciertos caracteres https://stackoverflow.com/questions/9600549/using-a-caret-when-using-git-for-windows-in-powershell, así que en vez de poner git reset --hard master^ será mejor poner git reset --hard master~ que para el caso es lo mismo.

git reset --hard master~
git checkout develop
git merge master
clip_image004

Como vemos, el cambio más reseñable es que el mensaje por defecto del commit de merge es distinto.

  • Merge branch ‘<rama-mergeada>’ si se está integrando en master
  • Merge branch ‘<rama-mergeada>’ into <rama> si se está integrando en cualquier otra rama

Si estamos trabajando solos, esos serían los dos tipos de commits de merge que deberíamos encontrarnos normalmente.

Podría haber un tercero, si un merge que podría haber sido resuelto con la estrategia “fast-forward” (esto es, simplemente avanzar el puntero sin crear ningún commit de merge) lo forzamos para que sí haya un commit de merge con --no-ff

cd c:\Temp
rm -rf *
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git checkout -b develop
echo F3 > F3.txt
git add .
git commit -m "C3"
git checkout master                     
clip_image005
git merge develop

clip_image006

clip_image007

Si ahora hacemos esto otro

git reset --hard master~
git merge develop --no-ff                    

clip_image008

clip_image009

Está claro, si la estrategia de merge es recursive será cuando veamos un commit de merge, si es fast-forward no.

--no-ff desde Source Tree equivale a marcar la casilla “Create a new commit even if fast-forward is possible”

clip_image010

Sin embargo, si trabajando en equipo, pueden aparecer otros commits de merge cuando nos traigamos los cambios del remoto con git pull

Para trabajar con un remoto y no tener que crearlo en github, bitbucket o similar, podemos usar un repositorio bare y así todo queda en casa.

cd C:\Temp
rm -rf *
mkdir central
cd central
git init --bare
cd ..
mkdir example1
cd example1
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git remote add origin C:\Temp\central
git push -u origin master
cd ..
git clone C:\Temp\central example2                    

En este momento ya tenemos un repositorio remoto y 2 repositorios locales, ¡todo listo!

cd example1
echo F3 > F3.txt
git add .
git commit -m "C3"
git push                    
clip_image011
cd ..
cd example2
echo F4 > F4.txt
git add .
git commit -m "C4"                      
clip_image012

Si hacemos un git push desde example2 fallará porque no estamos al día, hay cambios en el remoto que no nos hemos bajado.

git fetch                     
clip_image013

Para ver git pull me parece interesante comentar la ventana de SourceTree

clip_image015

Si no está marcado “Commit merged changes immediately”, sería un git pull --no-commit, luego tendríamos los cambios en el working copy pero no se hará el commit.

clip_image016

“Create a new commit even if fast-forward is possible” indicará si agregar o no el modificador --no-ff

“Rebase instead of merge (WARNING: make sure you haven’t pushed your changes)” ejecutará git pull --rebase

En caso de no estar marcada ninguna opción, simplemente será un git pull

clip_image017

La diferencia más notable en el commit de merge es que ahora el mensaje predeterminado es “Merge branch ‘<rama>’ of ‘dirección_remoto’

git pull es lo mismo que hacer git fetch + git merge origin/<current-branch>, la única diferencia sería el mensaje predeterminado del commit de merge, que ahora sería “Merge remote-tracking branch 'origin/<rama>'”

clip_image018

Y si hubiéramos lanzado git pull --rebase, nuestro commit C4 se aplica en lo alto de la rama evitando un commit extra de merge

clip_image019

Y llegados a este punto ya tenemos claro de dónde vienen los distintos commits de merge, cuando se pueden producir y como reconocerlos atendiendo a sus mensajes predeterminados.

Antes de meternos con rebase es importante entender el concepto de reescribir la historia y porqué tiene mucho peligro.

Seguramente haya muchos más comandos que reescriban la historia, pero los más habituales son git commit --amend y git rebase

cd C:\Temp
rm -rf *
mkdir example
cd example
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"                    
clip_image020

Ahora nos damos cuenta de que en el commit C2 el texto del fichero F2.txt no es correcto o no nos gusta el mensaje del commit o nos hemos dejado algún fichero en el index o sin guardar https://marketplace.visualstudio.com/items?itemName=PaulCBetts.SaveAllTheTime y queremos que sea parte del commit y, sea como sea, no queremos hacer un nuevo commit sino reemplazar el último. Esto se consigue con --amend que combina el staging area/index con el contenido del último commit y reemplaza el último commit. Con --no-edit simplemente decimos que nos vale el mensaje del último commit.

echo F2_upated > F2.txt
git add . 
git commit --amend --no-edit                    

El identificador del commit ha cambiado, hemos reescrito la historia.

clip_image021

En SourceTree también podemos hacer --amend

clip_image022

Como decía, el tema está en que hemos reescrito la historia, antes el commit era a59987c y ahora es 3d41599 (de nuevo, será otro en tu equipo), es decir, son 2 commits completamente diferentes a ojos del repositorio, luego si ya eran públicos (estaban subidos al remoto), cualquier otro commit que los estuviera referenciado ya no los va a encontrar y habrá lío garantizado…

Vamos a reproducir un lío gordo y así lo vemos.

cd C:\Temp
rm -rf *
mkdir central
cd central
git init --bare
cd ..
mkdir example1
cd example1
echo F1 > F1.txt
git init
git add .
git commit -m "C1"
echo F2 > F2.txt
git add .
git commit -m "C2"
git remote add origin C:\Temp\central
git push -u origin master
cd ..
git clone C:\Temp\central example2
cd example2
echo F3 > F3.txt
git add .
git commit -m "C3"                     

Aquí estamos a nivel, todo bien.

En example1

clip_image023

En example2, C3 tiene como padre el commit C2

clip_image024

Pero ahora el señor de example1 va a reescribir la historia haciendo un commit --amend y entonces C2 ya no será el mismo commit sino algún otro con un identificador distinto

cd ..
cd example1
echo F2_1 > F2_1.txt
git add .
git commit --amend -m "C2_updated”                    

Y en example1 tenemos lo siguiente:

clip_image025

¡Uy, que feo!, aparece C2_updated (a quien apunta master) pero sigue estando C2 (a quien apunta origin/master). De hecho, C2 tiene el mismo identificador, sigue estando ahí porque está en el remoto (es público).

Lógicamente tampoco el señor de example1 puede hacer un git push, no está al día, origin/master ya no está apuntado a master.

¿Qué opciones tiene example1? Pues hacer merge de origin/master en master

git merge origin/master master

Que podría o no darle un conflicto en función de que como haya sido su --amend, en nuestro ejemplo no dará conflicto, hemos añadido un fichero.

clip_image026

Estoy seguro de que el señor de example1 no quería este commit de merge… pero bueno, igualmente hace un push

                    git push
            

¿Y cómo queda el señor de example2?

cd ..
cd example2
git fetch                

Pues directamente le han hecho el lío, lo que él pensaba era un valor seguro, un commit público que estaba en el remoto grabado a piedra, pues ya no lo es tanto, sigue estando ahí pero ya no es lo mismo… se ha roto la regla de oro, no reescribir la historia en commits públicos

clip_image027
git pull
clip_image028

Y ahora ya puede hacer

git push

Gracias, señor de example1, acabas de afear la historia del repositorio y además has agregado una complejidad que me acordaré de ti por siempre… por lo menos no has hecho un git push –force, en fin…

La idea es que reescribir commits que son públicos es una pésima idea y fuente de problemas, no se hace y punto, es otro mantra.

Y por fin llegamos al rebase, que mola mucho… pero reescribe la historia, así que cuidado.

Rebasar es “mover” commits a un nuevo commit base.

Para integrar cambios, se puede hacer con merge o con rebase + merge, que siempre será fast-forward… y es aquí donde está el debate ¿merge o rebase? Según a quién preguntes te dirá una cosa u otra

Se hace rebase por alguno de estos motivos:

  • Mantener una historia “lineal” del proyecto para mejorar la legibilidad
    • Aunque se pierda trazabilidad en la integración de ramas porque no hay commits de merge
  • Hacer un clean-up de mi historia local con un rebase interactivo

Hacer un clean-up de mi historia local con un rebase interactivo.

Hacer un clean-up no parece que suscite debate (siempre y cuando se haga en local, en remoto no se hace, recuerda que reescribe la historia, el mantra…). Lo hacemos porque en local podríamos haber hecho n commits (porque nos apeteció, es tu repo, ahí no manda nadie) pero cuando queremos compartir los cambios con el resto (subir al remoto) queremos pasarle un poco la mopa para que quede todo bonito y apañado.

Por ejemplo, estoy trabajando en mi local con aparente desgana:

cd C:\Temp
rm -rf *
mkdir example
cd example
git init
echo example > example.txt
git add .
git commit -m "Initial commit"
echo F2 > F2.txt
git add .
git commit -m "C2"
echo F3 > F3.txt
git add .
git commit -m "C3"
echo F4 > F4.txt
git add .
git commit -m "C4"
echo F_fake > F_fake.txt
git add .
git commit -m "Fake"
echo F5 > F5.txt
git add .
git commit -m "C5"
echo F1 > F1.txt
git add .
git commit -m "C1"                    
clip_image029

Sinceramente, no voy a subir eso al remoto para que lo vean mis compañeros, los commits están desordenados, tengo un commit fake y además no me he preocupado por los comentarios, ¡ni yo sé que he incluido en cada commit1

Se podría hacer el rebase interactivo por comandos, pero con franqueza, una herramienta como SourceTree nos facilitará la vida.

clip_image030

Y pasamos de

clip_image031

A esto otro (haciendo squash, ordenando, eliminado y cambiando mensajes de commits)

clip_image032

clip_image033

Esto es otra cosa, ¡ya puedo subir mis cambios!

Si quieres hacerlo interactivo por consola, puedes hacerlo con git rebase [-i] <commit_base>, pero se va a abrir casi seguro VIM y ahí te apañes tú… Si por ejemplo “Initial commit” tuviera el identificador c7f0016

git rebase -i c7f0016
            
clip_image035

El otro sabor de rebase (el que entra en debate con merge) es el “no interactivo”, el que se rige por el mantra “estoy en y rebaso a”. Lo veremos con un ejemplo típico de feature branch.

cd C:\Temp
rm -rf *
mkdir example
cd example
git init
echo example > example.txt
git add .
git commit -m "Initial commit"
echo A1 > A1.txt
git add .
git commit -m "A1"
echo A2 > A2.txt
git add .
git commit -m "A2"
git checkout -b features/B
echo B1 > B1.txt
git add .
git commit -m "B1"
echo B2 > B2.txt
git add .
git commit -m "B2"
echo B3 > B3.txt
git add .
git commit -m "B3"
git checkout master
echo A3 > A3.txt
git add .
git commit -m "A3"
echo A4 > A4.txt
git add .
git commit -m "A4"
git checkout features/B                                  
clip_image036
git rebase master 

clip_image037

clip_image038

El rebase ha cogido todos los commits de features/B y los ha puesto a continuación del último commit de master, todo lineal, muy bonito… pero ha reescrito la historia de los commits de features/B

Si estás trabajando sólo tú en esa rama en este momento (y no hay ningún compañero pendiente de subir cambios al repositorio en esa rama, y tampoco nadie creó una rama a partir de un commit de esa rama), no hay problema, si no lío…

Desde Source Tree, hubieramos hecho

clip_image039

clip_image040

Por cierto, si el rebase da conflictos, los resolvemos y después git rebase --continue o git rebase --abort

Y si queremos deshacer el rebase podemos usar git reset --hard ORIG_HEAD https://stackoverflow.com/a/692763

Y después de este tipo de rebase tenemos que hacer un merge, que será siempre fast-forward

git checkout master
git merge features/B
clip_image041

Y nada más, espero que te sirva este post porque, lo que es seguro, es que yo sí volveré a él cada vez que tenga que hablar sobre este tema, seguramente para refrescar el porqué de las cosas o bien para actualizarlo porque he descubierto que algo no funcionaba como pensaba.

Un saludo!

martes, 30 de mayo de 2017

JSON en SQL Server 2016

Seguro que ya estabas enterado, pero en mi caso ha sido recientemente cuando he descubierto que a partir de SQL Server 2016 se puede trabajar con JSON.

Aunque hay una excelente documentación al respecto en JSON Data (SQL Server), de una forma resumida y con ejemplos que pueda recordar fácilmente, me gustaría contarte que posibilidades tenemos para trabajar con JSON en SQL Server.

Lo primero es que no hay un tipo json, en realidad trabajaremos con nvarchar y todo la magia ocurrirá a través de nuevas clausulas y funciones.

Para organizar el post, voy a plantear un escenario donde, primero importaremos datos desde un fichero .json, a continuación formatearemos en JSON la salida de una consulta SQL, para después hacer consultas SQL sobre una columna que guarda JSON y acabar, finalmente, con un consejo sobre índices para mejorar el rendimiento.

El script SQL necesario para todas las pruebas es el siguiente:

CREATE TABLE [dbo].[OrderLines](
	[Id] [int] NOT NULL,
	[Units] [int] NOT NULL,
	[Price] [decimal](18, 2) NOT NULL,
	[ProductId] [int] NOT NULL,
	[OrderId] [int] NOT NULL
 CONSTRAINT [PK_dbo.OrderLines] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
CREATE TABLE [dbo].[Orders](
	[Id] [int] NOT NULL,
	[CreatedDate] [datetime] NOT NULL,
	[Comment] [nvarchar](250) NULL
 CONSTRAINT [PK_dbo.Orders] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
CREATE TABLE [dbo].[Products](
	[Id] [int] NOT NULL,
	[Name] [nvarchar](250) NULL
 CONSTRAINT [PK_dbo.Products] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))
GO
ALTER TABLE [dbo].[OrderLines]  WITH CHECK ADD  CONSTRAINT [FK_dbo.OrderLines_dbo.Orders_OrderId] FOREIGN KEY([OrderId])
REFERENCES [dbo].[Orders] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[OrderLines] CHECK CONSTRAINT [FK_dbo.OrderLines_dbo.Orders_OrderId]
GO
ALTER TABLE [dbo].[OrderLines]  WITH CHECK ADD  CONSTRAINT [FK_dbo.OrderLines_dbo.Products_ProductId] FOREIGN KEY([ProductId])
REFERENCES [dbo].[Products] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[OrderLines] CHECK CONSTRAINT [FK_dbo.OrderLines_dbo.Products_ProductId]

Para importar datos desde un fichero .json podemos usar la función OPENROWSET con el parámetro SINGLE_CLOB, que lee y devuelve su contenido como una única fila y columna BulkColumn de tipo nvarchar(max). Además, tendremos que usar la función OPENJSON que convierte JSON en filas y columnas.

Nuestro fichero .json será como sigue

[
  {
    "Id": 1,
    "CreatedDate": "2017-05-29T00:00:00.000",
    "OrderLines": [
      {
        "Id": 1,
        "Units": 1,
        "Price": 1.25,
        "Product": {
          "Id": 1,
          "Name": "Product 1"
        }
      },
      {
        "Id": 2,
        "Units": 2,
        "Price": 2.5,
        "Product": {
          "Id": 2,
          "Name": "Product 2"
        }
      }
    ]
  },
  {
    "Id": 2,
    "CreatedDate": "2017-05-29T00:00:00.000",
    "Comment": "A brief but useful comment",
    "OrderLines": [
      {
        "Id": 3,
        "Units": 3,
        "Price": 3.75,
        "Product": {
          "Id": 1,
          "Name": "Product 1"
        }
      }
    ]
  }
]

Ahora podemos usar OPENROWSET y OPENJSON

SELECT BulkColumn, [key], [value], [type]
FROM OPENROWSET (BULK 'C:\panicoenlaxbox\data.json', SINGLE_CLOB) AS T
CROSS APPLY OPENJSON(BulkColumn)

OPENROWSET devuelve la columna BulkColumn, OPENJSON devuelve las columnas key, value y type.

image

Si en fichero .json tuviera un sólo objeto en vez de un array, la salida nos ayudaría a entender mejor como funciona OPENJSON

{
  "Id": 1,
  "CreatedDate": "2017-05-29T00:00:00.000",
  "OrderLines": [
    {
      "Id": 1,
      "Units": 1,
      "Price": 1.25,
      "Product": {
        "Id": 1,
        "Name": "Product 1"
      }
    },
    {
      "Id": 2,
      "Units": 2,
      "Price": 2.5,
      "Product": {
        "Id": 2,
        "Name": "Product 2"
      }
    }
  ]
}

image

OPENJSON tiene la clausula WITH con la que podemos, de forma explícita, establecer la estructura del resultado devuelto. Por ejemplo, para conseguir un conjunto de filas y columnas sobre la que poder trabajar directamente, ejecutaríamos la siguiente consulta

SELECT
	Orders.Id AS OrderId
   ,Orders.CreatedDate
   ,Orders.Comment
   ,OrderLines.Id AS OrderLineId
   ,OrderLines.Units
   ,OrderLines.Price
   ,Product.Id AS ProductId
   ,Product.[Name] AS ProductName
INTO #Table1
FROM OPENROWSET(BULK 'C:\panicoenlaxbox\data.json', SINGLE_CLOB) AS j
CROSS APPLY OPENJSON(BulkColumn)
WITH (
	Id INT,
	CreatedDate DATETIME '$.CreatedDate',
	Comment NVARCHAR(MAX),
	OrderLines NVARCHAR(MAX) AS JSON
) AS Orders
CROSS APPLY OPENJSON(Orders.OrderLines)
WITH (
	Id INT,
	Units INT,
	Price DECIMAL(18, 2),
	Product NVARCHAR(MAX) AS JSON
) AS OrderLines
CROSS APPLY OPENJSON(OrderLines.Product)
WITH (
	Id INT,
	[Name] NVARCHAR(MAX)
) AS Product    

image

Ahora ya sí podemos insertar estos datos leídos del fichero .json en nuestras tablas

INSERT INTO Products SELECT DISTINCT ProductId, ProductName FROM #table1;
INSERT INTO Orders SELECT DISTINCT OrderId, CreatedDate, Comment FROM #table1;
INSERT INTO OrderLines SELECT DISTINCT OrderLineId, Units, Price, ProductId, OrderId FROM #table1;    

Si hablamos ahora de formatear consultas, tendremos que usar la clausula FOR JSON

SELECT * FROM Orders O
INNER JOIN OrderLines OL ON OL.OrderId = O.Id
INNER JOIN Products P ON OL.ProductId = P.Id
FOR JSON AUTO

Que devuelve

    [{
    "Id": 1,
    "CreatedDate": "2017-05-29T00:00:00",
    "OL": [{
        "Id": 1,
        "Units": 1,
        "Price": 1.25,
        "ProductId": 1,
        "OrderId": 1,
        "P": [{
            "Id": 1,
            "Name": "Product 1"
        }]
    }, {
        "Id": 2,
        "Units": 2,
        "Price": 2.50,
        "ProductId": 2,
        "OrderId": 1,
        "P": [{
            "Id": 2,
            "Name": "Product 2"
        }]
    }]
}, {
    "Id": 2,
    "CreatedDate": "2017-05-29T00:00:00",
    "Comment": "A brief but useful comment",
    "OL": [{
        "Id": 3,
        "Units": 3,
        "Price": 3.75,
        "ProductId": 1,
        "OrderId": 2,
        "P": [{
            "Id": 1,
            "Name": "Product 1"
        }]
    }]
}]

Como probablemente este resultado no nos satisfaga, tendremos que tomar el control con FOR JSON PATH. Por ejemplo, la siguiente consulta devuelve exactamente lo mismo que tiene el fichero .json que usamos al comienzo para importar los datos

SELECT
	O.Id
   ,O.CreatedDate
   ,O.Comment
   ,(SELECT
			OL.Id
		   ,OL.Units
		   ,OL.Price
		   ,OL.ProductId AS 'Product.Id'
		   ,P.[Name] AS 'Product.Name'
		FROM OrderLines OL
		INNER JOIN Products P
			ON OL.ProductId = P.Id
		WHERE OL.OrderId = O.Id
		FOR JSON PATH)
	AS OrderLines
FROM Orders O
FOR JSON PATH

En cuanto a que podemos hacer para consultar datos JSON almacenados en una columna, encontramos varias funciones:

  • ISJSON
  • JSON_VALUE
  • JSON_QUERY
  • JSON_MODIFY

ISJSON valida que el texto es JSON válido

JSON_VALUE extrae un valor desde JSON

JSON_QUERY extrae como texto, un objeto u array desde JSON

JSON_MODIFY permite modificar JSON y devuelve el resultado

Lo más sencillo será agregar una nueva columna para poder jugar con ella

ALTER TABLE Orders
ADD SecurityContext NVARCHAR(MAX);
GO
UPDATE Orders SET SecurityContext = '{"Enabled":true,"Roles":["Salesman","Customer"],"Worflows":[{"Name":"Approval","Priority":1},{"Name":"Rejection","Priority":2}]}'
WHERE Id = 1;
UPDATE Orders SET SecurityContext = '{"Enabled":false}'
WHERE Id = 2;    

Ahora podemos ejecutar las siguientes consultas

--ISJSON valida si es JSON
SELECT ISJSON(SecurityContext) FROM Orders;

--JSON_VALUE extrae un valor desde JSON
--'$.Roles[1]' y 'lax $.Roles[1]' son lo mismo, por defecto es lax
SELECT JSON_VALUE(SecurityContext, '$.Roles[1]') FROM Orders;

--Con strict tendremos una excepción porque el segundo registro no tiene valor
--Property cannot be found on the specified JSON path.
--SELECT JSON_VALUE(SecurityContext, 'strict $.Roles[1]') FROM Orders;

--JSON_QUERY devuelve un objeto o un array
SELECT JSON_QUERY(SecurityContext, '$.Worflows') FROM Orders;
--[{"Name":"Approval","Priority":1},{"Name":"Rejection","Priority":2}]

DECLARE @json NVARCHAR(MAX)
SELECT @json = SecurityContext FROM Orders WHERE Id = 1;
--Modificar una propiedad
SELECT JSON_VALUE(JSON_MODIFY(@json, '$.Enabled', 'false'), '$.Enabled');
--Modificar un elemento de un array
SELECT JSON_QUERY(JSON_MODIFY(@json, '$.Roles[1]', 'Administrador'), '$.Roles');
--["Salesman","Administrador"]

--Agregar un elemento a un array
SELECT JSON_QUERY(JSON_MODIFY(@json, 'append $.Roles', 'Agent'), '$.Roles');
--["Salesman","Customer","Agent"]

Por último, sólo mencionar como podemos crear un índice para que JSON_VALUE lo use. Primero veremos el plan de ejecución de una consulta sin el índice y después como cambia cuando lo incluimos

SELECT JSON_VALUE(SecurityContext, '$.Enabled') FROM Orders 
WHERE JSON_VALUE(SecurityContext, '$.Enabled') = 'true'

image

Ahora agregamos el índice (bueno, en realidad agregamos una columna virtual y después el índice) y ejecutamos de nuevo la consulta para confirmar que lo está usando

ALTER TABLE Orders
ADD SecurityContextEnabled AS JSON_VALUE(SecurityContext,'$.Enabled')
GO
CREATE INDEX IX_Orders_Enabled
ON Orders(SecurityContextEnabled)  
GO
SELECT JSON_VALUE(SecurityContext, '$.Enabled') FROM Orders 
WHERE JSON_VALUE(SecurityContext, '$.Enabled') = 'true'

image

Un saludo!