Mostrando entradas con la etiqueta Entity Framework. Mostrar todas las entradas
Mostrando entradas con la etiqueta Entity Framework. Mostrar todas las entradas

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!

martes, 24 de noviembre de 2015

Crear índices en EF con Fluent API

Crear un índice vía Fluent API ha resultado ser una tarea nada intuitiva y más difícil de lo esperado. Es por ello que lo voy a dejar por escrito y así, siguientes veces, no tendré que navegar por mi código para buscar la chuleta de cómo hacerlo, porque en mi opinión es un código irrecordable y que no nunca logro descubrir a golpe de Intellisense.

Partiendo de una entidad muy sencilla como la siguiente:

class Customer

{

    public int Id { get; set; }

    public string Name { get; set; }

    public string Country { get; set; }

    public DateTime CreatedDate { get; set; }

}

Hacer un índice único por el campo Name sería asi:

modelBuilder

    .Entity<Customer>()

    .Property(t => t.Name)

    .HasMaxLength(250)

    .HasColumnAnnotation(IndexAnnotation.AnnotationName,

        new IndexAnnotation(new IndexAttribute() { IsUnique = true }));

Si esto ya parece un código algo enrevesado, crear un índice por varios campos es aun mas sorpredente porque es imprescindible dar un nombre al índice y una posición a cada campo:

var customer = modelBuilder.Entity<Customer>();

const string indexName = "IX_Customers";

customer

    .Property(t => t.Name)

    .HasMaxLength(250)

    .HasColumnAnnotation(

        IndexAnnotation.AnnotationName,

        new IndexAnnotation(

            new IndexAttribute(indexName, 1)

            {

                IsUnique = true

            }));

customer

    .Property(t => t.Country)

    .HasMaxLength(250)

    .HasColumnAnnotation(

        IndexAnnotation.AnnotationName,

        new IndexAnnotation(

            new IndexAttribute(indexName, 2)

            {

                IsUnique = true

            }));

El poner un tamaño a las propiedades de tipo string es porque si no se mapearán a nvarchar(max) y la creación del índice fallará en Sql Server.

Originalmente el post llevaba hasta aquí, pero el bueno de @gulnor me incito me incitó de mala manera a dar una solución y no sólo exponer un problema, y claro, después de leer su reciente y reflexivo post Código abierto, ecosistema cerrado, me he sentido un poco culpable y no he tenido otra que corresponder con una posible solución para que que así no se diga que no contribuyo (o al menos lo intento) :)

La idea es poder crear un índice sin todo el ruido que genera el código anterior, algo asi:

modelBuilder.Entity<Customer>().Index(p => p.Name, true, "IX_Customers", 1);

modelBuilder.Entity<Customer>().Index(p => p.Country, true, "IX_Customers", 2);

modelBuilder.Entity<Customer>().Index(p => p.CreatedDate);

El código con los métodos extensores es el siguiente (en el que no me gusta el nombre del parámetro genérico T2 y además reconozco abusar de la indentación, pero es por un buen motivo, para facilitar la lectura en el blog):

static class ModelBuilderExtensions

{

    private static void CreateIndex(

        PrimitivePropertyConfiguration property,

        bool isUnique = false,

        string name = null,

        int position = 1)

    {

        var indexAttribute = new IndexAttribute();

        if (!string.IsNullOrWhiteSpace(name))

        {

            indexAttribute = new IndexAttribute(

                name,

                position <= 0 ? 1 : position);

        }

        indexAttribute.IsUnique = isUnique;

        property.HasColumnAnnotation(

            IndexAnnotation.AnnotationName,

            new IndexAnnotation(indexAttribute));

    }

 

    public static void Index<T, T2>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, T2>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class where T2 : struct

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

 

    public static void Index<T, T2>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, T2?>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class where T2 : struct

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

 

    public static void Index<T>(

        this EntityTypeConfiguration<T> entity,

        Expression<Func<T, string>> propertyExpression,

        bool isUnique = false,

        string name = null,

        int position = 1) where T : class

    {

        var property = entity.Property(propertyExpression);

        CreateIndex(property, isUnique, name, position);

    }

}

Ahora sí, post cerrado!

Un saludo!