Ilova qatlamida munosabatlarni boshqarish

Ilova qatlamida munosabatlarni boshqarish

Oddiy server ilovalarida, subyektlar o'rtasidagi munosabatlar ko'pincha ma'lumotlar bazasining tashqi kalitlari va JPA assotsiatsiyalari orqali ifodalanadi. Ushbu yondashuv kuchli; bu ota-onalarsiz bolalarni saqlashni oldini oladi, ota-ona o'chirilganda bolalarning o'chirilishini tarqatadi va saqlash va o'chirish jarayonlarini ob'ekt grafiki bo'yicha birga boshqarishga imkon beradi.

Biroq, haqiqiy domendagi munosabatlar o'zgarishlari oddiy repositoriya darajasidagi munosabatlarni boshqarishdan ko'proq kengdir. Agar bir subyekt o'chirilsa, boshqa bir subyektni ham o'chirish kerak bo'lishi mumkin, lekin ba'zi hollarda avval bo'sh ota yaratish, bir vaqtda aka-uka subyektlarni yaratish yoki bog'langan qiymatlarni qayta hisoblash kerak bo'lishi mumkin. Ushbu maqola dastur darajasidagi munosabatlarni qanday boshqarishimiz va Vizend asosiy loyihasining JPO tuzilmasi asosida buni qanday qo'shishimiz kerakligini umumlashtiradi.

1. JPO'ni qisqa saqlash

Vizend asosiy loyihasining JPO'si umuman olganda qisqa. JPO'lar JPA subyektlari, lekin ular domen ob'ektlarini saqlash uchun saqlash modeliga yaqinroq, boy munosabatlarni ifodalovchi ORM modelidan ko'ra. Boshqacha qilib aytganda, JPO bitta qatorni saqlash va uni domen ob'ektiga aylantirishga qaratilgan, domenning butun ob'ekt grafikasini o'z ichiga olmaydi.

Oddiy JPA modelida, ota-onalar va bolalar ob'ekt havolalari orqali bog'lanishi mumkin, ota-onalar bolalarning doimiy to'plamiga ega. Biroq, Vizendning asosiy modeli bolalar ota-onaning ID'sini @FieldSourceId sifatida saqlaydi, va ota-ona bolalarni tezkor ro'yxatda saqlaydi. JPO ushbu tuzilmani oddiy maydon markazli usulda saqlaydi, hech qanday munosabat xaritasini qo'ymaydi.

class Board {
    private String id;
    private String name;
    private transient List<Card> cards;
}

class Card {
    private String id;

    @FieldSourceId
    private String boardId;

    private String title;
    private transient List<Comment> comments;
}

class Comment {
    private String id;

    @FieldSourceId
    private String cardId;
}

Ushbu tuzilmaning maqsadi saqlash modelini oddiy saqlash va munosabatlar dastur kodida talqin qilinishi va boshqarilishi uchun yondashuvni ko'rsatishdir, ma'lumotlar bazasi darajasidagi munosabatlarni boshqarishga katta qaramaslikdir.

2. Nima uchun munosabatlar dasturlar tomonidan boshqariladi, repositoriyalar emas

Eng katta sabablardan biri DB sotuvchisi va ishga tushirish muhitining o'zgarmasligidir. Asosiy loyihalar yoki kodni avtotashish asosida ishlab chiqilgan loyihalar boshlanishidan boshlab yo'lga qo'yiladigan aniq ma'lumotlar bazasiga ega bo'lmasligi mumkin. Ko'pgina RDBMSlar tomonidan tashqi kalit cheklovlari qo'llab-quvvatlangan bo'lsa-da, cheklovlarning qanday shakllantirilishi, havolalar harakatlarining o'ziga xos xususiyatlari, DDL avtomatik shakllantirish, migratsiya strategiyalari va triggerlar foydalanilishi ma'lumotlar bazasi va ishga tushirish muhitiga qarab farqlanadi.

