ObjectRepository: patrón de repositorio en memoria .NET para sus proyectos domésticos

¿Por qué almacenar todos los datos en la memoria?


Para almacenar datos del sitio o del servidor, el primer deseo de la mayoría de las personas sensatas será una base de datos SQL.


Pero a veces surge la idea de que el modelo de datos no es adecuado para SQL: por ejemplo, al crear una búsqueda o un gráfico social, debe buscar relaciones complejas entre objetos.


La peor situación es cuando trabaja en un equipo y un colega no puede generar consultas rápidas. ¿Cuánto tiempo pasaste resolviendo problemas de N + 1 y creando índices adicionales para que SELECT en la página principal funcionase en un tiempo razonable?


Otro enfoque popular es NoSQL. Hace unos años hubo una gran expectación sobre este tema: para cualquier oportunidad, implementamos MongoDB y disfrutamos de las respuestas en forma de documentos json (por cierto, ¿cuántas muletas tuvieron que insertarse debido a los enlaces circulares en los documentos?) .


¿Por qué no intentar almacenar todos los datos en la memoria de la aplicación, guardándolos periódicamente en un almacenamiento arbitrario (archivo, base de datos remota)?


La memoria se ha vuelto barata, y cualquier dato posible de la mayoría de los proyectos pequeños y medianos cabe en 1 GB de memoria. (Por ejemplo, mi proyecto de casa favorito: un rastreador financiero que mantiene estadísticas diarias y un historial de mis gastos, saldos y transacciones durante un año y medio, consume solo 45 MB de memoria).


Pros:


  • El acceso a los datos se está volviendo más fácil: no necesita preocuparse por las consultas, la carga diferida, las funciones ORM, trabaje con objetos C # comunes;
  • No hay problemas asociados con el acceso desde diferentes hilos;
  • Muy rápido: sin solicitudes de red, sin traducción de código al lenguaje de consulta, sin (des) serialización de objetos;
  • Está permitido almacenar datos en cualquier forma, al menos en XML en el disco, al menos en SQL Server, al menos en Azure Table Storage.

Contras:


  • Se pierde la escala horizontal y, como resultado, no se puede implementar el tiempo de inactividad cero;
  • Si la aplicación falla, puede perder parcialmente los datos. (Pero nuestra aplicación nunca falla, ¿verdad?)

Como funciona


El algoritmo es el siguiente:


  • Al principio, se establece una conexión con el almacén de datos y se descargan los datos;
  • Se construye un modelo de objetos, índices primarios e índices de relación (1: 1, 1: Muchos);
  • Se crea una suscripción para cambiar las propiedades de los objetos (INotifyPropertyChanged) y para agregar o eliminar elementos de la colección (INotifyCollectionChanged);
  • Cuando se activa la suscripción: el objeto modificado se agrega a la cola para escribir en el almacén de datos;
  • Periódicamente (por temporizador), los cambios en el almacenamiento se almacenan en la secuencia de fondo;
  • Cuando sale de la aplicación, los cambios en el repositorio también se guardan.

Ejemplo de código


Agregar las 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 el modelo de datos que se almacenará en el repositorio.
 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; } } 

Entonces el modelo de objeto:
 public class ParentModel : ModelBase {  public ParentModel(ParentEntity entity)  {    Entity = entity;  }    public ParentModel()  {    Entity = new ParentEntity(Guid.NewGuid());  }    //   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; } 

Y finalmente, la clase del repositorio en sí para acceder a los 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();  } } 

Cree una 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(); 

Si el proyecto usará HangFire
 public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) {  services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); } 

Insertar un nuevo objeto:


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

En esta llamada, el objeto ParentModel se agrega tanto a la memoria caché local como a la cola de escritura en la base de datos. Por lo tanto, esta operación toma O (1), y puede trabajar inmediatamente con este objeto.


Por ejemplo, para encontrar este objeto en el repositorio y asegurarse de que el objeto devuelto sea la misma instancia:


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

¿Qué pasa con esto? El conjunto <ParentModel> () devuelve un TableDictionary <ParentModel> , que contiene ConcurrentDictionary <ParentModel, ParentModel> y proporciona funcionalidad adicional para índices primarios y secundarios. Esto le permite tener métodos para buscar por Id (u otros índices personalizados arbitrarios) sin enumerar por completo todos los objetos.


Cuando se agregan objetos al ObjectRepository , se agrega una suscripción para cambiar sus propiedades, por lo que cualquier cambio en las propiedades también hace que este objeto se agregue a la cola de escritura.
Actualizar las propiedades desde el exterior tiene el mismo aspecto que trabajar con un objeto POCO:


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

Puede eliminar un objeto de las siguientes maneras:


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

Esto también agrega el objeto a la cola de borrado.


¿Cómo funciona la conservación?


ObjectRepository al cambiar los objetos rastreados (tanto al agregar o quitar como al cambiar propiedades) genera el evento ModelChanged , al que está suscrito IStorage . Las implementaciones de IStorage, cuando ocurre un evento ModelChanged , resumen los cambios en 3 colas: agregar, actualizar y eliminar.


Además, las implementaciones de IStorage durante la inicialización crean un temporizador que cada 5 segundos hace que se guarden los cambios.


Además, hay una API para forzar una llamada de guardado: ObjectRepository.Save () .


Antes de cada guardado, las primeras operaciones sin sentido se eliminan de las colas (por ejemplo, eventos duplicados, cuando el objeto ha cambiado dos veces o la adición / eliminación rápida de objetos), y solo luego el guardado en sí.


En todos los casos, se guarda todo el objeto, por lo que es posible que los objetos se guarden en un orden diferente al que se cambiaron, incluidas las versiones más nuevas de los objetos que en el momento de agregarlos a la cola.


Que mas hay


  • Todas las bibliotecas están basadas en .NET Standard 2.0. Se puede usar en cualquier proyecto moderno .NET.
  • La API es segura para subprocesos. Las colecciones internas se basan en ConcurrentDictionary , los controladores de eventos tienen bloqueos o no los necesitan.
    Lo único que debe recordar es llamar a ObjectRepository.Save ();
  • Índices personalizados (requieren unicidad):

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

¿Quién lo está usando?


Personalmente, comencé a usar este enfoque en todos los proyectos de pasatiempos, porque es conveniente y no requiere grandes gastos para escribir una capa de acceso a datos o implementar una infraestructura pesada. Personalmente, por regla general, almacenar datos en litedb o en un archivo suele ser suficiente para mí.


Pero en el pasado, cuando EscapeTeams, el inicio tardío, se hizo con el equipo ( pensaban que era dinero, pero no, experiencia otra vez ), usaron Azure Table Storage para almacenar datos.


Planes futuros


Me gustaría solucionar una de las principales desventajas de este enfoque: el escalado horizontal. Para hacer esto, necesita transacciones distribuidas (¡sic!), O tomar una decisión decidida de que los mismos datos de diferentes instancias no deberían cambiar, o dejar que cambien de acuerdo con el principio "quién es el último, eso es correcto".


Desde un punto de vista técnico, veo el siguiente esquema posible:


  • Almacene EventLog y Snapshot en lugar del modelo de objeto
  • Encuentre otras instancias (agregue puntos finales de todas las instancias? Descubrimiento de Udp? ¿Maestro / esclavo? A la configuración)
  • Replicar entre instancias EventLog a través de cualquiera de los algoritmos de consenso, como RAFT.

También hay otro problema que me molesta: la eliminación en cascada o la detección de casos de eliminación de objetos a los que se hace referencia desde otros objetos.


Código fuente


Si lee hasta aquí, solo queda código por leer, puede ser
encontrado en github .

Source: https://habr.com/ru/post/452232/


All Articles