ObjectRepository - .NET сиздин үй долбоорлоруңуз үчүн эс тутумдагы репозиторий үлгүсү

Эмне үчүн бардык маалыматтарды эс тутумда сактоо керек?

Вебсайтты же сервердик маалыматтарды сактоо үчүн, көпчүлүк акыл-эстүү адамдардын биринчи каалоосу SQL маалымат базасын тандоо болот. 

Бирок кээде маалымат модели SQL үчүн ылайыктуу эмес деген ой келет: мисалы, издөө же социалдык графикти курууда объекттердин ортосундагы татаал мамилелерди издөө керек. 

Эң начар жагдай - бул сиз командада иштегениңизде жана кесиптешиңиз тез суроону кантип түзүүнү билбейт. Негизги беттеги ТАНДОО акылга сыярлык убакытта бүтүшү үчүн N+1 маселелерин чечүүгө жана кошумча индекстерди түзүүгө канча убакыт короттуңуз?

Дагы бир популярдуу ыкма NoSQL болуп саналат. Бир нече жыл мурун бул теманын айланасында көп ызы-чуу болгон - каалаган ыңгайлуу учур үчүн алар MongoDBди жайгаштырышкан жана json документтери түрүндөгү жоопторго ыраазы болушкан. (Баса, документтердеги тегерек шилтемелерден улам канча балдак салууга туура келди?).

Мен башка, альтернативалуу ыкманы сынап көрүүнү сунуштайм - эмне үчүн бардык маалыматтарды колдонмонун эс тутумунда сактоого, аны мезгил-мезгили менен туш келди сактагычка (файл, алыскы маалымат базасы) сактоого аракет кылбаңыз? 

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

артыкчылыктары:

  • Берилиштерге жетүү жеңилдейт - сиз сурамдар, жалкоо жүктөө, ORM функциялары жөнүндө кабатырлануунун кереги жок, сиз кадимки C# объекттери менен иштейсиз;
  • Ар кандай жиптерден кирүү менен байланышкан көйгөйлөр жок;
  • Абдан тез - тармактык суроо-талаптар жок, кодду суроо тилине которуу жок, объекттерди сериалдаштыруунун (де) кереги жок;
  • Маалыматты каалаган формада сактоого болот - дисктеги XML, SQL Server же 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);

Бул чакыруу менен объект ParentModel жергиликтүү кэшке да, маалымат базасына жазуу кезегине да кошулат. Демек, бул операция O(1) алат жана бул объект менен дароо иштесе болот.

Мисалы, бул объектти репозиторийден табуу жана кайтарылган объект бир эле инстанция экенин текшерүү үчүн:

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

Эмне болуп жатат? Set() кайтып келет TableDictionary, камтыган ConcurrentDictionary жана баштапкы жана кошумча индекстердин кошумча функционалдуулугун камсыз кылат. Бул бардык объектилерди толугу менен кайталабастан Id (же башка каалаган колдонуучу индекстери) боюнча издөө ыкмаларына ээ болууга мүмкүндүк берет.

Объекттерди кошууда ObjectRepository алардын касиеттерин өзгөртүү үчүн жазылуу кошулат, ошондуктан касиеттердеги ар кандай өзгөрүү бул объекттин жазуу кезегине кошулушуна алып келет. 
Сырттан касиеттерди жаңыртуу POCO объекти менен иштөөгө окшош:

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

Сиз объектти төмөнкү жолдор менен жок кыла аласыз:

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

Бул дагы объектти жок кылуу кезегине кошот.

Сактоо кандай иштейт?

ObjectRepository көзөмөлдөнгөн объекттер өзгөргөндө (кошуу же жок кылуу, же касиеттерди өзгөртүү), окуяны козгойт ModelChangedжазылды IStorage. Ишке ашыруу IStorage окуя болгондо ModelChanged өзгөртүүлөр 3 кезекке коюлат - кошуу, жаңылоо жана жок кылуу үчүн.

Ошондой эле ишке ашыруу IStorage инициализациялоодо, алар ар бир 5 секундада сакталып турган өзгөрүүлөрдү пайда кылган таймерди түзүшөт. 

Мындан тышкары, сактоо чалууну мажбурлоо үчүн API бар: ObjectRepository.Save().

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

Бардык учурларда, учурдагы объект толугу менен сакталат, ошондуктан объекттер өзгөртүлгөндөн башка тартипте сакталышы мүмкүн, анын ичинде объекттердин кезекке кошулган учуруна караганда жаңыраак версиялары.

Дагы эмне бар?

  • Бардык китепканалар .NET Standard 2.0 негизделген. Ар кандай заманбап .NET долбоорунда колдонсо болот.
  • API жип коопсуз болуп саналат. Ички жыйымдар негизинде ишке ашырылат ConcurrentDictionary, окуяны иштеп чыгуучулардын же кулпулары бар же аларга муктаж эмес. 
    Эске алчу нерсе - чалуу 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

Source: www.habr.com

Комментарий кошуу