Agar domen qoidalari repositoriya darajasidagi munosabatlar boshqaruviga juda bog'liq bo'lsa, loyihaning ma'lumotlar bazasi uchun imkoniyatlar cheklangan bo'ladi. Ma'lumotlar bazasining cheklovlari, triggerlari va kaskad harakatlaridagi xususiyatlar asosida domen qoidalarini loyihalash, boshqa ma'lumotlar bazasiga o'tganda kutilganidan ko'ra katta farqlarga olib kelishi mumkin. Shuning uchun, asosiy model oddiy ID havolalarini saqlash uchun foydalidir, shunda dastur kodi munosabatlar qoidalarini talqin qila oladi, belgilangan DB xususiyatlariga qaramaslik kerak.

Ikkinchi sababi, munosabatlardagi o'zgarishlar o'chirish tarqatilishidan kengroqdir. Repositoriya darajasidagi munosabatlarni boshqarish bolalarni ota-onalarsiz saqlashni yoki ota-o'na o'chirilganda bolalarni birgalikda o'chirishni oldini olishda juda kuchli. Biroq, haqiqiy domen qoidalari bu yerda to'xtamaydi.

Masalan, A o'chirilganda B o'chirilishi kerak bo'lgan hollarda, bu ba'zi darajalarni repositoriya darajasidagi havola harakatlari yoki JPA remove kaskadlari bilan ifodalash mumkin. Biroq, B saqlanayotgan vaqtida ota A mavjud emasligi sababli A avval yaratilishi kerak bo'lgan yoki A aka-uka B bilan birga yaratilishi kerak bo'lgan yoki A ning qiymatini B ning qiymatini qayta hisoblash uchun o'zgartirish yoki o'chirish kerak bo'lgan vaziyatlarda soddalashgan tashqi kalit harakatlarini ifodalash qiyin.

Ma'lumotlar bazasi triggerlaridan foydalanish DB ichida ba'zi jarayonlarni o'tkazishga imkon beradi. Biroq, buni amalga oshirish domen logikasini ma'lumotlar bazasi ichida yashirishga olib keladi, dastur kodidan emas. Uni sinov va izlash qiyinlashadi, xizmat chegaralarini ajratish qiyinlashadi. Dastur o'sishi bilan, munosabatlar o'zgarishlari avtomatik repositoriya harakatlaridan ko'ra foydalanish holatlari va domen qoidalari bilan ko'proq moslashtiriladi.

3. Dastur kodida hal qilinganda yuzaga keladigan muammolar

Dastur kodida munosabatlarni boshqarish orqali domen qoidalarini yanada aniqroq ifodalash mumkin. Taxtani o'chirganda Kartalarni o'chirish va Kartani o'chirganda Kommentlarni o'chirish logikasi kodda aniqdir. O'chirishgina emas, balki tarixni yozib olish, holatni tekshirish, ruxsatlarni tekshirish va proyeksiya yangilash kabi domen xatti-harakatlari ham qo'shilishi mumkin.

Muammo shundaki, munosabatlarni boshqarish kodlari tobora tarqoq bo'lib boradi. Dastlab Taxta o'chirilganda faqat Kartani o'chirish kerak. Keyinchalik, Kartada Kommentlar yaratilganda, Kommentni o'chirish logikasi Kartani o'chirish logikasiga qo'shiladi. E'lon Qoidasi, DashboardVidjeti va Faoliyat Jurnali kabi tushunchalar paydo bo'lgandan so'ng, o'chirish tarqatilishi kodi kengayadi.

Birinchi muammo ommalashuvdir. Yangi havola maydoni qo'shilsa ham, mavjud o'chirish lohiyasi avtomatik ravishda xabardor qilinmaydi. Dasturchi o'sha munosabatni esda tutishi va o'chirish lohiyasiga aks ettirishi kerak. Agar e'tibordan chetda qoldirilsa, yetim ma'lumotlar qoladi.

