ObjectRepository - Таны гэрийн төслүүдэд зориулсан санах ойн .NET репозиторын загвар

Яагаад бүх өгөгдлийг санах ойд хадгалах ёстой вэ?

Вэбсайт эсвэл арын мэдээллийн санг хадгалахын тулд ихэнх эрүүл саруул хүмүүсийн хамгийн эхний хүсэл бол SQL мэдээллийн санг сонгох явдал юм. 

Гэхдээ заримдаа өгөгдлийн загвар нь SQL-д тохиромжгүй гэсэн бодол толгойд орж ирдэг: жишээлбэл, хайлт эсвэл нийгмийн график үүсгэх үед та объектуудын хоорондын нарийн төвөгтэй харилцааг хайх хэрэгтэй. 

Хамгийн муу нөхцөл байдал бол та багаар ажилладаг бөгөөд хамт ажиллагсад нь хэрхэн хурдан асуулга үүсгэхээ мэдэхгүй байх явдал юм. Үндсэн хуудсан дээрх СОНГОХ боломжийн хугацаанд дуусахын тулд та N+1 бодлогуудыг шийдэж, нэмэлт индекс байгуулахад хэр их цаг зарцуулсан бэ?

Өөр нэг түгээмэл арга бол NoSQL юм. Хэдэн жилийн өмнө энэ сэдвийн эргэн тойронд маш их шуугиан дэгдээж байсан - ямар ч тохиромжтой тохиолдлоор тэд MongoDB-г суулгаж, json баримт бичгийн хэлбэрээр хариулт авсандаа баяртай байсан. (Дашрамд хэлэхэд, баримт бичигт дугуй холбоос байгаа тул та хэдэн таяг оруулах шаардлагатай болсон бэ?).

Би өөр, өөр аргыг туршиж үзэхийг санал болгож байна - яагаад бүх өгөгдлийг програмын санах ойд хадгалж, үе үе санамсаргүй санах ойд (файл, алсын мэдээллийн сан) хадгалж болохгүй гэж? 

Санах ой хямд болсон бөгөөд ихэнх жижиг, дунд хэмжээний төслүүдэд боломжтой өгөгдөл нь 1 ГБ санах ойд багтах болно. (Жишээ нь, миний дуртай гэрийн төсөл санхүүгийн хянагч, өдөр хагасын турш миний зардал, үлдэгдэл, гүйлгээний статистик болон түүхийг хадгалдаг бөгөөд ердөө 45 МБ санах ой зарцуулдаг.)

Нөхцөл:

  • Өгөгдөл рүү хандах нь илүү хялбар болно - та асуулга, залхуу ачаалал, ORM функцүүдийн талаар санаа зовох шаардлагагүй, та энгийн C# объектуудтай ажилладаг;
  • Янз бүрийн хэлхээнээс хандахтай холбоотой асуудал байхгүй;
  • Маш хурдан - сүлжээний хүсэлт байхгүй, кодыг хайлтын хэл рүү орчуулах шаардлагагүй, объектуудыг цуврал болгох шаардлагагүй;
  • Диск дээрх XML, SQL сервер, Azure Table Storage гэх мэт ямар ч хэлбэрээр өгөгдлийг хадгалахыг зөвшөөрнө.

Нөхцөл байдал:

  • Хэвтээ масштаб алдагдаж, үүний үр дүнд тэг зогсолтыг байршуулах боломжгүй;
  • Хэрэв програм эвдэрсэн бол та хэсэгчлэн мэдээллээ алдаж болзошгүй. (Гэхдээ манай програм хэзээ ч гацдаггүй, тийм үү?)

энэ нь хэрхэн ажилладаг вэ?

Алгоритм дараах байдалтай байна.

  • Эхэндээ өгөгдөл хадгалах төхөөрөмжтэй холболт үүсч, өгөгдөл ачаалагдана;
  • Объектын загвар, үндсэн индексүүд болон харилцааны индексүүд (1:1, 1:Олон) бүтээгдсэн;
  • Объектын шинж чанарыг өөрчлөх (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));

Дараа нь юу болох вэ? Тохируулах () буцаж ирдэг Хүснэгтийн толь бичиг, агуулсан Concurrent толь бичиг анхдагч болон хоёрдогч индексүүдийн нэмэлт функцээр хангадаг. Энэ нь бүх объектыг бүрэн давталгүйгээр Id (эсвэл бусад дурын хэрэглэгчийн индекс) -ээр хайх аргуудтай болох боломжийг танд олгоно.

Объект нэмэх үед ObjectRepository Тэдний шинж чанарыг өөрчлөхийн тулд захиалга нэмэгдсэн тул шинж чанаруудын өөрчлөлт нь энэ объектыг бичих дараалалд нэмэхэд хүргэдэг. 
Гаднаас нь шинж чанаруудыг шинэчлэх нь POCO объекттой ажиллахтай адил харагдаж байна:

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

