ObjectRepository - .NET dalam memori repositori pola untuk proyek-proyek rumah Anda

Mengapa menyimpan semua data dalam memori?


Untuk menyimpan data situs atau backend, keinginan pertama kebanyakan orang waras adalah database SQL.


Tapi kadang-kadang muncul ide bahwa model data tidak cocok untuk SQL: misalnya, ketika membangun pencarian atau grafik sosial, Anda perlu mencari hubungan yang kompleks antara objek.


Situasi terburuk adalah ketika Anda bekerja dalam sebuah tim, dan seorang kolega tidak dapat membangun kueri cepat. Berapa banyak waktu yang Anda habiskan untuk menyelesaikan masalah N + 1 dan membangun indeks tambahan sehingga SELECT di halaman utama berhasil dalam waktu yang wajar?


Pendekatan populer lainnya adalah NoSQL. Beberapa tahun yang lalu ada hype besar di sekitar topik ini - untuk setiap kesempatan, kami menyebarkan MongoDB dan menikmati jawaban dalam bentuk dokumen json (omong-omong, berapa banyak kruk harus dimasukkan karena tautan melingkar dalam dokumen?) .


Mengapa tidak mencoba menyimpan semua data dalam memori aplikasi, menyimpannya secara berkala ke penyimpanan sewenang-wenang (file, basis data jauh)?


Memori telah menjadi murah, dan setiap data yang mungkin dari sebagian besar proyek kecil dan menengah akan masuk ke dalam memori 1 GB. (Misalnya, proyek rumah favorit saya - pelacak keuangan yang menyimpan statistik harian dan riwayat pengeluaran, saldo, dan transaksi saya selama satu setengah tahun hanya menghabiskan 45 MB memori.)


Pro:


  • Akses ke data menjadi lebih mudah - tidak perlu khawatir tentang pertanyaan, pemuatan malas, fitur ORM, bekerja dengan objek C # biasa;
  • Tidak ada masalah yang terkait dengan akses dari utas berbeda;
  • Sangat cepat - tidak ada permintaan jaringan, tidak ada terjemahan kode ke dalam bahasa query, tidak ada (de) serialisasi objek;
  • Diperbolehkan untuk menyimpan data dalam bentuk apa pun - setidaknya dalam XML pada disk, setidaknya dalam SQL Server, setidaknya dalam Azure Table Storage.

Cons:


  • Penskalaan horisontal hilang, dan sebagai hasilnya, penerapan downtime nol tidak dapat dilakukan;
  • Jika aplikasi macet, Anda dapat kehilangan sebagian data. (Tapi aplikasi kita tidak pernah crash, kan?)

Bagaimana cara kerjanya?


Algoritma adalah sebagai berikut:


  • Pada awalnya, koneksi ke data warehouse dibuat, dan data diunduh;
  • Model objek, indeks primer, dan indeks hubungan (1: 1, 1: Banyak) dibangun;
  • Langganan dibuat untuk mengubah properti objek (INotifyPropertyChanged) dan untuk menambah atau menghapus elemen ke koleksi (INotifyCollectionChanged);
  • Ketika langganan dipicu - objek yang diubah ditambahkan ke antrian untuk menulis ke gudang data;
  • Secara berkala (berdasarkan waktu), perubahan pada penyimpanan disimpan dalam aliran latar belakang;
  • Saat Anda keluar dari aplikasi, perubahan pada repositori juga disimpan.

Contoh kode


Tambahkan dependensi yang diperlukan
//   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 

Kami menggambarkan model data yang akan disimpan di repositori
 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; } } 

Kemudian model objek:
 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; } 

Dan akhirnya, kelas repositori itu sendiri untuk mengakses data:
 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();  } } 

Buat instance dari ObjectRepository:


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

Jika proyek akan menggunakan HangFire
 public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) {  services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); } 

Sisipkan objek baru:


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

Dalam panggilan ini, objek ParentModel ditambahkan ke cache lokal dan ke antrian tulis ke database. Oleh karena itu, operasi ini membutuhkan O (1), dan Anda dapat segera bekerja dengan objek ini.


Misalnya, untuk menemukan objek ini di repositori dan memastikan bahwa objek yang dikembalikan adalah contoh yang sama:


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

