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!