Та объектыг дараах аргаар устгаж болно.

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

Энэ нь мөн объектыг устгах дараалалд нэмнэ.

Хадгаламж хэрхэн ажилладаг вэ?

ObjectRepository хяналтанд байгаа объектууд өөрчлөгдөх үед (нэмэх, устгах, эсвэл шинж чанарыг өөрчлөх) үйл явдлыг үүсгэдэг Загвар өөрчлөгдсөнбүртгүүлсэн Хадгалах. Хэрэгжилт Хадгалах үйл явдал тохиолдох үед Загвар өөрчлөгдсөн өөрчлөлтийг нэмэх, шинэчлэх, устгах гэсэн 3 дараалалд оруулдаг.

Мөн хэрэгжүүлэлтүүд Хадгалах эхлүүлэх үед тэд 5 секунд тутамд өөрчлөлтүүдийг хадгалахад хүргэдэг таймер үүсгэдэг. 

Нэмж дурдахад дуудлагыг хадгалах API байдаг: ObjectRepository.Save().

Хадгалах бүрийн өмнө эхлээд утгагүй үйлдлүүдийг дарааллаас хасдаг (жишээлбэл, давхардсан үйл явдлууд - объектыг хоёр удаа өөрчлөх эсвэл объектыг хурдан нэмэх/хасах үед), зөвхөн дараа нь өөрөө хадгална. 

Бүх тохиолдолд одоогийн объектыг бүхэлд нь хадгалдаг тул объектуудыг дараалалд нэмсэн үеийнхээс шинэ хувилбаруудыг оруулаад өөрчилснөөс өөр дарааллаар хадгалах боломжтой.

Өөр юу байна?

  • Бүх номын сангууд нь .NET Standard 2.0 дээр суурилдаг. Орчин үеийн ямар ч .NET төсөлд ашиглаж болно.
  • API нь урсгалтай аюулгүй. Дотоод цуглуулгыг үндэслэн хэрэгжүүлдэг Concurrent толь бичиг, үйл явдал зохицуулагчид түгжээтэй эсвэл хэрэггүй. 
    Санаж байх ёстой цорын ганц зүйл бол залгах явдал юм ObjectRepository.Save();
  • Дурын индексүүд (өвөрмөц байдлыг шаарддаг):

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

Хэн үүнийг ашигладаг вэ?

Би хувьдаа энэ аргыг хоббитой холбоотой бүх төслүүдэд ашиглаж эхэлсэн, учир нь энэ нь тохиромжтой бөгөөд өгөгдөлд нэвтрэх давхарга бичих эсвэл хүнд дэд бүтцийг байрлуулахад их хэмжээний зардал шаарддаггүй. Би хувьдаа litedb эсвэл файлд өгөгдөл хадгалах нь надад хангалттай байдаг. 

Гэхдээ өмнө нь одоо татан буугдсан EscapeTeams стартап (Би энд байна, мөнгө гэж бодсон - гэхдээ үгүй, дахин туршлага) - Azure Table Storage-д өгөгдөл хадгалахад ашигладаг.

РџР »Р ° РЅС <РЅР ° Р ± САРРґЭсарС ‰ РμРμ

Би энэ аргын гол сул талуудын нэг болох хэвтээ масштабыг засахыг хүсч байна. Үүнийг хийхийн тулд та тараасан гүйлгээ (sic!), эсвэл өөр өөр тохиолдлуудын ижил өгөгдөл өөрчлөгдөх ёсгүй гэсэн хатуу шийдвэр гаргах эсвэл "хамгийн сүүлд хэн нь зөв" гэсэн зарчмын дагуу өөрчлөх хэрэгтэй.

Техникийн үүднээс авч үзвэл би дараах схемийг аль болох харж байна.

  • Объект загварын оронд EventLog болон Snapshot хадгал
  • Бусад тохиолдлуудыг олох (бүх тохиолдлын төгсгөлийн цэгийг тохиргоонд нэмэх үү? udp илрүүлэх? мастер/боол?)
  • RAFT гэх мэт аливаа зөвшилцлийн алгоритмаар EventLog жишээнүүдийн хооронд хуулбарлах.

Мөн миний санааг зовоож буй өөр нэг асуудал байна - каскад устгах, эсвэл бусад объектуудаас холбоос бүхий объектуудыг устгах тохиолдлуудыг илрүүлэх. 

Эх код

Хэрэв та энд хүртэл уншсан бол кодыг уншихад л үлддэг; үүнийг GitHub дээрээс олж болно:
https://github.com/DiverOfDark/ObjectRepository

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх