Förenkla LINQ to Entity Framework med Extensions Methods del 2

Detta är en fortsättning på https://dev.datema.se/2014/06/04/forenkla-linq-to-entity-framework-med-extensions-methods-del-1/

Filtrera vid behov

Eftersom LINQs underliggande struktur är ett träd, så är det väldigt enkelt att klistra på metoder där de behövs. I vår kod så har jag använt detta för att lägga på filter vid behov. OBS! Följande sätt att peta in Where lite här och där i LINQ-uttrycken tenderar att skapa horribla SQL-frågor. Om du jobbar med stora datamängder så är det mycket möjligt att detta kommer orsaka frågor som tar lång tid.

public class CoreBase
{
    public int CustomerId { get; set; }
    protected internal IQueryable _customers
    {
        get 
        {
            if (CustomerId > 0)
                return _context.Customers.Where(x => x.Id == CustomerId);
            return _context.Customers;
        }
    } 

    public int AdministratorId { get; set; }
    protected internal IQueryable _people
    {
        get
        {
            if (AdministratorId > 0)
                return _context.People.Where(x => x.Id == AdministratorId);
            return _context.People;
        }
    }
}

Jag har exponerat flertalet av mina DbSet på detta sätt. När jag exponerar metoder till övre lager för att exempelvis läsa ordrar så är det väldigt enkelt att lägga på filter för att endast läsa alla ordrar för en viss kund.

    core.CustomerId = 3; 
    var orders = core.GetOrders();
    
    // och i coreklassens basklass 
    public List GetOrders() 
    { 
        return (from o in _orders
                join c in _customers on o.CustomerId equals c.Id
                select o).ToList();
    }

Ogres and code is like onions, it has got layers

Vi använder Entity Framework Code First i projektet, så vi låter Entity Framework generera databasen åt oss. Mellan Entity Framework och databasen finns en ADO-adaptern som sköter översättningen av LINQ-uttrycken till SQL. ADO-Adaptern för SQLite stödjer inte sådant som att skapa databaser och ändra tabeller. Vi löste det problemet med arv, där vår basklass har definitionen av våra DbSet och diverse stödfunktioner, och all databasspecifik kod ligger i de ärvda klasserna.

    public abstract class ContextBase : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        ...
    }

    public class SqlContext : ContextBase
    {
        ...
        static SqlContext()
        {
            Database.SetInitializer<RegistryContext>(new CreateInitializer());
        }

        class CreateInitializer : CreateDatabaseIfNotExists<RegistryContext>
        {
            ...
        }
    } 

    public class SQLiteContext : ContextBase
    {
        ... 
        static SqlContext()
        {
            Database.SetInitializer<ClientRegistryContext>(new CreateSqlLiteInitializer());
        }

        public class CreateSqlLiteInitializer : IDatabaseInitializer<ClientRegistryContext>
        {
            public void InitializeDatabase(ClientRegistryContext context)
            {
                // I SQLite fallet så skapar vi själva upp tabellerna med SQL
                // via SqlQuery på context.Database
                context.Database.SqlQuery("CREATE TABLE...");
                ...
            }
        }
    }

Det ger mig fördelen att jag kan enkelt kontrollera om en viss Context används av SQL eller SQLite.

Entity Framework med SQLite

Att använda Entity Framework med MS-SQL och SQLite går generellt sett bra, men SQLite har problem med vissa mer komplexa LINQ-uttryck. Två punkter va viktiga för mig att tänka på för att hitta en lämlig lösning. Det ena är att LINQ mot databaser är i stort sett en delmängd av funktionaliteten av det som kan användas i LINQ to Objects. Med andra ord så borde alla LINQ-uttryck som resulterar i databasanrop, ge exakt samma resultat om de istället jobbar mot t ex listor. Den andra punkten är att i detta projekt så innehåller SQLite databaserna endast en liten den av den stora databasen, och vi har inte något fall en där datamängderna är större än några tusen rader. Med de kunskaperna i åtanke så beslutade jag att ta kostnaden att läsa allt data i minnet och använda LINQ to Objects istället. Lösningen blev att i min basklass som jag redan skapat för dessa mer komplicerade frågor så la jag till en till Extension Method för att läsa upp data i minnet om det inte är ett anrop mot MS-SQL. Det sker enkelt via en .ToList(), och en .AsQueryable() för att exponera listan som en IQueryable.


    public static class CombinedCoreExtension
    {
        public static IQueryable<T> Transform<T>(this IRegistryContext context, IQueryable<T> source)
        {
            // Om frågan är mot en MS-SQL-databas så behövs ingen förändring
            if (context is RegistryContext)
                return source;

            // Om det är mot SQLite så läs upp i minnet.
            return source.ToList().AsQueryable();
        }
    }

Sedan ändrade jag min Core-bassklass så den använder denna nya Extension Method.

    protected internal IQueryable _customers
    {
        get 
        {
            // Här lades Transform till
            if (CustomerId > 0)
                return _context.Transform(context.Customers).Where(x => x.Id == CustomerId);
            return _context.Transform(context.Customers);
        }
    } 

Tester är bra, inte bara för testning

Om du har läst del 1 så kanske du undrar över de Extension Methods som jag skapade där. De borde orsaka NullReferenceException när en List försöks cast:as till DbSet. Mina exempel va förenklingar för att få de lite kortare. Metoderna ser egentligen ut på sätt:

    public static class CustomerExtensions
    {
        public static IQueryable EagerlyLoadOrdersRowsAndPeople(this IQueryable source)
        {
            if (s is DbSet)
                return (source as DbSet).Include(x => x.Orders.Select(x => x.Rows)).Include(x => x.People);
            return source;
        }
    }
 

Vi gör på det sättet eftersom i våra enhetstester så använder vi inte Entity Framework utan använder minnesstrukturer.

Lämna en kommentar