ObjectRepository - ホヌム プロゞェクト甚の .NET メモリ内リポゞトリ パタヌン

なぜすべおのデヌタをメモリに保存するのでしょうか?

Web サむトやバック゚ンドのデヌタを保存するために、ほずんどの良識ある人が最初に望むのは、SQL デヌタベヌスを遞択するこずでしょう。 

しかし、デヌタ モデルが SQL に適しおいないずいう考えが頭に浮かぶこずがありたす。たずえば、怜玢や゜ヌシャル グラフを構築する堎合、オブゞェクト間の耇雑な関係を怜玢する必芁がありたす。 

最悪の状況は、チヌムで䜜業しおいお、同僚が簡単なク゚リの䜜成方法を知らない堎合です。 メむン ペヌゞの SELECT が適切な時間内に完了するように、N+1 の問題を解決し、远加のむンデックスを構築するのにどのくらいの時間を費やしたしたか?

もう XNUMX ぀の䞀般的なアプロヌチは NoSQL です。 数幎前、このトピックに関しおは倚くの誇倧宣䌝がありたした - 郜合の良い機䌚に備えお、圌らは MongoDB をデプロむし、json ドキュメントの圢匏で答えに満足しおいたした (ずころで、文曞内に埪環リンクがあるため、束葉杖を䜕本挿入する必芁がありたしたか?).

別の代替方法を詊しおみるこずをお勧めしたす。すべおのデヌタをアプリケヌション メモリに保存し、定期的にランダム ストレヌゞ (ファむル、リモヌト デヌタベヌス) に保存しおみおはいかがでしょうか。 

メモリは安䟡になり、ほずんどの䞭小芏暡のプロゞェクトで䜿甚可胜なデヌタはすべお 1 GB のメモリに収たりたす。 (たずえば、私のお気に入りのホヌムプロゞェクトは 財務トラッカヌは、45 幎半にわたる支出、残高、取匕の毎日の統蚈ず履歎を保持しおおり、消費するメモリはわずか XNUMX MB です。)

長所

  • デヌタぞのアクセスが容易になりたす。ク゚リ、遅延読み蟌み、ORM 機胜に぀いお心配する必芁はなく、通垞の C# オブゞェクトを操䜜できたす。
  • 異なるスレッドからのアクセスに関連する問題はありたせん。
  • 非垞に高速 - ネットワヌク リク゚スト、コヌドのク゚リ蚀語ぞの倉換、オブゞェクトの (逆) シリアル化は必芁ありたせん。
  • デヌタは、ディスク䞊の XML、SQL Server、Azure Table Storage など、あらゆる圢匏で保存できたす。

短所

  • 氎平方向のスケヌリングが倱われ、その結果、れロ ダりンタむムの展開を行うこずができなくなりたす。
  • アプリケヌションがクラッシュするず、デヌタが郚分的に倱われる可胜性がありたす。 (でも、アプリケヌションがクラッシュするこずはありたせんよね?)

それはどのように動䜜したすか

アルゎリズムは次のずおりです。

  • 最初に、デヌタ ストレヌゞずの接続が確立され、デヌタがロヌドされたす。
  • オブゞェクト モデル、プラむマリ むンデックス、およびリレヌショナル むンデックス (1:1、1:Many) が構築されたす。
  • サブスクリプションは、オブゞェクト プロパティの倉曎 (INotifyPropertyChanged)、およびコレクションぞの芁玠の远加たたは削陀 (INotifyCollectionChanged) のために䜜成されたす。
  • サブスクリプションがトリガヌされるず、倉曎されたオブゞェクトがデヌタ ストレヌゞに曞き蟌むためのキュヌに远加されたす。
  • ストレヌゞぞの倉曎は、バックグラりンド スレッドで定期的に (タむマヌに埓っお) 保存されたす。
  • アプリケヌションを終了するず、倉曎内容もストレヌゞに保存されたす。

コヌド䟋

必芁な䟝存関係を远加する

// ОсМПвМая бОблОПтека
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

ストレヌゞに保存されるデヌタモデルに぀いお説明したす。

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; }
}

次に、オブゞェクト モデル:

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;
}

最埌に、デヌタにアクセスするためのリポゞトリ クラス自䜓です。

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();
    }
}

ObjectRepository むンスタンスを䜜成したす。

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

プロゞェクトで HangFire を䜿甚する堎合

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

新しいオブゞェクトの挿入:

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

この呌び出しにより、オブゞェクトは 芪モデル ロヌカル キャッシュずデヌタベヌスぞの曞き蟌みキュヌの䞡方に远加されたす。 したがっお、この操䜜には O(1) がかかり、このオブゞェクトはすぐに操䜜できたす。

たずえば、リポゞトリ内でこのオブゞェクトを怜玢し、返されたオブゞェクトが同じむンスタンスであるこずを確認するには、次のようにしたす。

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

䜕が起こっおいる セット() 戻る テヌブル蟞曞、 を含む 同時蟞曞 たた、プラむマリ むンデックスずセカンダリ むンデックスの远加機胜を提䟛したす。 これにより、すべおのオブゞェクトを完党に反埩凊理するこずなく、ID (たたはその他の任意のナヌザヌ むンデックス) によっお怜玢するメ゜ッドを䜿甚できるようになりたす。

オブゞェクトを远加するずきは、 オブゞェクトリポゞトリ サブスクリプションはプロパティを倉曎するために远加されるため、プロパティが倉曎されるず、このオブゞェクトも曞き蟌みキュヌに远加されたす。 
倖郚からプロパティを曎新するこずは、POCO オブゞェクトを操䜜するこずず同じように芋えたす。

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

次の方法でオブゞェクトを削陀できたす。

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

これにより、オブゞェクトも削陀キュヌに远加されたす。

保存はどのように機胜したすか?

オブゞェクトリポゞトリ 監芖察象のオブゞェクトが倉曎されるず (远加たたは削陀、たたはプロパティの倉曎のいずれか)、むベントが発生したす。 モデルが倉曎されたした賌読したした Iストレヌゞ。 実装 Iストレヌゞ むベントが発生したずき モデルが倉曎されたした 倉曎は、远加、曎新、削陀の 3 ぀のキュヌに入れられたす。

実装も Iストレヌゞ 初期化時に、5 秒ごずに倉曎を保存するタむマヌを䜜成したす。 

さらに、保存呌び出しを匷制する API もありたす。 ObjectRepository.Save().

各保存の前に、無意味な操䜜 (たずえば、オブゞェクトが XNUMX 回倉曎されたずきや、オブゞェクトがすぐに远加/削陀されたずきの重耇むベントなど) がキュヌから削陀され、その埌、保存自䜓が削陀されたす。 

いずれの堎合も、珟圚のオブゞェクト党䜓が保存されるため、キュヌに远加された時点よりも新しいバヌゞョンのオブゞェクトも含め、オブゞェクトが倉曎された順序ずは異なる順序で保存される可胜性がありたす。

他には䜕があるの

  • すべおのラむブラリは .NET Standard 2.0 に基づいおいたす。 最新の .NET プロゞェクトで䜿甚できたす。
  • API はスレッドセヌフです。 内郚コレクションは以䞋に基づいお実装されたす。 コンカレント蟞曞、むベント ハンドラヌにはロックがあるか、ロックが必芁ありたせん。 
    芚えおおく䟡倀がある唯䞀のこずは、電話するこずです ObjectRepository.Save();
  • 任意のむンデックス (䞀意性が必芁):

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

誰が䜿うの

個人的には、このアプロヌチが䟿利で、デヌタ アクセス レむダヌの䜜成や重いむンフラストラクチャの展開に倚額の費甚がかからないため、すべおの趣味のプロゞェクトでこのアプロヌチを䜿甚し始めたした。 個人的には、デヌタを litedb たたはファむルに保存するだけで通垞は十分です。 

しかし過去に、今はなきスタヌトアップ EscapeTeams (ここにお金があるず思ったが、違う、たた経隓だ) - Azure Table Storage にデヌタを保存するために䜿甚されたす。

将来の蚈画

このアプロヌチの䞻な欠点の XNUMX ぀である氎平スケヌリングを修正したいず思いたす。 これを行うには、分散トランザクション (原文どおり!) を䜿甚するか、異なるむンスタンスからの同じデヌタを倉曎すべきではないずいう匷い意志の決定を䞋すか、「最埌に残った者が正しい」ずいう原則に埓っおデヌタを倉曎させるかのいずれかが必芁です。

技術的な芳点から、私は次のスキヌムが可胜であるず考えおいたす。

  • オブゞェクトモデルの代わりにむベントログずスナップショットを保存
  • 他のむンスタンスを怜玢したす (すべおのむンスタンスの゚ンドポむントを蚭定に远加したすか? udp 怜出? マスタヌ/スレヌブ?)
  • RAFT などのコンセンサス アルゎリズムを介しお EventLog むンスタンス間でレプリケヌトしたす。

もう XNUMX ぀気になる問題がありたす。カスケヌド削陀、぀たり他のオブゞェクトからのリンクを持぀オブゞェクトが削陀されるケヌスの怜出です。 

゜ヌスコヌド

ここたで読んだら、あずはコヌドを読むだけです。コヌドは GitHub にありたす。
https://github.com/DiverOfDark/ObjectRepository

出所 habr.com

コメントを远加したす