Systémové řazení entit
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.
Zkopírovat odkaz na sekciEntity 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 |
Zkopírovat odkaz na sekciVypnutí 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.
Zkopírovat odkaz na sekciVypnutí systémového řazení pro konkrétní entitu
1 public class ProjectFeature extends AbstractBaseEdeeShopFeature<EdeeShopModuleConfig> {2 @Override3 public void afterSpringInitialization(AbstractRefreshableApplicationContext moduleContext) {4 super.afterSpringInitialization(moduleContext);5 PriorityService priorityService = moduleContext.getBean(SharedBeanNames.PRIORITY_SERVICE, PriorityService.class);6 priorityService.disableDefaultPriorityManagement(Category.ENTITY_NAME);7 }8}
Zkopírovat odkaz na sekciVypnutí systémového řazení pro všechny entity
1 public class ProjectFeature extends AbstractBaseEdeeShopFeature<EdeeShopModuleConfig> {2 @Override3 public void afterSpringInitialization(AbstractRefreshableApplicationContext moduleContext) {4 super.afterSpringInitialization(moduleContext);5 PriorityService priorityService = moduleContext.getBean(SharedBeanNames.PRIORITY_SERVICE, PriorityService.class);6 priorityService.disableDefaultPriorityManagement();7 }8}
Zkopírovat odkaz na sekciImplementační detaily
Zkopírovat odkaz na sekciDatový 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.
Zkopírovat odkaz na sekciKonfigurace
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ý createJournalPublishPreviousCatalogEntityFunction pro 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.
Zkopírovat odkaz na sekciInterceptory
Zkopírovat odkaz na sekciMovePriorityPersistenceInterceptor (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í v beforeStore:
- trash / force-unlink — entita přechází do TRASHED (nebo je jí force unlink vynucen): processDeleting načte původní data z DB (moveItemDao.getOriginalData), zavolá moveAfterDeleting → unlink aktualizuje previousEntity u následníka, movePriority stlačí zbylé priority dolů, setActualEntityPriority(entityId, NO_PRIORITY) ukončí odpojení a publishChanges zapíše záznamy do journalu pro changedIds.
- restore / force-link — entita se vrací z TRASHED: processRestoring přiřadí nejnižší (resp. nejvyšší) prioritu volného slotu, moveAfterRestoring zavolá link, které nastaví previousEntity na nové entitě i na novém následníkovi, a publikuje. Varianta processRestoringForCallback se používá v callback cestě (např. BaseVariantProductRelation při restore variantního produktu) a vynucuje provedení link inline — bez ní by za placeNewEntityToBeginning=true spojový seznam zůstal nezalinkovaný, protože callback restore neproudí přes afterStore.
- nová entita — processCreating přiřadí prioritu (nejnižší−1 / nejvyšší+1) a předvyplní previousEntity přes moveItemDao.getLastEntityId. Skutečný link (vč. publikace) proběhne až v afterStore, kdy je známé id nové entity.
- změna priority existující entity — updatePriority: unlink na původní pozici (publikuje), movePriority posune sousedy mezi starou a novou prioritou, nastaví novou prioritu, link na 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=true interceptor vrátí container okamžitě a žádná z výše uvedených operací neproběhne — priorita zůstane na hodnotě, kterou nastavil volající.
-
intercept(MethodType.RemoveContainers, query) — beforeRemove (skutečné DELETE). Z WHERE klauzule dotazu extrahuje id mazaných entit a pro každé spustí processDeleting. Bez eq/in na id (nebo MovePriorityRemoveHint) vyhodí výjimku — hromadný DELETE bez znalosti konkrétních id by porušil spojový seznam.
-
intercept(MetadataContainerProvider, …) — afterStore. Pouze pro placeNewEntityToBeginning=true: dokončí link nové 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ž v beforeStore přes processCreating a tato cesta neběží.
Zkopírovat odkaz na sekciSpojový 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 na previousEntity během přemisťování.
- link(parentKeys, entityId, priority) — symetrická operace: najde nového předchůdce a následníka, nastaví entity.previousEntity a x3.previousEntity = entityId, vrátí oba změněné id pro publikaci.
- movePriority(parentKeys, newPriority, originalPriority) — hromadně posune priority ostatních entit mezi originalPriority a newPriority o ±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 od previousEntity).
- publishChanges(prioritySettings, changedIds, entityId) — pro každou změněnou entitu vloží do T_EDEESHOP_ENTITY_PUBLISHING_JOURNAL záznam typu prioritySettings.getPublishingType() (default CHANGE_PRIORITY). Sloupec data obsahuje id katalogové entity předchůdce (prev.idCatalogEntity) nebo NULL. 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.
Zkopírovat odkaz na sekciParameterItemMovePriorityPersistenceInterceptor
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šechny parameterInTypes (jak standardní, tak syntetickou stranu) a pro každý vyvolá processRemoved. Pro odpojené (priority == NO_PRIORITY) PITy je processRemoved no-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á processRestoring pro každý PIT, který není absorbovaný (idParameterItemSynth == null).
- změna idParameterItemSynth na PITu (processParameterItemRelationBeforeStore):
- null → non-null = absorpce: processRemoved odpojí PIT ze spojového seznamu jeho parameterType.
- non-null → null = uvolnění z absorpce: processRestoring zalinkuje PIT zpět na konec seznamu (nejnižší−1 priorita).
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í idParameterItemSynth a item má ještě syntetický PIT, odstraní syntetické PITy (removeSyntheticTypeRelations) a uloží znovu — ve druhém cyklu pak processParameterItemRelationBeforeStore zpracuje syntetický PIT v removedItems přes processRemoved a publikuje pro colorSynth view.
- Při uvolnění, kdy item zůstal bez syntetické relace, přidá nový syntetický PIT (addNewParameterInType(synthParameterType)) a uloží znovu. processChangedOrNew v druhém cyklu nastaví prioritu a previousEntity, ale samotný link SQL ani publikaci nezavolá (pro placeNewEntityToBeginning=false). Proto interceptor po druhém ukládání explicitně publikuje přes publishNewSyntheticParameterInType. 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ě.
Zkopírovat odkaz na sekciSpecifika doménových entit
Zkopírovat odkaz na sekciProduct / variant
- Product se řadí v rámci katalogu (parentKeys = idCatalog). Varianty mají na úrovni Produktu priority = NO_PRIORITY (nejsou v katalogovém seznamu) a jsou zařazené ve spojovém seznamu VariantProductRelation v rámci masteru (parentKeys = idMasterProduct). Varianty publikují pod typem CHANGE_PRIORITY_VARIANT — CHANGE_PRIORITY filtr je nevidí.
- Konverze revertVariantToBasicProduct / createProductVariant vyvolává přesun mezi dvěma spojovými seznamy: vyřazení z původního přes unlink a zařazení do nového přes link. Obě fáze publikují samostatně.
- Ukončení prodeje (endProductSale) se chová jako trash pro účely seznamu (processDeleting s publishCurrentEntity=true), aby byla publikována i samotná entita.
Zkopírovat odkaz na sekciParameterItem / ParameterItemInType
- Jedno tlačítko v UI „přiřaď pod syntetickou hodnotu" mění jeden sloupec idParameterItemSynth na standardním PITu, ale projevuje se ve dvou spojových seznamech (standardní v color, syntetický v colorSynth). 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ému unlink ve špatném slotu spojového seznamu.
Zkopírovat odkaz na sekciPř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).
Zkopírovat odkaz na sekcidisableDefaultPriorityManagement
- Default false. Při true interceptor v intercept (beforeStore i beforeRemove) i v afterStore okamžitě vrací bez jediné SQL operace navíc — žádný unlink/link/ movePriority/publishChanges se nezavolá.
- Důsledek: previousEntity zůstává NULL u všech entit, do publikačního journalu pro tuto entitu nepřijde žádný CHANGE_PRIORITY záznam, prioritu si musí řídit externí systém (typicky import z ERP).
- Vypnout nelze tam, kde je registrované alwaysEnableDefaultPriorityManagement — to využívá např. ParameterItem a ParameterItemInType, 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 previousEntity spojového seznamu nelze garantovat výsledek přesunu.
Zkopírovat odkaz na sekciplaceNewEntityToBeginning
- Default false — nová entita dostane prioritu lowest − 1 a zařadí se na konec seznamu (nejnižší zobrazená priorita). Předchůdcem je dosavadní poslední položka.
- Při true dostane nová entita prioritu highest + 1 a zařadí se na začátek (nejvyšší zobrazená priorita). Žádný předchůdce.
- Linkování nové entity (nastavení previousEntity na ní i na následníkovi, publikace záznamů) proběhne až v afterStore — beforeStore v processCreating pouze předvyplní prioritu a previousEntity = null, protože id nové entity ještě není známé. Při restore přes processRestoringForCallback se naopak link vynucuje inline (callback neproudí přes afterStore).
- 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ší.
Zkopírovat odkaz na sekciDatový tok publikace — shrnutí
1 ┌─────────────────────────────────────────┐2 │ aplikační kód mění priority / link list │3 └────────────────────┬────────────────────┘4 ▼5 ┌────────────────────────────────────────────────────────┐6 │ MovePriorityPersistenceInterceptor (beforeStore) │7 │ + ParameterItemMovePriorityPersistenceInterceptor │8 │ (jen pro doménu parametrů) │9 └────────────────────┬───────────────────────────────────┘10 ▼11 ┌────────────────────────────────────────────────────────┐12 │ MoveItemJdbcTemplateDao │13 │ unlink → movePriority → setActualEntityPriority │14 │ → link → publishChanges │15 └────────────────────┬───────────────────────────────────┘16 ▼17 ┌────────────────────────────────────────────────────────┐18 │ T_EDEESHOP_ENTITY_PUBLISHING_JOURNAL │19 │ type = CHANGE_PRIORITY[*] │20 │ data = prev.idCatalogEntity | NULL │21 └────────────────────┬───────────────────────────────────┘22 ▼23 ┌────────────────────────────────────────────────────────┐24 │ konzument (Evita publishing service) │25 │ aplikuje POSLEDNÍ záznam na (code, publishingType) │26 └────────────────────────────────────────────────────────┘
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.