Systémové řazení entit
Systémové řazení entit spočívá v automatické tvorbě spojového seznamu pro určení pořadí entit. Výpočet pořadí a přerovnání listu je složitá a výpočetně náročná operace.
Pokud je priorita stanovena na straně ERP či můstku je vhodné systémové zpracování priorit vypnout, po vypnutí priorit pro konkrétní entitu již není možné provádět D&D operace a změnit pořadí entit v AR.
Entity s prioritou
| Entita | Popis |
|---|---|
| Brand | pořadí v seznamu značek |
| Category | pořadí kategorií v rámci nadřazené kategorie |
| ProductCategory | pořadí produktů v dané kategorii |
| ProductGroup | pořadí produktů v dané skupině |
| Product | globální pořadí produktů (v případě zobrazení seznamu produktů bez vazby na konkrétní kategorii nebo skupinu) |
| Group | pořadí skupin při výpisu skupin |
| ParameterType | pořadí parametrů |
| ParameterItem | pořadí hodnot parametrů (nelze importovat z externího systému momentálně, vždy lze definovat pouze v edeem) |
| ParameterGroup | pořadí skupin parametrů |
| Stock | pořadí skladů |
| ProductInSet | pořadí položek setu v rámci setu |
| ProductVariant | pořadí variant v rámci masteru |
| PaymentMethod | pořadí platebních metod |
| ShippingMethod | pořadí doprav |
| PriceList | pořadí ceníků (má smysl pouze pokud se cena vyhodnocuje na základě pořadí ceníků a ne nějakým jiným mechanismem - třeba nejnižší cenou) |
| PickupPoint | pořadí odběrných míst pro osobní odběr |
Vypnutí systémového řazení entit
Vypnutí systémového řazení entit je možné pouze pomocí Java kódu v konkrétní projektové implementaci.
Vypnout řazení můžeme pro všechny entity, nebo entity můžeme specifikovat.
Vypnutí systémového řazení pro konkrétní entitu
public class ProjectFeature extends AbstractBaseEdeeShopFeature<EdeeShopModuleConfig> { @Override public void afterSpringInitialization(AbstractRefreshableApplicationContext moduleContext) { super.afterSpringInitialization(moduleContext); PriorityService priorityService = moduleContext.getBean(SharedBeanNames.PRIORITY_SERVICE, PriorityService.class); priorityService.disableDefaultPriorityManagement(Category.ENTITY_NAME); }}Vypnutí systémového řazení pro všechny entity
public class ProjectFeature extends AbstractBaseEdeeShopFeature<EdeeShopModuleConfig> { @Override public void afterSpringInitialization(AbstractRefreshableApplicationContext moduleContext) { super.afterSpringInitialization(moduleContext); PriorityService priorityService = moduleContext.getBean(SharedBeanNames.PRIORITY_SERVICE, PriorityService.class); priorityService.disableDefaultPriorityManagement(); }}Implementační detaily
Datový model
Každá entita zařazená do systémového řazení implementuje rozhraní WithPriority a v tabulce má dva sloupce:
priority(long) — relativní pořadí v rámci nadřazeného klíče. Vyšší hodnota = výše v seznamu. Hodnota-1(NO_PRIORITY) znamená, že entita ze spojového seznamu vypadla (TRASHED, ukončený prodej, varianta produktu absorbovaná pod jiný master atd.).previousEntity(int, FK na stejnou tabulku,UNIQUE) — id předchůdce ve spojovém seznamu. První entita mápreviousEntity = NULL. Unikátní omezení zaručuje, že každá entita má nejvýše jednoho následníka v rámci jednoho parent klíče.
Skupinu, v níž se entity řadí, definují parentKeys v MovePrioritySettings (např. idCatalog
pro Product, idParameterType pro ParameterItemInType, idMasterProduct pro variantu).
Spojový seznam vždy existuje samostatně pro každou kombinaci parentKeys.
Konfigurace
Konfigurace se registruje při startu kontextu (typicky v *PriorityDao.initPrioritySettings())
a sestává ze dvou objektů:
MovePrioritySettings— strukturální popis (řídí se podle entity, nemění se za běhu):parentKeys,joinTable,createJournal,publishingEntityType,publishingType,additionalCondition, callbacky pro kaskádní akce a volitelnýcreateJournalPublishPreviousCatalogEntityFunctionpro custom SQL publikace.MovePriorityBehaviorSettings— runtime přepínače (lze měnit za běhu — kód i testy toho využívají): viz Přepínače chování.
Dále PriorityService udržuje seznam equivalentViews (např. ParameterItem ↔
ParameterItemInType) — toggle aplikovaný na jeden název prosakuje na všechny ekvivalenty.
Interceptory
MovePriorityPersistenceInterceptor (základní)
Sedí na třech metadatových hookcích pro každou entitu s MovePrioritySettings:
-
intercept(MethodType.StoreContainer, container)— beforeStore. Podle stavu a vstupních příznaků zvolí jednu z větví vbeforeStore:- trash / force-unlink — entita přechází do
TRASHED(nebo je jí force unlink vynucen):processDeletingnačte původní data z DB (moveItemDao.getOriginalData), zavolámoveAfterDeleting→unlinkaktualizujepreviousEntityu následníka,movePrioritystlačí zbylé priority dolů,setActualEntityPriority(entityId, NO_PRIORITY)ukončí odpojení apublishChangeszapíše záznamy do journalu prochangedIds. - restore / force-link — entita se vrací z
TRASHED:processRestoringpřiřadí nejnižší (resp. nejvyšší) prioritu volného slotu,moveAfterRestoringzavolálink, které nastavípreviousEntityna nové entitě i na novém následníkovi, a publikuje. VariantaprocessRestoringForCallbackse používá v callback cestě (např.BaseVariantProductRelationpři restore variantního produktu) a vynucuje provedenílinkinline — bez ní by zaplaceNewEntityToBeginning=truespojový seznam zůstal nezalinkovaný, protože callback restore neproudí přesafterStore. - nová entita —
processCreatingpřiřadí prioritu (nejnižší−1 / nejvyšší+1) a předvyplnípreviousEntitypřesmoveItemDao.getLastEntityId. Skutečnýlink(vč. publikace) proběhne až vafterStore, kdy je známé id nové entity. - změna priority existující entity —
updatePriority:unlinkna původní pozici (publikuje),movePriorityposune sousedy mezi starou a novou prioritou, nastaví novou prioritu,linkna nové pozici (publikuje). Pokud se zároveň měníparentKeys(přesun mezi nadřazenými skupinami), provádí se obě fáze samostatně.
Při
disableDefaultPriorityManagement=trueinterceptor vrátí container okamžitě a žádná z výše uvedených operací neproběhne — priorita zůstane na hodnotě, kterou nastavil volající. - trash / force-unlink — entita přechází do
-
intercept(MethodType.RemoveContainers, query)— beforeRemove (skutečné DELETE). ZWHEREklauzule dotazu extrahuje id mazaných entit a pro každé spustíprocessDeleting. Bezeq/innaid(neboMovePriorityRemoveHint) vyhodí výjimku — hromadný DELETE bez znalosti konkrétních id by porušil spojový seznam. -
intercept(MetadataContainerProvider, …)— afterStore. Pouze proplaceNewEntityToBeginning=true: dokončílinknové entity (id už je k dispozici po insertu) a zapíše publikační záznamy. V default módu (placeNewEntityToBeginning=false) je linkování pro novou entitu hotové už vbeforeStorepřesprocessCreatinga tato cesta neběží.
Spojový seznam — SQL operace
Tři DAO metody nad tabulkou entity:
unlink(parentKeys, entityId, originalPriority)— najde předchůdce (x2) a následníka (x3) entity, nastavíx3.previousEntity = x2.id, vrátí[x3.id]jako kandidáta na publikaci. Před updatem zavolácleanActualEntityPreviousLink(entityId), který odpojí samotnou entitu — to obchází unique constraint napreviousEntityběhem přemisťování.link(parentKeys, entityId, priority)— symetrická operace: najde nového předchůdce a následníka, nastavíentity.previousEntityax3.previousEntity = entityId, vrátí oba změněné id pro publikaci.movePriority(parentKeys, newPriority, originalPriority)— hromadně posune priority ostatních entit mezioriginalPriorityanewPriorityo ±1 tak, aby vznikl/zanikl slot. Provádí to jediným UPDATE; momentální kolize na páru(parent, priority)jsou ošetřené tím, že na tom páru není unique constraint (na rozdíl odpreviousEntity).publishChanges(prioritySettings, changedIds, entityId)— pro každou změněnou entitu vloží doT_EDEESHOP_ENTITY_PUBLISHING_JOURNALzáznam typuprioritySettings.getPublishingType()(defaultCHANGE_PRIORITY). Sloupecdataobsahuje id katalogové entity předchůdce (prev.idCatalogEntity) neboNULL. Konzument (Evita publishing) záznamy zpracovává v pořadí podle id a aplikuje pouze poslední záznam pro daný(code, publishingType); mezilehlé záznamy jsou validní snapshoty průběhu, finální musí odpovídat DB.
ParameterItemMovePriorityPersistenceInterceptor
Speciální interceptor pro doménu parametrů, který dědí ze základního a registruje se navíc na
view ParameterItem. Základní interceptor je pro ParameterItem explicitně vypnutý přes
movePriorityPersistenceInterceptor.addDisabledFor(ParameterItem.ENTITY_NAME) — celý lifecycle
parametrů řídí tato specializace.
Klíčové větve:
- trash celého
ParameterItem(processTrashOrRestoreBeforeStore) — iteruje všechnyparameterInTypes(jak standardní, tak syntetickou stranu) a pro každý vyvoláprocessRemoved. Pro odpojené (priority == NO_PRIORITY) PITy jeprocessRemovedno-op. Originální prioritu si bere z DB (moveItemDao.getOriginalData) — nikoli z in-memory entity, protože volající může předat zastaralou referenci. - restore celého
ParameterItem— symetricky voláprocessRestoringpro každý PIT, který není absorbovaný (idParameterItemSynth == null). - změna
idParameterItemSynthna PITu (processParameterItemRelationBeforeStore):- null → non-null = absorpce:
processRemovedodpojí PIT ze spojového seznamu jehoparameterType. - non-null → null = uvolnění z absorpce:
processRestoringzalinkuje PIT zpět na konec seznamu (nejnižší−1 priorita).
- null → non-null = absorpce:
Druhá fáze kaskády při absorpci / uvolnění probíhá v after-persistence hooku
ParameterItemEntityAfterPersistenceWithStorageFlagInterceptor. Ten po prvním uložení
detekuje, že je nutné srovnat syntetickou stranu (shouldUpdateRelation):
- Při absorpci, kdy už všechny standardPITy mají
idParameterItemSyntha item má ještě syntetický PIT, odstraní syntetické PITy (removeSyntheticTypeRelations) a uloží znovu — ve druhém cyklu pakprocessParameterItemRelationBeforeStorezpracuje syntetický PIT vremovedItemspřesprocessRemoveda publikuje procolorSynthview. - Při uvolnění, kdy item zůstal bez syntetické relace, přidá nový syntetický PIT
(
addNewParameterInType(synthParameterType)) a uloží znovu.processChangedOrNewv druhém cyklu nastaví prioritu apreviousEntity, ale samotnýlinkSQL ani publikaci nezavolá (proplaceNewEntityToBeginning=false). Proto interceptor po druhém ukládání explicitně publikuje přespublishNewSyntheticParameterInType. Tento publish se spouští jen když zároveň došlo ke změně existujícího PITu (modifiedItems != ∅), aby se neduplikoval u prvotního vytvořeníParameterItem, kde se synth PIT zakládá v rámci téže transakce a konzument o existenci entity ví z událostí o katalogové entitě.
Specifika doménových entit
Product / variant
Productse řadí v rámci katalogu (parentKeys = idCatalog). Varianty mají na úrovni Produktupriority = NO_PRIORITY(nejsou v katalogovém seznamu) a jsou zařazené ve spojovém seznamuVariantProductRelationv rámci masteru (parentKeys = idMasterProduct). Varianty publikují pod typemCHANGE_PRIORITY_VARIANT—CHANGE_PRIORITYfiltr je nevidí.- Konverze
revertVariantToBasicProduct/createProductVariantvyvolává přesun mezi dvěma spojovými seznamy: vyřazení z původního přesunlinka zařazení do nového přeslink. Obě fáze publikují samostatně. - Ukončení prodeje (
endProductSale) se chová jako trash pro účely seznamu (processDeletingspublishCurrentEntity=true), aby byla publikována i samotná entita.
ParameterItem / ParameterItemInType
- Jedno tlačítko v UI „přiřaď pod syntetickou hodnotu" mění jeden sloupec
idParameterItemSynthna standardním PITu, ale projevuje se ve dvou spojových seznamech (standardní vcolor, syntetický vcolorSynth). Symetrie je zajištěna kaskádou popsanou výše — absorpce odstraňuje synth PIT, uvolnění ho zakládá zpět. - Při běhu testu generačního typu (
ParameterItemInTypePriorityGenerationalTest) je dobré mít na paměti, že voláníparameterService.removeParameterItem(item, …)čte prioritu PITů z in-memory reference. Volající proto musí předat čerstvě načtenou entitu — jinak zastaralá priorita vede k chybnémuunlinkve špatném slotu spojového seznamu.
Přepínače chování
Obě hodnoty jsou na MovePriorityBehaviorSettings a lze je měnit za běhu (užívá toho i admin
nastavení projektu i unit testy přes PriorityTestUtils.executeWithDisabledPriorityManagement /
executeWithPlaceToBeginning).
disableDefaultPriorityManagement
- Default
false. Přitrueinterceptor vintercept(beforeStore i beforeRemove) i vafterStoreokamžitě vrací bez jediné SQL operace navíc — žádnýunlink/link/movePriority/publishChangesse nezavolá. - Důsledek:
previousEntityzůstáváNULLu všech entit, do publikačního journalu pro tuto entitu nepřijde žádnýCHANGE_PRIORITYzáznam, prioritu si musí řídit externí systém (typicky import z ERP). - Vypnout nelze tam, kde je registrované
alwaysEnableDefaultPriorityManagement— to využívá např.ParameterItemaParameterItemInType, kde syntetická kaskáda spolehlivě funguje jen s defaultním řízením. - V UI EdeeShopu jsou pro takto označenou entitu zakázané D&D operace v AR — bez
previousEntityspojového seznamu nelze garantovat výsledek přesunu.
placeNewEntityToBeginning
- Default
false— nová entita dostane prioritulowest − 1a zařadí se na konec seznamu (nejnižší zobrazená priorita). Předchůdcem je dosavadní poslední položka. - Při
truedostane nová entita priorituhighest + 1a zařadí se na začátek (nejvyšší zobrazená priorita). Žádný předchůdce. - Linkování nové entity (nastavení
previousEntityna ní i na následníkovi, publikace záznamů) proběhne až vafterStore— beforeStore vprocessCreatingpouze předvyplní prioritu apreviousEntity = null, protože id nové entity ještě není známé. Při restore přesprocessRestoringForCallbackse naopak link vynucuje inline (callback neproudí přesafterStore). - Praktický dopad: nová entita je nahoře v seznamu a okolní entity se neposouvají (jen se nový head zařadí před ně) — to je důležité třeba pro produktové katalogy, kde má každá nová položka začínat jako nejviditelnější.
Datový tok publikace — shrnutí
┌─────────────────────────────────────────┐ │ aplikační kód mění priority / link list │ └────────────────────┬────────────────────┘ ▼ ┌────────────────────────────────────────────────────────┐ │ MovePriorityPersistenceInterceptor (beforeStore) │ │ + ParameterItemMovePriorityPersistenceInterceptor │ │ (jen pro doménu parametrů) │ └────────────────────┬───────────────────────────────────┘ ▼ ┌────────────────────────────────────────────────────────┐ │ MoveItemJdbcTemplateDao │ │ unlink → movePriority → setActualEntityPriority │ │ → link → publishChanges │ └────────────────────┬───────────────────────────────────┘ ▼ ┌────────────────────────────────────────────────────────┐ │ T_EDEESHOP_ENTITY_PUBLISHING_JOURNAL │ │ type = CHANGE_PRIORITY[*] │ │ data = prev.idCatalogEntity | NULL │ └────────────────────┬───────────────────────────────────┘ ▼ ┌────────────────────────────────────────────────────────┐ │ konzument (Evita publishing service) │ │ aplikuje POSLEDNÍ záznam na (code, publishingType) │ └────────────────────────────────────────────────────────┘Invariant ověřovaný generačními testy
(ProductPriority*GenerationalTest, ParameterItemInTypePriorityGenerationalTest): pro každý
(code, parameterType) musí poslední záznam v journalu odpovídat aktuálnímu stavu DB
(previousEntity → previousCatalogEntity). Pokud ne, někde v kaskádě chybí publikace —
přesně to byl případ uvolnění z absorpce u syntetických hodnot parametru, který opravil
publishNewSyntheticParameterInType v ParameterItemEntityAfterPersistenceWithStorageFlagInterceptor.