Apa yang terjadi dengan ini? Setel <ParentModel> () mengembalikan TableDictionary <ParentModel> , yang berisi ConcurrentDictionary <ParentModel, ParentModel> dan menyediakan fungsionalitas tambahan untuk indeks primer dan sekunder. Ini memungkinkan Anda memiliki metode untuk mencari berdasarkan ID (atau indeks kustom arbitrer lainnya) tanpa menyebutkan secara lengkap semua objek.


Ketika objek ditambahkan ke ObjectRepository , langganan ditambahkan untuk mengubah propertinya, sehingga setiap perubahan properti juga menyebabkan objek ini ditambahkan ke antrian tulis.
Memperbarui properti dari luar terlihat sama dengan bekerja dengan objek POCO:


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

Anda dapat menghapus objek dengan cara berikut:


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

Ini juga menambahkan objek ke antrian hapus.


Bagaimana cara kerja konservasi?


ObjectRepository ketika mengubah objek yang dilacak (baik menambah atau menghapus, dan mengubah properti) memunculkan peristiwa ModelChanged , yang berlangganan IStorage . Implementasi IStorage, ketika peristiwa ModelChanged terjadi, merangkum perubahan dalam 3 antrian - tambah, perbarui, dan hapus.


Juga, implementasi IStorage selama inisialisasi membuat timer yang setiap 5 detik menyebabkan perubahan disimpan.


Selain itu, ada API untuk memaksa panggilan save: ObjectRepository.Save () .


Sebelum setiap penyimpanan, operasi pertama yang tidak berarti dihapus dari antrian (misalnya, acara duplikat - ketika suatu objek berubah dua kali atau penambahan / penghapusan cepat objek), dan hanya kemudian menyimpan itu sendiri.


Dalam semua kasus, seluruh objek dipertahankan, sehingga ada kemungkinan bahwa objek disimpan dalam urutan yang berbeda dari yang diubah, termasuk versi yang lebih baru dari objek daripada pada saat menambahkan ke antrian.


Apa lagi yang ada di sana?


  • Semua perpustakaan didasarkan pada .NET Standard 2.0. Ini dapat digunakan dalam proyek .NET modern.
  • API aman utas. Koleksi internal didasarkan pada ConcurrentDictionary , pengendali acara memiliki kunci atau tidak memerlukannya.
    Satu-satunya hal yang perlu diingat adalah memanggil ObjectRepository.Save ();
  • Indeks khusus (memerlukan keunikan):

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

Siapa yang menggunakannya?


Secara pribadi, saya mulai menggunakan pendekatan ini dalam semua proyek hobi, karena itu nyaman, dan tidak memerlukan biaya besar untuk menulis lapisan akses data atau menggunakan infrastruktur yang berat. Secara pribadi, sebagai aturan, menyimpan data dalam litedb atau dalam file biasanya cukup bagi saya.


Tetapi di masa lalu, ketika EscapeTeams, startup yang terlambat, dibuat dengan tim (mereka pikir itu adalah uang - tapi tidak, pengalaman lagi ) - mereka menggunakan Azure Table Storage untuk menyimpan data.


Rencana masa depan


Saya ingin memperbaiki salah satu kelemahan utama dari pendekatan ini - penskalaan horizontal. Untuk melakukan ini, Anda perlu salah satu transaksi terdistribusi (sic!), Atau membuat keputusan dengan tekad kuat bahwa data yang sama dari contoh berbeda tidak boleh berubah, atau biarkan mereka berubah sesuai dengan prinsip "siapa yang terakhir - itu benar."


Dari sudut pandang teknis, saya melihat skema berikut ini mungkin:


  • Simpan EventLog dan Snapshot alih-alih model objek
  • Temukan instance lain (tambahkan titik akhir dari semua instance? Penemuan Udp? Master / slave? Ke pengaturan)
  • Replikasi antara instance EventLog melalui salah satu algoritma konsensus, seperti RAFT.

Ada juga masalah lain yang mengganggu saya - adalah penghapusan cascading, atau mendeteksi kasus menghapus objek yang dirujuk dari objek lain.


Kode sumber


Jika Anda membaca hingga sini - maka hanya kode yang harus dibaca, itu bisa saja
ditemukan di github .

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


All Articles