ObjectRepository - Patrón de repositorio .NET en memoria para os teus proxectos domésticos

Por que almacenar todos os datos na memoria?

Para almacenar datos do sitio web ou do backend, o primeiro desexo da maioría das persoas sensatas será escoller unha base de datos SQL. 

Pero ás veces vén á mente o pensamento de que o modelo de datos non é adecuado para SQL: por exemplo, ao construír unha busca ou un gráfico social, cómpre buscar relacións complexas entre obxectos. 

A peor situación é cando traballas en equipo e un compañeiro non sabe como facer consultas rápidas. Canto tempo dedicaches a resolver problemas N+1 e a construír índices adicionais para que SELECT na páxina principal se completase nun período de tempo razoable?

Outro enfoque popular é NoSQL. Hai varios anos houbo moita publicidade sobre este tema: para calquera ocasión conveniente implantaron MongoDB e estaban satisfeitos coas respostas en forma de documentos json (por certo, cantas muletas tiveches que inserir polas ligazóns circulares dos documentos?).

Suxiro probar outro método alternativo: por que non tentar almacenar todos os datos na memoria da aplicación, gardándoos periodicamente nun almacenamento aleatorio (arquivo, base de datos remota)? 

A memoria abaratouse e calquera dato posible para a maioría dos proxectos pequenos e medianos encaixará en 1 GB de memoria. (Por exemplo, o meu proxecto doméstico favorito é rastreador financeiro, que mantén as estatísticas diarias e o historial dos meus gastos, saldos e transaccións durante ano e medio, consome só 45 MB de memoria).

Pros:

  • O acceso aos datos faise máis sinxelo: non tes que preocuparte polas consultas, a carga perezosa, as funcións ORM, traballas con obxectos C# comúns;
  • Non hai problemas asociados co acceso desde diferentes fíos;
  • Moi rápido: sen solicitudes de rede, sen tradución de código a unha linguaxe de consulta, sen necesidade de (des)serialización de obxectos;
  • É aceptable almacenar datos de calquera forma, xa sexa en XML no disco, en SQL Server ou en Azure Table Storage.

Contras:

  • A escala horizontal pérdese e, como resultado, non se pode facer a implantación de tempo de inactividade cero;
  • Se a aplicación falla, podes perder parcialmente os datos. (Pero a nosa aplicación nunca falla, non?)

Como funciona isto?

O algoritmo é o seguinte:

  • Ao comezo, establécese unha conexión co almacenamento de datos e cárganse os datos;
  • Constrúese un modelo de obxectos, índices primarios e índices relacionais (1:1, 1:Moitos);
  • Créase unha subscrición para cambios nas propiedades do obxecto (INotifyPropertyChanged) e para engadir ou eliminar elementos á colección (INotifyCollectionChanged);
  • Cando se activa a subscrición, o obxecto modificado engádese á cola para escribir no almacenamento de datos;
  • Os cambios no almacenamento gárdanse periodicamente (nun temporizador) nun fío de fondo;
  • Cando sae da aplicación, os cambios tamén se gárdanse no almacenamento.

Exemplo de código

Engadindo as dependencias necesarias

// Основная библиотека
Install-Package OutCode.EscapeTeams.ObjectRepository
    
// Хранилище данных, в котором будут сохраняться изменения
// Используйте то, которым будете пользоваться.
Install-Package OutCode.EscapeTeams.ObjectRepository.File
Install-Package OutCode.EscapeTeams.ObjectRepository.LiteDb
Install-Package OutCode.EscapeTeams.ObjectRepository.AzureTableStorage
    
// Опционально - если нужно хранить модель данных для Hangfire
// Install-Package OutCode.EscapeTeams.ObjectRepository.Hangfire

Describimos o modelo de datos que se almacenará no almacenamento

public class ParentEntity : BaseEntity
{
    public ParentEntity(Guid id) => Id = id;
}
    
public class ChildEntity : BaseEntity
{
    public ChildEntity(Guid id) => Id = id;
    public Guid ParentId { get; set; }
    public string Value { get; set; }
}

A continuación, o modelo de obxectos:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Пример связи 1:Many
    public IEnumerable<ChildModel> Children => Multiple<ChildModel>(x => x.ParentId);
    
    protected override BaseEntity Entity { get; }
}
    
public class ChildModel : ModelBase
{
    private ChildEntity _childEntity;
    
    public ChildModel(ChildEntity entity)
    {
        _childEntity = entity;
    }
    
    public ChildModel() 
    {
        _childEntity = new ChildEntity(Guid.NewGuid());
    }
    
    public Guid ParentId
    {
        get => _childEntity.ParentId;
        set => UpdateProperty(() => _childEntity.ParentId, value);
    }
    