Ikkinchi muammo ko'p yo'llardir. Agar A dan B orqali C ga va A dan D orqali C ga bir vaqtda yo'llar bo'lsa, A ning o'chirish jarayonida C ikkita marta yetib borilishi mumkin. O'chirish faqat bir marta amalga oshirilishi kerak, ammo har bir qo'llanma yoki harakat mustaqil ravishda keyingi o'chirishlarni chaqirsa, bir xil C o'chirishini ikki marta chaqirish xavfi mavjud.

A -> B -> C
A -> D -> C

Uchinchi muammo ta'sir doirasini tushunishda qiyinchilikdir. A ni o'chirishdan oldin, qaysi sub'ektlar birga ta'sirlanishini bilish qiyin. Agar munosabatlarni boshqarish ombor darajasida bo'lsa, DB yoki JPA belgilangan qoidalarni bajaradi, ammo ilova kodidagi kaskad jarayon yo'llari bir necha harakalarga va qo'llanmalarga tarqalgan.

Ushbu muammolar ombor darajasidagi munosabatlarni boshqarish qo'llanilmaganligi sababli emas, balki munosabatlarning tarqatilishi ilova kodida bajarilish oqimini izchil boshqarilmasdan amalga oshirilganligi sababli yuzaga keladi.

4. Hal qilish yo'nalishi: DataEvent va CascadeContext

Flow, Logic, PolicyHandler va CascadeContext ga bo'lingan, har bir ro'yxatni alohida qilib belgilash va o'z vazifalarini bajarishiga imkoniyat yaratadi.

Flow creates execution context
Logic performs its own CUD
Logic publishes DataEvent
PolicyHandler reacts to DataEvent
CascadeContext prevents duplicate actions

Flow foydalanish holatining kirish nuqtasi. Bu yerda, bitta bajarilish doirasi yaratiladi va CascadeContext tayyorlanadi. Logic haqiqiy CQRSni bajaradi. U o'z sub'yektini yaratadi, o'zgartiradi va o'chiradi va DataEvent ni nashr etadi. PolicyHandler DataEvent ni oladi va tegishli Logicni chaqiradi.

Misol uchun, Agar Board ni o'chirish so'rovi kelib tushsa, Flow kontekstni yaratadi va BoardLogicni chaqiradi. BoardLogic faqat Board ni o'chirish va BoardRemoved voqeasini nashr qilish uchun javobgardir. BoardRemoved voqeasini olgach, BoardPolicyHandler Card ro'yxatini topadi va CardLogicni chaqiradi. Keyin CardLogic CardRemoved voqeasini nashr etadi va CardPolicyHandler CommentLogic ni chaqiradi.

Ushbu tuzilmada har bir Logic faqat o'zini boshqaradi. BoardLogic Cards haqida bilmaydi. CardLogic Comments haqida bilmaydi. PolicyHandler munosabatlarning tarqatilishiga javobgardir.

Voqealarga asoslangan tarqatishdagi eng ehtiyotkor nuqta ikki marta ishlov berishdir. Bir xil sub'ekt ko'p yo'llar orqali etib borilishi mumkin. Buni oldini olish uchun CascadeContext bir martalik bajarilish oqimida allaqachon bajarilgan harakatlarni eslab qolish uchun taqdim etiladi.

public class CascadeContext {
    private final Set<String> completed = new HashSet<>();

    public boolean enter(Class<?> type, String id, String action) {
        String key = type.getName() + ":" + id + ":" + action;
        return completed.add(key);
    }
}

PolicyHandler keyingi Logicni chaqirishdan oldin kontekstni tekshiradi. Agar harakat allaqachon bajarilgan bo'lsa, u uni o'tkazib yuboradi, agar bu birinchi uchrashgan harakat bo'lsa, keyingi Logicni chaqiradi.

public class BoardPolicyHandler {
    public void onBoardRemoved(DataEvent event) {
        CascadeContext context = cascadeContextHolder.current();
        List<Card> cards = cardStore.findByBoardId(event.entityId());

        for (Card card : cards) {
            if (!context.enter(Card.class, card.getId(), "REMOVE")) {
                continue;
            }

            cardLogic.removeCard(card.getId());
        }
    }
}

Shu tarzda, agar A dan B orqali C ga va A dan D orqali C ga etib boradigan tuzilmalar bo'lmasa, C faqat bir marta qayta ishlanadi. Agar bir xil harakat yana kelsa, context.enter false qaytaradi va ushbu qo'llanma keyingisiga o'tadi.

Ushbu tuzilmani qo'llashda Spring voqealarining sinxron tarzda bajarilishi aniq bo'lishi kerak. Bir xil tranzaksiyada tarqatish uchun umumiy sinxron EventListener oqimi ishlatilishi kerak. Asinxron voqealarni yoki qo'llanmani amalga oshirgandan keyin voqealarni ishlatish bir xil CascadeContext va tranzaksiyani saqlashni qiyinlashtiradi.

5. Metadata yordamida munosabatlarni kuzatish

CascadeContext bajarilish takrorlanishini oldini oladi. Biroq, qaysi munosabatlar mavjudligini, qaysi munosabatlarning qayta ishlanayotganini va qaysi munosabatlar yetishmayotganini bilish uchun alohida kuzatuv zarur. Bu yerda, FieldSourceId va RelationPolicy birga ishlatilishi mumkin.

FieldSourceId ma'lumotlar bog'lanishlarini aniqlash uchun ishlatilishi mumkin. Masalan, agar FieldSourceId Card.boardId ga bog'langan bo'lsa, dastur Card ning Board ga havola qilganini biladi. Ushbu ma'lumotni skaner qilib, havola grafigi yaratish mumkin.

ReferenceGraph

Board
Card.boardId

Card
Comment.cardId

Biroq, faqatgina havola grafigi ota-onaning CUD ning bolalarga qanday ta'sir qilishini bizga ayta olmaydi. Board o'chirilganda, Card ni o'chirish, Card.boardId ni tozalash yoki o'chirishni oldini olish yaxshi bir siyosatdir.

Buni amalga oshirish uchun, PolicyHandler metodlarida RelationPolicy kabi metadata bo'lishi mumkin. Ushbu annotatsiya handler ning xulq-atvorini o'zgartirmaydi. Asl xulq-atvor metod tanasida qoladi. Annotatsiya, ushbu handler qaysi turdagi havola tarqatilishiga javobgar ekanligini ko'rsatish uchun metadata sifatida ishlaydi.

@RelationPolicy(
    source = Board.class,
    event = DataEventType.REMOVED,
    target = Card.class
)
public void onBoardRemoved(DataEvent event) {
    List<Card> cards = cardStore.findByBoardId(event.entityId());

    for (Card card : cards) {
        cardLogic.removeCard(card.getId());
    }
}

Bu yo'l bilan, dasturda ikkita turdagi grafiklar bo'lishi mumkin. Havola grafi kim kimni havola qilganini ko'rsatadi. Tarqatish grafi qaysi voqealar qaysi maqsadlarga ta'sir qilishini ko'rsatadi.

ReferenceGraph
FieldSourceId based graph

PropagationGraph
RelationPolicy based graph

Ikkita grafikni taqqoslash orqali, biz ta'sir doirasini, yo'qolgan siyosatlarni, takroriy yo'llarni va aylanma yo'llarni ko'rib chiqishimiz mumkin. Masalan, agar havola grafida Card dan NotificationRule ga bog'lanish bo'lsa, lekin tarqatish grafida CardRemoved hodisasi uchun NotificationRule siyosati bo'lmasa, bu yo'qolgan siyosat sifatida ko'rilishi mumkin. Aksincha, agar A dan C gacha B orqali va A dan C gacha D orqali yo'llar bo'lsa, grafik tahlili davomida ko'p yo'llar haqida oldindan ogohlantirish berishimiz mumkin.

