ObjectRepository - Padrão de repositório em memória .NET para seus projetos domésticos

Por que armazenar todos os dados na memória?


Para armazenar dados de site ou de back-end, o primeiro desejo da maioria das pessoas sãs será um banco de dados SQL.


Mas, às vezes, surge a ideia de que o modelo de dados não é adequado para SQL: por exemplo, ao criar uma pesquisa ou um gráfico social, você precisa procurar relacionamentos complexos entre objetos.


A pior situação é quando você trabalha em equipe e um colega não é capaz de criar consultas rápidas. Quanto tempo você gastou na solução de problemas de N + 1 e na criação de índices adicionais para que o SELECT na página principal funcionasse em um tempo razoável?


Outra abordagem popular é o NoSQL. Alguns anos atrás, houve um grande hype em torno desse tópico - para qualquer oportunidade, implantamos o MongoDB e ficamos felizes com as respostas na forma de json-documents (a propósito, quantas muletas tiveram que ser inseridas por causa de links circulares nos documentos?) .


Por que não tentar armazenar todos os dados na memória do aplicativo, salvando-os periodicamente em armazenamento arbitrário (arquivo, banco de dados remoto)?


A memória ficou barata e todos os dados possíveis da maioria dos projetos pequenos e médios cabem em 1 GB de memória. (Por exemplo, meu projeto doméstico favorito - um rastreador financeiro que mantém estatísticas diárias e um histórico de minhas despesas, saldos e transações por um ano e meio consome apenas 45 MB de memória.)


Prós:


  • O acesso aos dados está se tornando mais fácil - não há necessidade de se preocupar com consultas, carregamento lento, recursos ORM, trabalhar com objetos C # comuns;
  • Não há problemas associados ao acesso de diferentes segmentos;
  • Muito rápido - sem solicitações de rede, sem tradução de código no idioma da consulta, sem (des) serialização de objetos;
  • É permitido armazenar dados de qualquer forma - pelo menos em XML no disco, pelo menos no SQL Server, pelo menos no Armazenamento de Tabelas do Azure.

Contras:


  • A escala horizontal é perdida e, como resultado, a implantação de tempo de inatividade zero não pode ser feita;
  • Se o aplicativo falhar, você poderá perder parcialmente os dados. (Mas nosso aplicativo nunca falha, certo?)

Como isso funciona?


O algoritmo é o seguinte:


  • No início, uma conexão com o data warehouse é estabelecida e os dados são baixados;
  • Um modelo de objeto, índices primários e índices de relacionamento (1: 1, 1: Many) são criados;
  • Uma assinatura é criada para alterar as propriedades dos objetos (INotifyPropertyChanged) e para adicionar ou remover elementos da coleção (INotifyCollectionChanged);
  • Quando a assinatura é acionada - o objeto alterado é adicionado à fila para gravação no armazém de dados;
  • Periodicamente (por timer), as alterações no armazenamento são salvas no fluxo em segundo plano;
  • Quando você sai do aplicativo, as alterações no repositório também são salvas.

Exemplo de código


Adicione as dependências necessárias
//   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 

Descrevemos o modelo de dados que será armazenado no repositório
 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; } } 

Então o 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; } 

E, finalmente, a própria classe de repositório para acessar dados:
 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();  } } 

Crie uma instância do 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 projeto usar o HangFire
 public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) {  services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); } 

Insira um novo objeto:


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

Nesta chamada, o objeto ParentModel é adicionado ao cache local e à fila de gravação no banco de dados. Portanto, essa operação usa O (1) e você pode trabalhar imediatamente com esse objeto.


Por exemplo, para encontrar este objeto no repositório e verifique se o objeto retornado é a mesma instância:


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

O que acontece com isso? O conjunto <ParentModel> () retorna um TableDictionary <ParentModel> , que contém ConcurrentDictionary <ParentModel, ParentModel> e fornece funcionalidade adicional para índices primários e secundários. Isso permite que você tenha métodos para pesquisar por ID (ou outros índices personalizados arbitrários) sem enumerar completamente todos os objetos.


Quando objetos são adicionados ao ObjectRepository , uma assinatura é adicionada para alterar suas propriedades, portanto, qualquer alteração nas propriedades também faz com que esse objeto seja adicionado à fila de gravação.
Atualizar propriedades de fora parece o mesmo que trabalhar com um objeto POCO:


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

Você pode excluir um objeto das seguintes maneiras:


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

Isso também adiciona o objeto à fila de exclusão.


Como funciona a conservação?


O ObjectRepository ao alterar objetos rastreados (adicionando ou excluindo ou alterando propriedades) gera o evento ModelChanged , no qual IStorage está inscrito . As implementações do IStorage, quando ocorre um evento ModelChanged , resumem as alterações em três filas - adicionar, atualizar e excluir.


Além disso, as implementações do IStorage durante a inicialização criam um timer que a cada 5 segundos faz com que as alterações sejam salvas.


Além disso, há uma API para forçar uma chamada de salvamento: ObjectRepository.Save () .


Antes de cada salvamento, as primeiras operações sem sentido são removidas das filas (por exemplo, eventos duplicados - quando um objeto é alterado duas vezes ou a adição / remoção rápida de objetos) e somente então o salvamento em si.


Em todos os casos, o objeto inteiro é mantido, portanto, é possível que os objetos sejam salvos em uma ordem diferente da que foram alterados, incluindo versões mais recentes dos objetos do que no momento da inclusão na fila.


O que mais existe?


  • Todas as bibliotecas são baseadas no .NET Standard 2.0. Pode ser usado em qualquer projeto .NET moderno.
  • A API é segura para threads. As coleções internas são baseadas no ConcurrentDictionary , os manipuladores de eventos têm bloqueios ou não precisam deles.
    A única coisa a lembrar é chamar ObjectRepository.Save ();
  • Índices personalizados (exigem exclusividade):

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

Quem está usando?


Pessoalmente, comecei a usar essa abordagem em todos os projetos de hobby, porque é conveniente e não exige grandes despesas para gravar uma camada de acesso a dados ou implantar uma infraestrutura pesada. Pessoalmente, como regra, armazenar dados no litedb ou em um arquivo geralmente é suficiente para mim.


Mas, no passado, quando o EscapeTeams, a inicialização tardia, era feita com a equipe (eles pensavam que eram dinheiro - mas não, experimentam novamente ) - eles usavam o Armazenamento de Tabela do Azure para armazenar dados.


Planos futuros


Gostaria de corrigir uma das principais desvantagens dessa abordagem - escala horizontal. Para fazer isso, você precisa de transações distribuídas (sic!), Ou tomar uma decisão decidida de que os mesmos dados de diferentes instâncias não devem mudar ou deixá-los mudar de acordo com o princípio "quem é o último - está certo".


Do ponto de vista técnico, vejo o seguinte esquema possível:


  • Armazene EventLog e Snapshot em vez do modelo de objeto
  • Encontre outras instâncias (adicione pontos de extremidade de todas as instâncias? Descoberta de Udp? Mestre / escravo? Às configurações)
  • Replicar entre instâncias do EventLog através de qualquer um dos algoritmos de consenso, como o RAFT.

Há também outro problema que me incomoda - é a exclusão em cascata ou a detecção de casos de exclusão de objetos referenciados de outros objetos.


Código fonte


Se você ler até aqui - somente o código ainda será lido, pode ser
encontrado no github .

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


All Articles