Sto sviluppando una nuova API REST utilizzando Asp.Net Core e Entity Framework Core. Effettueremo il porting dei dati da un sistema legacy che utilizza il partizionamento orizzontale del database (sharding). Sto cercando di pensare a un buon modo per gestirlo in EF Core. La nostra precedente strategia di sharding prevedeva un database Prime centrale e più database clienti. Tutte le domande includevano un CustomerId
. Esaminiamo il database Prime con CustomerId per determinare quale database del cliente contiene i dati del cliente specifico. Gli schemi del database assomigliano a questo:
Prime Database
dbo.Database
DatabaseId INTEGER
ConnectionString VARCHAR(200)
dbo.Customer
CustomerId BIGINT
DatabaseId INTEGER
Database clienti
dbo.Order
CustomerId BIGINT
OrderId INT
...
Un esempio di chiamata REST per ottenere un ordine sarebbe qualcosa come http://foo.com/api/Customers/{CustomerId}/Orders/{OrderId}
Devo avere il mio CustomerDbContext
utilizzare una stringa di connessione determinata in modo dinamico con ogni richiesta REST. Devo creare nuove istanze di DbContext
per ogni richiesta? Oppure posso modificare la stringa di connessione in fase di esecuzione?
Se creo nuovi DbContexts, come dovrei farlo? La maggior parte del codice di esempio che posso trovare utilizza Dependency Injection da Startup.cs per creare un singolo DbContext
.
Ecco cosa mi è venuto in mente. È ancora molto difficile, e apprezzerei davvero ogni critica che potrebbe essere offerta.
Ho aggiunto un "UseForNewCustomer BOOLEAN" a dbo.Database. Sto usando Database Migrations per creare nuovi Shards al volo.
ShardDbContextFactory
public class ShardDbContextFactory : IDbContextFactory<ShardDbContext>
{
public ShardDbContext Create(DbContextFactoryOptions opts)
{
return this.Create("This-Connection-String-Isn't-Used");
}
public ShardDbContext Create(string connectionString)
{
var optsBldr = new DbContextOptionsBuilder<ShardDbContext>();
//This is for PostGres. If using MS Sql Server, use 'UseSqlServer()'
optsBldr.UseNpgsql(connectionString);
return new ShardDbContext(optsBldr.Options);
}
}
ShardContextService.cs
public interface IShardContextService {
ShardDbContext GetContextForCustomer(int customerId);
void ActivateShard(string connectionString, string dbType);
}
public class ShardContextService : IShardContextService {
private readonly PrimeDbContext _primeContext;
public ShardContextService(SystemDbContext primeContext) {
_primeContext = primeContext;
}
public CustomerDbContext GetContextForCustomer(int customerId)
{
Database shard = null;
var customer = _primeContext.Customers
.Include(m=>m.Database)
.SingleOrDefault(c=>c.CustomerId == customerId);
if (customer == null)
{
shard = _primeContext.Databases.Single(db=>db.UseForNewCustomer);
if (shard == null) throw new System.Exception("Unable to determine shard: This is a new customer, and no shards are designated as useable for new customers.");
_primeContext.Customers.Add(new Customer {
CustomerId = customerId,
DatabaseId = shard.DatabaseId
});
_primeContext.SaveChanges();
}
else
{
shard = customer.Database;
}
return (new ShardDbContextFactory()).Create(shard.ConnectionString)
}
public void ActivateShard(string connectionString)
{
using (var customerContext = (new ShardDbContextFactory()).Create(connectionString))
{
customerContext.Database.Migrate();
}
var previous = _primeContext.Databases.SingleOrDefault(d=>d.UseForNewCustomers);
if (previous != null)
{
previous.UseForNewCustomers = false;
}
var existing = _primeContext.Databases.SingleOrDefault(d=>d.ConnectionString == connectionString);
if (existing != null)
{
existing.UseForNewCustomers = true;
}
else
{
_primeContext.Databases.Add(new Database {
ConnectionString = connectionString,
UseForNewCustomers = true
});
}
_primeContext.SaveChanges();
}
}
Controller Azione per la creazione di un nuovo frammento
[HttpPost]
public IActionResult Shard([FromBody] string connectionString) {
try {
_shardContextService.ActivateShard(connectionString);
return Ok("Shard activated");
} catch (System.Exception e) {
return StatusCode(500, e);
}
}
Azione del controller per l'interrogazione
[HttpGet]
[Route("/api/Customers/{customerId}/Orders/{orderId}")]
public virtual IActionResult GetOrdersForCustomer([FromRoute]long customerId, [FromRoute] long orderId)
{
using (var ctx = _shardContextService.GetContextForCustomer(customerId))
{
var order = ctx.Orders.Where(o => o.CustomerId == customerId && o.OrderId = orderId).Single();
if (order == null) return NotFound("Unable to find this order.");
else return new ObjectResult(order);
}
}