    public string Value
    {
        get => _childEntity.Value;
        set => UpdateProperty(() => _childEntity.Value, value
    }
    
    // Доступ с поиском по индексу
    public ParentModel Parent => Single<ParentModel>(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

E, finalmente, a propia clase do repositorio para acceder aos datos:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Для тестов, позволяет не сохранять изменения в базу
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Если используется Hangfire и необходимо хранить модель данных для Hangfire в ObjectRepository
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

Crear unha instancia de ObjectRepository:

var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
    
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();

Se o proxecto usará HangFire

public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
    services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}

Inserir un novo obxecto:

var newParent = new ParentModel()
repository.Add(newParent);

Con esta chamada, o obxecto Modelo de pais engádese tanto á caché local como á cola para escribir na base de datos. Polo tanto, esta operación leva O(1) e este obxecto pódese traballar inmediatamente.

Por exemplo, para atopar este obxecto no repositorio e verificar que o obxecto devolto é a mesma instancia:

var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));

Que está pasando? Conxunto () volve Dicionario de táboas, que contén Dicionario Concurrente e proporciona funcionalidade adicional de índices primarios e secundarios. Isto permítelle ter métodos para buscar por Id (ou outros índices de usuarios arbitrarios) sen iterar completamente sobre todos os obxectos.

Ao engadir obxectos a Repositorio de obxectos engádese unha subscrición para cambiar as súas propiedades, polo que calquera cambio nas propiedades tamén provoca que este obxecto se engada á cola de escritura. 
Actualizar propiedades desde o exterior ten o mesmo aspecto que traballar cun obxecto POCO:

myParent.Children.First().Property = "Updated value";

Podes eliminar un obxecto das seguintes formas:

repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());

Isto tamén engade o obxecto á cola de eliminación.

Como funciona o aforro?

Repositorio de obxectos cando os obxectos monitorizados cambian (xa sexa engadindo ou borrando ou cambiando propiedades), provoca un evento Modelo cambiadosubscrito ISalmacenamento. Implementacións ISalmacenamento cando ocorre un evento Modelo cambiado Os cambios ponse en 3 filas: para engadir, actualizar e eliminar.

Tamén implementacións ISalmacenamento despois da inicialización, crean un temporizador que fai que os cambios se garde cada 5 segundos. 

Ademais, hai unha API para forzar unha chamada de gardar: ObjectRepository.Save().

Antes de cada gardado, as operacións sen sentido son eliminadas das filas (por exemplo, eventos duplicados - cando un obxecto foi cambiado dúas veces ou rapidamente engadidos/eliminados obxectos), e só despois o propio gardado. 

En todos os casos, gárdase todo o obxecto actual, polo que é posible que os obxectos se garden nunha orde diferente da que se cambiaron, incluídas as versións máis novas dos obxectos que no momento en que se engadiron á cola.

Que máis hai?

  • Todas as bibliotecas están baseadas en .NET Standard 2.0. Pódese usar en calquera proxecto .NET moderno.
  • A API é segura para fíos. As coleccións internas impléntanse en función de Dicionario Concurrente, os controladores de eventos teñen bloqueos ou non os necesitan. 
    O único que vale a pena lembrar é chamar ObjectRepository.Save();
  • Índices arbitrarios (requiren unicidade):

repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");

Quen o usa?

Persoalmente, comecei a usar este enfoque en todos os proxectos de hobby porque é cómodo e non require grandes gastos para escribir unha capa de acceso a datos ou implantar unha infraestrutura pesada. Persoalmente, almacenar datos en litedb ou nun ficheiro adoita ser suficiente para min. 

Pero no pasado, cando a xa desaparecida startup EscapeTeams (Pensei que aquí está, diñeiro, pero non, experiencia de novo) - usado para almacenar datos en Azure Table Storage.

Planos para o futuro

Gustaríame corrixir unha das principais desvantaxes deste enfoque: a escala horizontal. Para iso, necesitas transaccións distribuídas (sic!), ou tomar unha decisión voluntaria de que os mesmos datos de diferentes instancias non deben cambiar, ou deixalos cambiar segundo o principio "quen é o último ten razón".

Desde o punto de vista técnico, vexo posible o seguinte esquema:

  • Almacena EventLog e Snapshot en lugar do modelo de obxectos
  • Buscar outras instancias (engadir puntos finais de todas as instancias á configuración? descubrimento udp? mestre/escravo?)
  • Replica entre instancias de EventLog mediante calquera algoritmo de consenso, como RAFT.

Tamén hai outro problema que me preocupa: a eliminación en cascada, ou a detección de casos de eliminación de obxectos que teñen ligazóns doutros obxectos. 

Código fonte

Se leu ata aquí, só queda ler o código; pódese atopar en GitHub:
https://github.com/DiverOfDark/ObjectRepository

Fonte: www.habr.com

Engadir un comentario