Impact
Missing policy
Duplicate path
Cycle

Ushbu strukturaning afzalligi shundaki, u annotatsiyalar ichida domen mantiqini yashirmasdan bog'lanishlarni boshqarishda kuzatish imkoniyatini ta'minlaydi. FieldSourceId - havola metadata, RelationPolicy - tarqatish metadata, va asl domen reaksiyalari PolicyHandler ning metod tanasida qoladi.

Nihoyat, ushbu tuzilma ta'sir doirasining oldindan ko'rinishiga kengaytirilishi mumkin. CascadeContext ga dryRun flag va ta'sir ro'yxatini qo'shib, Logic dry-run holatida bo'lganida, biz faqat haqiqiy repozitoriy o'zgarishlarini o'rtaga qo'ymasdan DataEvents ni e'lon qilishimiz mumkin. Bu haqiqiy bajarilishiga o'xshash voqealar zanjirini kuzatishga imkon beradi, shu bilan birga DB o'zgarishlaridan qochib, qaysi jonzotlar ta'sirlanganini hisoblash mumkin. Biroq, dry-run faqat barcha DataEvent handlerlari bir xil CascadeContext qoidalariga amal qilsa ishonchli hisoblanadi, shuning uchun ta'sir doirasi boshqariladigan sohalardan boshlab ehtiyotkorlik bilan joriy qilish yaxshiroqdir.

6. Xulosa

Ma'lumotlar bazasi chetki kalitlari va JPA kaskadlari bog'lanishlarni boshqarish uchun kuchli vositalardir. Biroq, barcha bog'lanish o'zgarishlari repozitoriya darajasidagi umr sikli tarqatilishi bilan tushuntirilmaydi. Dasturlar o'sib borishi bilan, bog'lanish o'zgarishlari o'chirish tarqatilishidan tashqari, yaratish to'g'rilanishlari, aka-uka yaratishlar, qaytadan hisoblashlar va proyeksiya yangilanishlarini o'z ichiga oladi.

Vizend ning asosiy JPOsining qisqa va lo'g'jon bo'lishi, bolalar ota-onaning ID sini FieldSourceId sifatida ushlab turishlari va ota-onaning bolalar ro'yxatini vaqtinchalik qilishlari ushbu kontekstda tushunilishi mumkin. Bu bog'lanishlarni dastur kodida boshqarish uchun joy qoldirish tanlovi, DB/JPA ga kuchli ravishda delegatsiya qilmasdan.

Albatta, ushbu tanlov mas'uliyatlarni ham olib keladi. Dastur ma'lumotlar bazasi himoya qilgan yaxlitlikni to'g'ridan-to'g'ri boshqarishi va tarqatish yo'qotilishiga yoki takroriy bajarilishlarga yo'l qo'ymasligi kerak. Bunga erishish uchun biz Flow, Logic, PolicyHandler va CascadeContext rolini, shuningdek, FieldSourceId va RelationPolicy bilan bog'lanishlarni kuzatish tuzilmasini muhokama qildik.

Logic changes only its own entity
Logic publishes DataEvent
PolicyHandler reacts to DataEvent
CascadeContext prevents duplicate actions
FieldSourceId describes reference graph
RelationPolicy describes propagation graph

Ushbu tuzilma, faqatgina repozitoriya darajasidagi bog'lanishlarni dastur kodida taqlid qilish maqsadida emas, balki bog'lanish o'zgarishlari ma'nosini domen kodida qoldirishga urinayotgan va bajarilish oqimini barqaror boshqarishga harakat qilishdir.

O'qiganingiz uchun rahmat.

HHkk

Site footer