Publikace dat do evitaDB
Tento dokument popisuje kompletní systém publikace dat z EdeeShop do evitaDB, včetně definice schématu pomocí anotací, vytváření nových publikovaných entit a projektového rozšiřování.
1. Úvod
Publikace dat do evitaDB je mechanismus, který transformuje data z relační databáze EdeeShop do dokumentově orientované evitaDB databáze. Tato transformace umožňuje:
- Rychlé fulltextové vyhledávání - optimalizované pro e-commerce použití
- GraphQL API - automaticky generované z definovaného schématu
- Facetové filtrování - efektivní práce s parametry, kategoriemi a dalšími filtry
- Vícejazyčná podpora - lokalizované atributy a data
2. Základní koncepty
Před tím, než začnete pracovat s publikovanými daty, je důležité pochopit základní koncepty a architekturu systému.
2.1 Architektura publikovaných entit
Každá publikovaná entita v EdeeShopu se skládá z několika komponent, které následují READ_MODEL / WRITE_MODEL pattern:
Pattern READ_MODEL / WRITE_MODEL
Tento pattern odděluje rozhraní pro čtení (READ_MODEL) od rozhraní pro zápis (WRITE_MODEL):
- READ_MODEL - immutable rozhraní s getter metodami, slouží pro čtení dat z evitaDB
- WRITE_MODEL (Editor) - mutable rozhraní se setter metodami, slouží pro zápis dat při publikaci
// READ_MODEL - pro čtenípublic interface PublishedProduct extends PublishedProductContract<PublishedProduct, PublishedProductEditor> {}
// WRITE_MODEL - pro zápis při publikacipublic interface PublishedProductEditor extends PublishedProductEditorContract<PublishedProduct, PublishedProductEditor> {}Contract rozhraní
Contract rozhraní obsahují definici schématu pomocí anotací evitaDB:
@Entity(name = "Product", description = "...")public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends PublishedEntityBase, SealedInstance<READ_MODEL, WRITE_MODEL> {
String ATTRIBUTE_CODE_SHORT = "codeShort";
@Attribute(name = ATTRIBUTE_CODE_SHORT, filterable = true) String getCodeShort() throws ContextMissingException;}Contract rozhraní jsou generická a umožňují projektové rozšíření.
Editor Contract
Editor Contract rozhraní obsahují setter metody pro zápis dat:
public interface PublishedProductEditorContract<READ_MODEL, WRITE_MODEL> extends PublishedProductContract<READ_MODEL, WRITE_MODEL>, PublishedEntityBaseEditor, InstanceEditor<READ_MODEL> {
@AttributeRef(ATTRIBUTE_CODE_SHORT) void setCodeShort(String codeShort) throws ContextMissingException;}2.2 Traity (WithPublished*)
Traity jsou mixin rozhraní, která přidávají konkrétní funkcionalitu do publikovaných entit pomocí vícenásobné dědičnosti. Všechny traity začínají prefixem WithPublished*.
Kompoziční přístup
Díky traitům lze snadno sdílet společnou funkcionalitu napříč různými entitami:
public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends PublishedEntityBase, // Základní funkcionalita WithPublishedLocalization, // název, popis WithPublishedPrice, // ceny WithPublishedURL, // URL WithPublishedMedia, // obrázky WithPublishedCategories, // kategorie WithPublishedValidity, // časová platnost SealedInstance<READ_MODEL, WRITE_MODEL> { // ... specifické atributy produktu}Běžně používané traity
Lokalizační:
WithPublishedLocalization- název, popis
Časové:
WithPublishedValidity- časová platnost (validFrom/validTo)
Viditelnost:
WithPublishedVisibility- viditelnost entity
URL a obsah:
WithPublishedURL- URL funkcionalita (slugy, aktivní URL)WithPublishedTags- tagyWithPublishedMedia- obrázky a multimédiaWithPublishedRelatedFiles- soubory (PDF, dokumenty)
Produktové:
WithPublishedPrice- cenová funkcionalitaWithPublishedCategories- kategorie produktůWithPublishedGroups- skupiny produktůWithPublishedParameters- parametry produktůWithPublishedBrand- značka produktuWithPublishedStock- skladové zásobyWithPublishedMasterVariantProperties- master/variant produkty
Detailní popis všech traitů najdete v kapitole 7. Reference - přehled traitů.
2.3 Základní komponenty
PublishedEntityBase
Základní rozhraní pro všechny publikované entity:
public interface PublishedEntityBase extends WithEntityContract, TraitSupport, WithPublishedCatalog, WithPublishedCode, WithPublishedDatetime, WithPublishedChanged, WithLocales, WithEntitySchema, WithVersion, WithTraits {
@PrimaryKey(autoGenerate = false) int getId();
@Attribute(name = "status", global = true, representative = true) CatalogEntityStatus getStatus() throws ContextMissingException;}EntityToEvitaConversionSnippet
Snippet definuje, jak se mají data z relační databáze (PublishedCatalogEntity) mapovat do evitaDB entity:
public interface EntityToEvitaConversionSnippet<T> { Class<T> getRequiredType();
void convert( PublishedCatalogEntity catalogEntity, T entity, AbstractEvitaJobContext context );}EvitaPublishingService
Služba pro správu publikačních deskriptorů a registraci snippetů:
evitaPublishingService.registerConversionSnippet(new MyConversionSnippet());evitaPublishingService.getPublishingDescriptorFor(PRODUCT) .registerPublishedServiceClass(MyPublishedService.class);3. Anotace evitaDB
evitaDB používá anotace pro deklarativní definici schématu entit. Tyto anotace jsou umístěny na rozhraních publikovaných entit a definují, jak budou data uložena, indexována a dotazována.
3.1 Entitové anotace
@Entity
Označuje rozhraní jako publikovanou entitu v evitaDB. Tato anotace je povinná pro každé rozhraní, které reprezentuje entitu.
Parametry:
name(povinný) - název entity v evitaDBdescription- textový popis entity (použit v GraphQL schema)allowedEvolution- režimy automatické evoluce schématu
Příklad:
@Entity( name = "Product", description = """ Product represents an article that can be displayed and sold on e-shop.""", allowedEvolution = { EvolutionMode.ADDING_ATTRIBUTES, EvolutionMode.ADDING_ASSOCIATED_DATA })public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends ... { String ENTITY_NAME = "Product";}@PrimaryKey
Označuje primární klíč entity. V EdeeShopu je primární klíč vždy int a odpovídá ID z tabulky T_EDEESHOP_CATALOG_ENTITY.
Příklad:
@PrimaryKey(autoGenerate = false)int getId();3.2 Datové anotace
@Attribute
Označuje atribut entity. Atributy jsou základní jednotky dat, které mohou být filtrovány, řazeny a lokalizovány.
Parametry:
name(povinný) - název atributudescription- popis atributu (použit v GraphQL schema)filterable- zda lze podle atributu filtrovat (výchozí: false)sortable- zda lze podle atributu řadit (výchozí: false)unique- typ unikátnostilocalized- zda je atribut lokalizovaný (výchozí: false)nullable- zda může být atribut null (výchozí: false)representative- zda je atribut reprezentativní (zobrazuje se v přehledech), u duplicitních referencí na stejné entity (např. vazba na média) identifikuje tzv. diskriminátory (klíčové atributy odlišující jednotlivé instance reference)global- zda je atribut globální pro všechny entity daného typuscope- nastavení atributu podle scope (Live, Archived)
Příklady:
// Jednoduchý filtrovatelný atribut@Attribute( name = "codeShort", description = "Product shortcode for feeds and GTM", unique = AttributeUniquenessType.UNIQUE_WITHIN_COLLECTION)String getCodeShort() throws ContextMissingException;
// Atribut s různým nastavením podle scope@Attribute( name = "productType", description = "Defines product types (BASIC, MASTER, VARIANT, SET)", scope = { @ScopeAttributeSettings(scope = Scope.LIVE, filterable = true), @ScopeAttributeSettings(scope = Scope.ARCHIVED, filterable = true) })ProductType getProductType() throws ContextMissingException;
// Lokalizovaný atribut@Attribute( name = "name", description = "Localized name of the entity", localized = true, filterable = true, sortable = true)String getName() throws ContextMissingException;@AttributeRef
Používá se v editorech pro označení setter metod:
@AttributeRef(ATTRIBUTE_CODE_SHORT)void setCodeShort(String codeShort) throws ContextMissingException;@AssociatedData
Označuje asociovaná data entity - komplexnější datové struktury (objekty, kolekce), které nejsou indexovány:
@AssociatedData( name = "hreflang", description = "List of hreflang URLs for SEO", nullable = true)List<ActiveUrl> getHreflang() throws ContextMissingException;@SortableAttributeCompound
Definuje složený atribut pro řazení - umožňuje vytvořit složený index z více atributů:
@SortableAttributeCompound( name = "productPriorityAndName", description = "Compound index for sorting by priority and name", attributeElements = { @AttributeElement(attributeName = "priority", direction = OrderDirection.DESC), @AttributeElement(attributeName = "name", direction = OrderDirection.ASC) })3.3 Hierarchické anotace
@ParentEntity
Označuje vztah k rodičovské entitě v hierarchické struktuře:
@ParentEntityInteger getParentId() throws ContextMissingException;
@ParentEntityPublishedCategoryContract<READ_MODEL, WRITE_MODEL> getParent() throws ContextMissingException;
// Optional přístup@ParentEntityOptional<PublishedCategoryContract<READ_MODEL, WRITE_MODEL>> getParentIfAvailable();3.4 Referenční anotace
@Reference
Označuje referenci na jinou entitu:
@Reference( name = "representedCategory", description = "Reference to standard category for shortcut categories")PublishedCategoryContract<READ_MODEL, WRITE_MODEL> getRepresentedCategory() throws ContextMissingException;@ReferenceRef
Používá se pro metody s Optional přístupem:
@ReferenceRef(REFERENCE_REPRESENTED_CATEGORY)Optional<PublishedCategoryContract<READ_MODEL, WRITE_MODEL>> getRepresentedCategoryIfAvailable();@ReflectedReference
Označuje zpětnou referenci - automaticky vypočítanou referenci z opačného směru vztahu:
@ReflectedReference( name = "products", ofEntity = "Product", ofName = "categories", description = "Products assigned to this category", attributeInheritanceFilter = { "assignmentValidity" })List<PublishedCategoryProduct> getProducts() throws ContextMissingException;Parametry:
name- název zpětné referenceofEntity- název entity, ze které reference pocházíofName- název původní referenceattributeInheritanceFilter- atributy pro dědění z původní reference
@ReferencedEntity, @ReferencedEntityGroup
Označují přímý odkaz na entitu nebo skupinu entit v rámci reference:
@ReferencedEntityPublishedCategory getCategory();
@ReferencedEntityGroupPublishedGroup getGroup();3.5 Cenové anotace
@Price
Označuje kolekci cen entity:
@PriceList<PriceContract> getPrices();@PriceForSale
Označuje aktuální prodejní cenu entity:
@PriceForSalePriceContract getPriceForSale() throws ContextMissingException;4. Praktické návody
Tato kapitola obsahuje konkrétní postupy pro práci s publikovanými daty v EdeeShopu.
4.1 Projektové rozšíření existující entity
Kdy použít: Když potřebujete přidat nové atributy, reference nebo asociovaná data do existující entity (Product, Category, Brand, atd.) na konkrétním projektu.
Níže je popsán postup na příkladu rozšíření produktu o nový atribut demoNumber.
Krok 1: Projektové Contract rozhraní
Vytvořte projektové rozhraní s dodatečnými atributy. Použijte anotace z kapitoly 3. Anotace evitaDB:
public interface DemoPublishedProductContract {
String ATTRIBUTE_DEMO_NUMBER = "demoNumber";
/** * Ukázkový atribut rozšiřující PublishedProduct. * * @throws ContextMissingException if the attribute was not fetched */ @Attribute( name = ATTRIBUTE_DEMO_NUMBER, description = "Custom demo attribute. It doesn't have to be unique.", filterable = true, sortable = true, nullable = true, representative = true ) @Nullable String getDemoNumber() throws ContextMissingException;}Krok 2: Hlavní rozhraní entity
Vytvořte hlavní rozhraní produktu, které dědí jak základní contract, tak projektový contract:
public interface DemoPublishedProduct extends PublishedProductContract<DemoPublishedProduct, DemoPublishedProductEditor>, DemoPublishedProductContract {}Vysvětlení generických typů:
- První typ (
DemoPublishedProduct) - projektové rozhraní entity (READ_MODEL) - Druhý typ (
DemoPublishedProductEditor) - rozhraní editoru (WRITE_MODEL)
Krok 3: Projektové Editor Contract
Vytvořte projektové editor rozhraní se setter metodami:
public interface DemoPublishedProductEditorContract extends DemoPublishedProductContract {
/** * @param demoNumber demo-number of entity * @throws ContextMissingException if the attribute was not fetched */ @AttributeRef(ATTRIBUTE_DEMO_NUMBER) void setDemoNumber(@Nullable String demoNumber) throws ContextMissingException;}Krok 4: Hlavní rozhraní editoru
public interface DemoPublishedProductEditor extends PublishedProductEditorContract<DemoPublishedProduct, DemoPublishedProductEditor>, DemoPublishedProductEditorContract {}Krok 5: Servisa
Vytvořte projektovou servisu pro přístup k datům:
public class DemoPublishedProductService extends AbstractPublishedProductService<DemoPublishedProduct, DemoPublishedProductEditor> {
public DemoPublishedProductService( @NonNull EvitaContract evita, @NonNull ReflectionLookup reflectionLookup, @NonNull WrappingService wrappingService ) { super(evita, reflectionLookup, wrappingService); }
@Override public @NonNull Class<DemoPublishedProduct> getEntityClass() { return DemoPublishedProduct.class; }
@Override public @NonNull Class<DemoPublishedProductEditor> getEntityEditorClass() { return DemoPublishedProductEditor.class; }}Krok 6: Conversion Snippet
Vytvořte snippet pro mapování dat z relační databáze:
public class DemoPublishedProductConversionSnippet implements EntityToEvitaConversionSnippet<DemoPublishedProductEditor> {
@Override public @NonNull Class<DemoPublishedProductEditor> getRequiredType() { return DemoPublishedProductEditor.class; }
@Override public void convert( @NonNull PublishedCatalogEntity catalogEntity, @NonNull DemoPublishedProductEditor entity, @NonNull AbstractEvitaJobContext context ) { entity.setDemoNumber(catalogEntity.getStringData(ATTRIBUTE_DEMO_NUMBER)); }}Krok 7: Registrace ve feature
Zaregistrujte servisu a snippet v projektovém feature modulu:
@Overridepublic void beforeSpringInitialization() { super.beforeSpringInitialization();
if (isFeatureEnabled()) { final EntityModelClassConfiguration entityModel = getFeature(CatalogFeature.class).getEntityModelClassConfiguration();
// registrace rozšíření pro zdrojová data (pokud potřebujete) entityModel.getModelDescriptor(PRODUCT).addTrait(WithDemoNumber.class);
// registrace conversion snippetu a servisní třídy pro publikovaná data final EvitaPublishingService evitaPublishingService = getFeature(EvitaFeature.class).getEvitaPublishingService();
evitaPublishingService.registerConversionSnippet(new DemoPublishedProductConversionSnippet()); evitaPublishingService.getPublishingDescriptorFor(PRODUCT) .registerPublishedServiceClass(DemoPublishedProductService.class); }}
// Projektové rozšíření se musí vykonat až po rozšíření dané entity@Overridepublic FeatureDependency[] getRequiredFeatures() { return new FeatureDependency[]{ new FeatureDependency(ProductFeature.class, true) };}Použití
Následně všechny načtené produkty budou obsahovat i atribut demoNumber, který bude možné filtrovat a řadit:
DemoPublishedProduct product = publishedProductService.getProductByQuery( new ProductQuery(catalogCode) .withCode("p1") .withAttributes(ATTRIBUTE_DEMO_NUMBER)).orElseThrow();
String demoNumber = product.getDemoNumber();Traity na přání
Pokud potřebujete na pouze na konkrétním místě (nikoliv v rámci celé aplikace, ale například jen pouze v rámci jedné
služby nebo administrační stránky atp.) dynamicky doplnit další trait, použijte wrapInto():
@Traitpublic interface CustomTrait extends LocalDataStore { default void setData(Integer data) { setLocalData("data", data); }
default Integer getData() { return getLocalData("data"); }}
// PoužitíDemoPublishedProduct product = publishedProductService.getProductByQuery(...);DemoPublishedProduct extendedProduct = publishedProductService.wrapInto(product, CustomTrait.class);
assertTrue(extendedProduct instanceof CustomTrait);((CustomTrait)extendedProduct).setData(42);Tato funkcionalita dává smysl, když nechcete změnit rozhraní produktu v celém systému, ale jen v nějaké konkrétní situaci.
4.2 Vytvoření nové entity od základu
Kdy použít: Když potřebujete vytvořit zcela novou entitu (ne pouze rozšířit existující), např. nový typ katalogové entity.
⚠️ Upozornění: Většina kroků je shodná s kapitolou 4.1 Projektové rozšíření. Níže jsou popsána pouze specifika pro vytvoření nové entity.
Specifika pro novou entitu
1. Základní struktura rozhraní
Každá nová publikovaná entita se skládá ze čtyř hlavních rozhraní:
PublishedMyEntity- hlavní rozhraní entity (pro nahrazení na projektu)PublishedMyEntityContract<READ_MODEL, WRITE_MODEL>- contract s anotacemiPublishedMyEntityEditor- rozhraní editoru (pro nahrazení na projektu)PublishedMyEntityEditorContract<READ_MODEL, WRITE_MODEL>- contract editoru
2. Contract musí dědit z PublishedEntityBase:
@Entity( name = "MyEntity", description = "MyEntity represents...", allowedEvolution = { EvolutionMode.ADDING_ATTRIBUTES, EvolutionMode.ADDING_ASSOCIATED_DATA })public interface PublishedMyEntityContract< READ_MODEL extends PublishedMyEntityContract<READ_MODEL, WRITE_MODEL>, WRITE_MODEL extends PublishedMyEntityEditorContract<READ_MODEL, WRITE_MODEL>>extends PublishedEntityBase, // ← povinné! SealedInstance<READ_MODEL, WRITE_MODEL>, WithPublishedVisibility, WithPublishedValidity {
String ENTITY_NAME = "MyEntity";
// Název deskriptoru pro publikaci String PUBLISHED_DESCRIPTOR_NAME = ClassSchemaAnalyzer.extractEntityTypeFromClass( PublishedMyEntityContract.class, ReflectionLookup.NO_CACHE_INSTANCE ).orElseThrow();
@Override default String getPublishingDescriptorName() { return PUBLISHED_DESCRIPTOR_NAME; }
@Override default CatalogEntityType getEntityType() { return CatalogEntityType.MY_ENTITY; // ← musí existovat v enum! }
// ... atributy, reference, asociovaná data}3. Editor Contract musí dědit z PublishedEntityBaseEditor:
public interface PublishedMyEntityEditorContract< READ_MODEL extends PublishedMyEntityContract<READ_MODEL, WRITE_MODEL>, WRITE_MODEL extends PublishedMyEntityEditorContract<READ_MODEL, WRITE_MODEL>>extends PublishedMyEntityContract<READ_MODEL, WRITE_MODEL>, PublishedEntityBaseEditor, // ← povinné! WithPublishedValidityEditor, WithPublishedVisibilityEditor, InstanceEditor<READ_MODEL> {
// setter metody}4. Model Class Descriptor:
Pro novou entitu musíte vytvořit descriptor:
public class PublishedMyEntityModelClassDescriptor extends ModelClassDescriptor implements PublishedModelClassDescriptor<PublishedMyEntity, PublishedMyEntityEditor> {
public PublishedMyEntityModelClassDescriptor() { super(PublishedMyEntityContract.ENTITY_NAME, PublishedMyEntity.class); }
@Override public Class<PublishedMyEntity> getReadModelClass() { return PublishedMyEntity.class; }
@Override public Class<PublishedMyEntityEditor> getWriteModelClass() { return PublishedMyEntityEditor.class; }}5. Servisa a Snippet:
Vytváří se stejným způsobem jako v kapitole 4.1, jen místo dědění z AbstractPublishedProductService budete dědit z příslušné abstraktní třídy pro vaši entitu.
6. Registrace:
evitaPublishingService.registerConversionSnippet(new MyEntityConversionSnippet());evitaPublishingService.getPublishingDescriptorFor(MY_ENTITY) .registerPublishedServiceClass(PublishedMyEntityService.class);7. CatalogEntityType:
Nezapomeňte přidat nový typ do enum CatalogEntityType:
public enum CatalogEntityType { PRODUCT, CATEGORY, BRAND, MY_ENTITY // ← nový typ}Kompletní příklad struktury nového rozhraní najdete v kapitole 4.1 - kroky jsou identické, jen se vytváří od základu místo rozšiřování existujícího.
4.2.1 Entity bez vazby na CatalogEntityType
Kdy použít: Když potřebujete publikovat data do evitaDB, která nemají oporu v CatalogEntityType ani v tabulce T_EDEESHOP_CATALOG_ENTITY. Typické případy:
- entity z Edee CMS
- entity z externích systémů
Příklad: feature lib_eshop_tag publikuje entity Tag a TagCategory do evitaDB.
Klíčové rozdíly oproti standardnímu přístupu
| Aspekt | Standardní entita | Entity bez CatalogEntityType |
|---|---|---|
| CatalogEntityType | Vyžaduje přidání do enum | Nepotřebuje |
| Dědičnost Contract | PublishedEntityBase | WithEntitySchema, TraitSupport, Serializable |
| Registrace schématu | Automatická | Manuální (defineEntitySchemaFromModelClass) |
| Publikace dat | Přes PublishingJournalService | Manuální přes evitaDB session |
| Aktualizace | Automatické | Vlastní change listener |
Krok 1: Contract rozhraní
Contract definuje schéma entity pomocí anotací. Nedědí z PublishedEntityBase:
@Entity( name = PublishedTagContract.ENTITY_NAME, description = "Tag is equivalent to EdeeTag that is published to evitaDB.")public interface PublishedTagContract< READ_MODEL extends PublishedTagContract<READ_MODEL, WRITE_MODEL>, WRITE_MODEL extends PublishedTagEditorContract<READ_MODEL, WRITE_MODEL>>extends WithEntitySchema, // přístup ke schématu WithPublishedCode, // atribut code WithPublishedName, // lokalizovaný název WithPublishedDatetime, // timestamp publikace TraitSupport, Serializable, SealedInstance<READ_MODEL, WRITE_MODEL> {
String ENTITY_NAME = "Tag"; String ATTRIBUTE_IS_VISIBLE_IN_FILTER = "isVisibleInFilter"; String REFERENCE_CATEGORY = "categories";
@Attribute( name = ATTRIBUTE_IS_VISIBLE_IN_FILTER, description = "Contains true if tag should be displayed as a part of the product filter.", filterable = true ) boolean isVisibleInFilter() throws ContextMissingException;
@Reference( name = REFERENCE_CATEGORY, description = "Tag categories aggregating similar tags together.", entity = "TagCategory", indexed = ReferenceIndexType.FOR_FILTERING ) @Nonnull List<PublishedTagCategory> getCategories() throws ContextMissingException;}Krok 2: Editor Contract
public interface PublishedTagEditorContract< READ_MODEL extends PublishedTagContract<READ_MODEL, WRITE_MODEL>, WRITE_MODEL extends PublishedTagEditorContract<READ_MODEL, WRITE_MODEL>>extends InstanceEditor<READ_MODEL>, WithPublishedCodeEditor, WithPublishedNameEditor, PublishedTagContract<READ_MODEL, WRITE_MODEL> {
@AttributeRef(ATTRIBUTE_IS_VISIBLE_IN_FILTER) void setVisibleInFilter(boolean visibleInFilter);
@ReferenceRef(REFERENCE_CATEGORY) @CreateWhenMissing void addOrUpdateCategory(int categoryId) throws ContextMissingException;
@ReferenceRef(REFERENCE_CATEGORY) @RemoveWhenExists void removeCategories() throws ContextMissingException;}Krok 3: Hlavní rozhraní
public interface PublishedTag extends PublishedTagContract<PublishedTag, PublishedTagEditor> {}
public interface PublishedTagEditor extends PublishedTagEditorContract<PublishedTag, PublishedTagEditor> {}Krok 4: Publisher callback (registrace schématu a počáteční publikace)
Publisher dědí z AbstractEvitaIndexingCallback a přepisuje metodu catalogDefined():
@RequiredArgsConstructorpublic class EdeeTagPublisher extends AbstractEvitaIndexingCallback { private final EntityTagService tagService; private final TagToEvitaConverter tagToEvitaConverter;
@Override public int getCallbackOrder() { return -1000; // záporná hodnota = provede se před ostatními }
@Override public void catalogDefined( @NonNull String catalog, @NonNull String evitaCatalog, @NonNull EvitaContract evita, @NonNull EvitaPublishingService evitaPublishingService, @NonNull SchemaPostProcessor postConfigurator ) { evita.updateCatalog(evitaCatalog, session -> { // 1. Registrace schématu z model class SealedEntitySchema tagSchema = session.defineEntitySchemaFromModelClass( PublishedTag.class, postConfigurator );
// 2. Publikace všech entit for (EdeeTag tag : tagService.getAllTags(catalog)) { tagToEvitaConverter.convertToNewTag( catalog, tag, tagSchema.getLocales(), session ); } }); }}Krok 5: Change listener (pro real-time aktualizace)
Listener reaguje na události a aktualizuje data v evitaDB:
@RequiredArgsConstructorpublic class EdeeTagChangeListener implements ApplicationListener<TagEvent> { private final EntityTagService tagService; private final TagToEvitaConverter tagToEvitaConverter; private final EvitaClientContract evita;
@Override public void onApplicationEvent(TagEvent event) { final Tag tag = event.getSubject();
for (String catalog : evita.getCatalogNames()) { evita.updateCatalog(catalog, session -> { session.getEntitySchema(PublishedTag.ENTITY_NAME).ifPresent(schema -> { if (event instanceof TagRemovedEvent) { // Smazání entity session.deleteEntity(PublishedTag.ENTITY_NAME, Math.toIntExact(tag.getId())); } else { // Aktualizace nebo vytvoření entity final Optional<? extends PublishedTagContract<?,?>> existing = session.getEntity(PublishedTag.class, Math.toIntExact(tag.getId()), QueryConstraints.entityFetchAllContent());
if (existing.isPresent()) { // Aktualizace existující PublishedTagEditorContract<?,?> editor = existing.get().openForWrite(); tagToEvitaConverter.convert(catalog, tagService.getTagById(tag.getId()), schema.getLocales(), editor, session); } else { // Vytvoření nové tagToEvitaConverter.convertToNewTag(catalog, tagService.getTagById(tag.getId()), schema.getLocales(), session); } } }); }); } }}Krok 6: Konvertor
Konvertor transformuje zdrojová data do evitaDB entity:
@RequiredArgsConstructorpublic class TagToEvitaConverter {
public void convertToNewTag( @NonNull String catalogCode, @Nonnull EdeeTag tag, @Nonnull Set<Locale> allowedLocales, @Nonnull EvitaSessionContract session ) { // Vytvoření nové entity s daným ID PublishedTagEditorContract<?, ?> publishedTag = session.createNewEntity(PublishedTagEditorContract.class, Math.toIntExact(tag.getId()));
convert(catalogCode, tag, allowedLocales, publishedTag, session); }
public void convert( @NonNull String catalogCode, @Nonnull EdeeTag tag, @Nonnull Set<Locale> allowedLocales, @Nonnull PublishedTagEditorContract<?, ?> publishedTag, @NonNull EvitaSessionContract session ) { publishedTag.setCode(tag.getSystemId()); publishedTag.setVisibleInFilter(tag.isFilterable());
for (Locale locale : allowedLocales) { final String tagName = tag.getName(locale.getLanguage()); if (tagName != null) { publishedTag.setName(locale, tagName); } }
// Uložení entity session.upsertEntity(publishedTag); }}Krok 7: Registrace ve feature
V metodě afterSpringInitialization() zaregistrujte callback:
@Overridepublic void afterSpringInitialization(AbstractRefreshableApplicationContext moduleContext) { super.afterSpringInitialization(moduleContext);
if (isFeaturesEnabled(EvitaFeature.class)) { final EvitaPublishingService evitaPublishingService = getFeature(EvitaFeature.class).getEvitaPublishingService();
// Registrace callbacku pro publikaci final EdeeTagPublisher edeeTagPublisher = moduleContext.getBean(EdeeTagPublisher.class); evitaPublishingService.addEvitaIndexingCallback(edeeTagPublisher); }}Reference na kód
Tip: Tímto způsobem lze přidávat libovolné množství entit do evitaDB bez nutnosti rozšiřovat
CatalogEntityTypeenum a bez závislosti na standardním publikačním mechanismu EdeeShop.
4.3 Automatická aktualizace schémat
Kdy použít: Při nasazení nové verze aplikace, která obsahuje změny v definici schémat publikovaných entit.
Jak to funguje
Při startu EdeeShop modulu se automaticky aktualizují schémata existujících katalogů v evitaDB. Aktualizace probíhá formou "upsert" operace:
- ✅ Přidání nových atributů, referencí, asociovaných dat
- ✅ Aktualizace stávajících definic (kromě přejmenování)
- ❌ Nikdy nedochází k automatickému odstraňování položek
Přejmenované prvky se chovají jako nové položky - původní zůstávají v databázi.
Co dělat po aktualizaci schématu
-
Inkrementální indexace - nově přidané prvky jsou prázdné, je nutné je naplnit daty:
java// Vynutit reindex entity v evitaDB na základě stavu ve STRUCTURE tabulkáchpublishingJournalService.triggerEntityChangeForPublishedEntities( catalog, entityType, new ListingQueryImpl( in(AdamPublishedCatalogEntity.PROPERTY_ID_CATALOG_ENTITY, entityIds) )); // Nebo kompletní přepublikace z primárních dat (STRUCTURE tabulky + evitaDB)publishingJournalService.republishAllPublishedEntities( catalog, entityType, PublishingRequestType.ENTITY_UPDATE, new ListingQueryImpl( in(AdamPublishedCatalogEntity.PROPERTY_ID_CATALOG_ENTITY, entityIds) )); -
Úklid nepoužívaných prvků - po delší době (až klienti jako Juan přestanou staré prvky využívat) je vhodné:
- Provést plnou reindexaci dat, nebo
- Vytvořit job pro manuální odstranění nepoužívaných prvků ze schématu
Omezení a známé problémy
| Problém | Popis | Řešení |
|---|---|---|
| Indexy na starých datech | Změny v indexech se nepromítají na již zaindexovaná data (issue #409) | Provést plnou reindexaci |
| Staré indexy v paměti | Původní indexy zůstávají v DB a paměti i po změně schématu | Restart + reindexace |
| Prázdné nové prvky | Nově založené atributy/reference nemají data | Inkrementální indexace |
Reference na kód
- PublishingJournalService#triggerEntityChangeForPublishedEntities - reindex entity v evitaDB
- PublishingJournalService#republishAllPublishedEntities - kompletní přepublikace
⚠️ Doporučení: Tento mechanismus je relativně nový. Důkladně otestujte na testovacím prostředí před nasazením do produkce.
5. Pokročilé koncepty
5.1 Scope (Live vs Archived)
evitaDB podporuje koncept scopes (dosahů), které umožňují mít různé nastavení atributů v různých kontextech:
- Scope.LIVE - aktivní data - plně dostupná na frontendu
- Scope.ARCHIVED - archivovaná data (např. ukončené produkty) - s omezenou dostupností a možnostmi na frontendu
Použití:
@Attribute( name = "productType", scope = { @ScopeAttributeSettings(scope = Scope.LIVE, filterable = true), @ScopeAttributeSettings(scope = Scope.ARCHIVED, filterable = true) })ProductType getProductType() throws ContextMissingException;Globální atributy jsou sdílené napříč všemi scope:
@Attribute( name = "status", global = true, // ← sdílený napříč scope representative = true, scope = { @ScopeAttributeSettings(scope = Scope.LIVE, filterable = true), @ScopeAttributeSettings(scope = Scope.ARCHIVED, filterable = true) })CatalogEntityStatus getStatus() throws ContextMissingException;5.2 EvolutionMode
EvolutionMode definuje, jak může se schéma entity automaticky rozšiřovat při publikaci nových dat, aniž by bylo
nutné manuálně měnit schéma v evitaDB:
@Entity( name = "Product", allowedEvolution = { EvolutionMode.ADDING_ATTRIBUTES, // přidávání atributů EvolutionMode.ADDING_ASSOCIATED_DATA, // přidávání asociovaných dat EvolutionMode.ADDING_REFERENCES, // přidávání referencí EvolutionMode.ADDING_LOCALES, // přidávání lokalizací EvolutionMode.ADDING_CURRENCIES, // přidávání měn EvolutionMode.ADDING_HIERARCHY // přidávání hierarchie })⚠️ Upozornění: Po automatické evoluci je nutné provést inkrementální indexaci (viz kapitola 4.3).
5.3 Representative attributes
Atributy s representative = true nemají jiný význam, než ten, že tyto atributy jsou zobrazené v evitaLab při výpisu
entit v gridu, aniž by bylo nutné je explicitně do pohledu přidávat:
@Attribute( name = "catalogNumber", representative = true, // ← automaticky načten nullable = true)String getCatalogNumber() throws ContextMissingException;Použití u referencí: Representative označuje také klíčové atributy u referencí umožňujících duplikované vazby na stejné cílové entity (např. vazby na média).
5.4 Compound attributes
Složené atributy umožňují řazení podle více kritérií najednou (evitaDB má odlišný přístup k sekundárnímu řazení oproti tomu, na co jste zvyklí z relačních databází):
@SortableAttributeCompound( name = "productPriorityAndName", description = "Compound index for sorting", attributeElements = { @AttributeElement(attributeName = "priority", direction = OrderDirection.DESC), @AttributeElement(attributeName = "name", direction = OrderDirection.ASC) })5.5 Lokalizované atributy
Atributy s localized = true mají různé hodnoty pro různé jazyky:
@Attribute( name = "name", localized = true, // ← hodnoty podle jazyka filterable = true, sortable = true)String getName() throws ContextMissingException;Typicky se kombinuje s @LocalizedLabel na primárních datech:
@Attribute(name = "unit", localized = true)// odpovídá @LocalizedLabel(code = Product.UNIT_CODE) na primárních datechString getUnit() throws ContextMissingException;5.6 Unikátnost atributů
// Globálně unikátní v celé kolekci@Attribute( name = "codeShort", unique = AttributeUniquenessType.UNIQUE_WITHIN_COLLECTION)String getCodeShort() throws ContextMissingException;
// Unikátní v rámci kolekce a lokalizace@Attribute( name = "url", localized = true, unique = AttributeUniquenessType.UNIQUE_WITHIN_COLLECTION_LOCALE)String getUrl() throws ContextMissingException;5.7 Nullable vs Non-nullable
Pokud neuvedete jinak jsou všechny atributy považovány za povinné (non-nullable). Pro volitelné atributy použijte nullable = true:
// Povinný atribut@Attribute(name = "productType")@NonnullProductType getProductType() throws ContextMissingException;
// Volitelný atribut@Attribute(name = "catalogNumber", nullable = true)@NullableString getCatalogNumber() throws ContextMissingException;
// Atribut s default hodnotou@Attribute(name = "stepOrderQuantity")@Nonnulldefault BigDecimal getStepOrderQuantity() { return BigDecimal.ONE;}5.8 Dědičnost atributů v referencích
U zpětných referencí lze specifikovat, které atributy se mají dědit:
@ReflectedReference( name = "products", ofEntity = "Product", ofName = "categories", attributeInheritanceFilter = { "assignmentValidity" } // ← pouze tento atribut)List<PublishedCategoryProduct> getProducts() throws ContextMissingException;6. Best practices
6.1 Pojmenování a konstanty
✅ Vždy používejte konstanty:
public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> { String ENTITY_NAME = "Product"; String ATTRIBUTE_CODE_SHORT = "codeShort";
@Attribute(name = ATTRIBUTE_CODE_SHORT, ...) String getCodeShort() throws ContextMissingException;}❌ Nikdy hardcoded stringy:
@Attribute(name = "codeShort", ...) // ŠPATNĚ!6.2 Dokumentace
✅ Vždy vyplňte description:
@Attribute( name = "orderedQuantity", description = """ Returns count of items that has been already sold. Allows sorting by most sold products.""", sortable = true)BigDecimal getOrderedQuantity() throws ContextMissingException;✅ Používejte ContextMissingException:
Jen tak, dostanete výjimku, pokud daný údaj může sice v databázi existovat, ale na entitě není dostupný, protože nebyl v rámci dotazu z databáze načten.
Pokud výjimku na metodě nedeklarujete, vrátí se hodnota null i pro reálně existující data, což může vést k těžko dohledatelným chybám.
/** * Returns product shortcode for feeds and GTM. * * @return product shortcode * @throws ContextMissingException if not fetched */@Attribute(name = ATTRIBUTE_CODE_SHORT, ...)String getCodeShort() throws ContextMissingException;6.3 Využívání traitů
✅ Používejte existující traity:
public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends PublishedEntityBase, WithPublishedLocalization, // místo reimplementace WithPublishedPrice, WithPublishedURL {}❌ Neduplikujte funkcionalitu:
// ŠPATNĚ - toto už je v WithPublishedLocalization!@Attribute(name = "name", localized = true, filterable = true)String getName();6.4 Plánování indexace
Po přidání nových atributů:
- Přidejte atribut do schématu
- Nasaďte změny
- Vytvořte job pro inkrementální indexaci:
java
publishingJournalService.triggerEntityChangeForPublishedEntities( catalog, entityType, new ListingQueryImpl( in(AdamPublishedCatalogEntity.PROPERTY_ID_CATALOG_ENTITY, entityIds) ));
⚠️ Nové atributy nemají hodnoty automaticky!
6.5 Testování
✅ Otestujte GraphQL query:
query { listProduct(filterBy: { codeShort_equals: "PROD123" }) { recordPage { data { primaryKey codeShort name } } }}✅ Ověřte schema:
query { __type(name: "Product") { fields { name type { name } } }}6.6 Filtrovatelné atributy
✅ Označte jako filterable/sortable pouze když je to nutné:
Každý index zabírá paměť a může zpomalit indexaci.
// DOBŘE@Attribute(name = "availability", filterable = true)String getAvailabilityRaw();
// ŠPATNĚ - zbytečný index@Attribute(name = "longDescription", filterable = true)String getLongDescription();6.7 Representative atributy
✅ Označte důležité atributy:
@Attribute(name = "catalogNumber", representative = true)String getCatalogNumber();❌ Ne všechno:
// ŠPATNĚ@Attribute(name = "longDescription", representative = true)String getLongDescription();6.8 Zpětná kompatibilita
✅ Při změnách schématu:
- Nové atributy přidávejte jako
nullable = true - Nepřejmenovávejte existující atributy
- Neodstraňujte atributy, které používají klienti
✅ Deprecated anotace:
/** * @deprecated Use {@link #getNewAttribute()} instead. * Will be removed in version 2026.06. */@Deprecated@Attribute(name = "oldName", nullable = true)String getOldAttribute();6.9 Error handling
✅ Správně používejte ContextMissingException:
// DOBŘE@Attribute(name = "name")String getName() throws ContextMissingException;
// ŠPATNĚ - chybí throws@Attribute(name = "name")String getName();*✅ Používejte IfAvailable metody:
// Pro povinné fetchnutí@Reference(name = "brand")PublishedBrand getBrand() throws ContextMissingException;
// Pro optional přístup@ReferenceRef("brand")Optional<PublishedBrand> getBrandIfAvailable();6.10 Performance
✅ Načítejte pouze potřebná data:
// DOBŘEpublishedProductService.getProductByQuery( new ProductQuery(catalogCode) .withCode(productCode) .withAttributes(ATTRIBUTE_NAME, ATTRIBUTE_PRICE));
// ŠPATNĚpublishedProductService.getProductByQuery( new ProductQuery(catalogCode) .withCode(productCode) .fetchAll() // POMALÉ!);✅ Batch operace:
// DOBŘE - jedna operaceList<PublishedProduct> products = publishedProductService.getProductsByQuery( new ProductQuery(catalogCode).withCodes(productCodes));
// ŠPATNĚ - N dotazůList<PublishedProduct> products = productCodes.stream() .map(code -> publishedProductService.getProductByCode(catalogCode, code)) .collect(Collectors.toList());6.11 Shrnutí
- Vždy používejte konstanty pro názvy
- Dokumentujte pomocí
descriptiona JavaDoc - Využívejte existující traity
- Plánujte inkrementální indexaci po změnách
- Testujte GraphQL API
- Označujte jako filterable/sortable pouze co je nutné
- Udržujte zpětnou kompatibilitu
- Správně používejte
ContextMissingException - Optimalizujte načítání dat
7. Reference - přehled traitů
Tato kapitola obsahuje detailní popis všech běžně používaných traitů.
7.1 Lokalizační traity
WithPublishedLocalization
Přidává základní lokalizované atributy jako název a popis entity.
public interface WithPublishedLocalization { String ATTRIBUTE_NAME = "name"; String ATTRIBUTE_DESCRIPTION = "description";
@Attribute(name = ATTRIBUTE_NAME, localized = true, filterable = true, sortable = true) @Nonnull String getName() throws ContextMissingException;
@Attribute(name = ATTRIBUTE_DESCRIPTION, localized = true, nullable = true) @Nullable String getDescription() throws ContextMissingException;}Používají: Product, Category, Brand, Group, Parameter, ParameterValue
7.2 Časové traity
WithPublishedValidity
Přidává časovou platnost entity (validFrom/validTo).
public interface WithPublishedValidity { String ATTRIBUTE_VALIDITY = "validity";
@Attribute( name = ATTRIBUTE_VALIDITY, nullable = true, filterable = true, sortable = true ) @Nullable DateTimeRange getValidity() throws ContextMissingException;}Používají: Product, Category, Brand, Group, PaymentMethod, ShippingMethod
7.3 Viditelnost
WithPublishedVisibility
Přidává atribut pro ovládání viditelnosti entity.
public interface WithPublishedVisibility { String ATTRIBUTE_VISIBLE = "visible";
@Attribute(name = ATTRIBUTE_VISIBLE, filterable = true) boolean isVisible() throws ContextMissingException;}Používají: Category, Brand, Group
7.4 URL traity
WithPublishedURL
Přidává URL funkcionalitu pro entitu (slugy, aktivní URL).
public interface WithPublishedURL extends WithPublishedDomain { String ATTRIBUTE_URL = "url"; String ASSOCIATED_DATA_ALL_ACTIVE_URLS = "allActiveUrls";
@Attribute(name = ATTRIBUTE_URL, localized = true, nullable = true) @Nullable String getUrl() throws ContextMissingException;
@AssociatedData(name = ASSOCIATED_DATA_ALL_ACTIVE_URLS, nullable = true) @Nullable List<ActiveUrl> getAllActiveUrls() throws ContextMissingException;}Používají: Product, Category, Brand, Group
7.5 Obsahové traity
WithPublishedTags
Přidává podporu pro tagy.
public interface WithPublishedTags { String REFERENCE_TAGS = "tags";
@Reference(name = REFERENCE_TAGS) @Nonnull List<PublishedTag> getTags() throws ContextMissingException;}Používají: Product, Category, Brand
WithPublishedMedia
Přidává podporu pro obrázky a multimédia.
public interface WithPublishedMedia { String REFERENCE_MEDIA = "media";
@Reference(name = REFERENCE_MEDIA) @Nonnull List<PublishedMedia> getMedia() throws ContextMissingException;}Používají: Product, Category, Brand, Group
WithPublishedRelatedFiles
Přidává podporu pro související soubory (PDF, dokumenty).
Používají: Product, Category, Brand
7.6 Produktové traity
WithPublishedPrice
Přidává cenovou funkcionalitu.
public interface WithPublishedPrice { @Price List<PriceContract> getPrices();
@PriceForSale PriceContract getPriceForSale() throws ContextMissingException;}Používají: Product, ProductBundle, Voucher
WithPublishedCategories
Přidává reference na kategorie.
public interface WithPublishedCategories { String REFERENCE_CATEGORIES = "categories";
@Reference(name = REFERENCE_CATEGORIES) @Nonnull List<PublishedProductCategory> getCategories() throws ContextMissingException;}Používají: Product
WithPublishedGroups
Přidává reference na skupiny produktů.
public interface WithPublishedGroups { String REFERENCE_GROUPS = "groups";
@Reference(name = REFERENCE_GROUPS) @Nonnull List<PublishedProductGroup> getGroups() throws ContextMissingException;}Používají: Product
WithPublishedParameters
Přidává parametry produktu (technické specifikace).
public interface WithPublishedParameters { String REFERENCE_PARAMETERS = "parameters";
@Reference(name = REFERENCE_PARAMETERS) @Nonnull List<PublishedProductParameter> getParameters() throws ContextMissingException;}Používají: Product
WithPublishedBrand
Přidává referenci na značku.
public interface WithPublishedBrand { String REFERENCE_BRAND = "brand";
@Reference(name = REFERENCE_BRAND) @Nullable PublishedBrand getBrand() throws ContextMissingException;}Používají: Product
7.7 Variantní traity
WithPublishedMasterVariantProperties
Přidává podporu pro master/variant produkty.
Používají: Product
7.8 Skladové traity
WithPublishedStock
Přidává informace o skladových zásobách.
Používají: Product
WithPublishedStockVisibility
Přidává nastavení viditelnosti skladů.
Používají: Product
7.9 Výhody kompozičního přístupu
public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends PublishedEntityBase, // Základní funkcionalita WithPublishedLocalization, // Název, popis WithPublishedPrice, // Ceny WithPublishedRelatedFiles, // Soubory WithPublishedMedia, // Obrázky WithPublishedURL, // URL WithPublishedTags, // Tagy WithPublishedParameters, // Parametry WithPublishedCategories, // Kategorie WithPublishedGroups, // Skupiny WithPublishedValidity, // Časová platnost WithPublishedStock, // Sklady WithPublishedMasterVariantProperties, SealedInstance<READ_MODEL, WRITE_MODEL> { // ... specifické atributy produktu}Výhody:
- Znovupoužitelnost - stejná funkcionalita na více entitách
- Konzistenci - jednotné pojmenování a chování
- Modulárnost - snadné přidání/odebrání funkcionality
- Udržovatelnost - změny na jednom místě se projeví všude
