Particionar base de datos con Entity Framework Core

Supongamos que necesitamos dar soporte a varios clientes con una única aplicación. Por ejemplo ofreciendo SAAS (Software as a service). En la que tenemos el mismo software para todos los clientes, pero sus datos son independientes. La forma más sencilla seria crear una instancia de base de datos para cada cliente. Pero esto es costoso y no siempre viable, sobretodo si tenemos muchos clientes con un volumen de datos pequeño por cliente.

Azure nos ofrece la posibilidad de crear grupos elásticos. Pero estos están limitados a un máximo de 500 bases de datos. Aunque parecen muchas, es posible que tengamos más clientes y la mayoría de ellos inactivos. Con lo que está opción no es siempre viable.

Otra forma es marcar cada registro en base de datos con un campo instancia. En el que identificamos a que cliente o instancia pertenece da registro. Teniendo que insertar y meter en todas nuestras consultas la referencia a la instancia.

Si utilizamos Entity Framework Core, estre trabajo se puede automatizar, quedando de una forma transparente para la mayoría de desarrolladores.

Al definir las entidades del modelo. Deberemos introducir un campo en el que indicaremos a que partición pertenece el registro. Para ello crearemos una "propiedad sombra" la cual nos permite enlazar a un registro de base de datos, sin la necesidad de asociarlo a un registro en nuestra entidad.

modelBuilder.Entity<Person>(entity =>
{
   entity.Property<int>("IdPartition");
});

Para hacerlo más trasparente. Nos crearemos una clase PartitionValueGenerator que herede de ValueGenerator, la cual nos permitirá generar un valor automáticamente a nuestra propiedad. Así nos evitaremos la necesidad de darle el valor, cada vez que creamos un objeto.

public class PartitionValueGenerator : ValueGenerator<int>
{
    public override int Next(EntityEntry entry)
    {
        return ((MyDbContext)(entry.Context)).IdPartition;
    }

    public override bool GeneratesTemporaryValues => false;
}

Y se lo asignaremos a nuestra propiedad

modelBuilder.Entity<Person>(entity =>
{
     entity.Property<int>("IdPartition")
         .HasValueGenerator((p, e) => new PartitionValueGenerator(););
});

Con esto ya conseguimos insertar un valor de forma transparente en nuestro registro. Ahora vamos ha definir como hacer las consultas. Para ello necesitamos definirnos una versión personalizada de DbSet. Con la que podremos modificar el objeto Expression antes de lanzar la consulta. Redefinimos la propiedad IQueryable.Expression de la siguiente forma.


public class PartitionalDbSet<TEntity, TIdPartition> : DbSet<TEntity>,
            IQueryable<TEntity>, IAsyncEnumerableAccessor<TEntity>, Infrastructure<IServiceProvider>
        where TEntity : class
{
..... 

Expression IQueryable.Expression
{
    get
    {
        Expression dbExpression = ((IQueryable)mDbSetBase).Expression;
        TIdPartition idPartition = mIdPartition;

        Expression<Func<TEntity, bool>> expresionFuncFilter = b => EF.Property<TIdPartition>(b, "IdPartition").Equals(idPartition);

        IQueryable<TEntity> source = null;
        Expression<Func<TEntity, bool>> predicate = null;

        var meth = GetMethodInfo<IQueryable<TEntity>, Expression<Func<TEntity, bool>>,
                    IQueryable<TEntity>>(new Func<IQueryable<TEntity>, Expression<Func<TEntity, bool>>, IQueryable<TEntity>>
                        (Queryable.Where<TEntity>), source, predicate);

         Expression expression = Expression.Call(null, meth, dbExpression, expresionFuncFilter);

    return expression;
    }
}

Ya solo nos queda definir los DbSet en nuestra contexto.

private DbSet<Person> mPersons;

public DbSet<Person> Persons
{
    get
    {
        if (mPersons == null)
        {
           mPersons = new PartitionalDbSet<Person, int>(base.Set<Person>(), mIdPartition);
        }
        return mPersons;
    }
}
Con esto podremos definir diferentes instancias del contexto de base de datos, para diferentes clientes o subconjuntos de datos, de una forma muy sencilla y trasparente.

using (var db = new MyDbContext(1))
{
    Person person = new Person()
    {
         Name = "Name 1",
         Description = "Description 1"
    };

    db.Persons.Add(person);
    await db.SaveChangesAsync();
}

using (var db = new MyDbContext(1))
{
     var person = await db.Persons.Where(p => p.Name.StartsWith("Name")).ToListAsync();
     Assert.IsTrue(person.Count() > 0);
}

Si os interesa este tema lo podéis explorar con más detenidamente descargado un ejemplo en:
https://github.com/andrechi1/Blog-Examples/tree/master/PartitionEFCore

Comentarios

Entradas populares de este blog

Certificados de no confianza con Git

Pruebas unitarias con Entity Framework Core

Buenas practicas con Async/Await