EdeeShop · Implementace

Publikace dat do evitaDB

Upraveno

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
java
// 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:

java
@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:

java
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:

java
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 - tagy
  • WithPublishedMedia - obrázky a multimédia
  • WithPublishedRelatedFiles - soubory (PDF, dokumenty)

Produktové:

  • WithPublishedPrice - cenová funkcionalita
  • WithPublishedCategories - kategorie produktů
  • WithPublishedGroups - skupiny produktů
  • WithPublishedParameters - parametry produktů
  • WithPublishedBrand - značka produktu
  • WithPublishedStock - skladové zásoby
  • WithPublishedMasterVariantProperties - 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:

java
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:

java
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ů:

java
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 evitaDB
  • description - textový popis entity (použit v GraphQL schema)
  • allowedEvolution - režimy automatické evoluce schématu

Příklad:

java
@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:

java
@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 atributu
  • description - 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átnosti
  • localized - 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 typu
  • scope - nastavení atributu podle scope (Live, Archived)

Příklady:

java
// 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:

java
@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:

java
@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ů:

java
@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:

java
@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:

java
@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:

java
@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:

java
@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é reference
  • ofEntity - název entity, ze které reference pochází
  • ofName - název původní reference
  • attributeInheritanceFilter - 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:

java
@ReferencedEntityPublishedCategory getCategory();
@ReferencedEntityGroupPublishedGroup getGroup();

3.5 Cenové anotace

@Price

Označuje kolekci cen entity:

java
@PriceList<PriceContract> getPrices();

@PriceForSale

Označuje aktuální prodejní cenu entity:

java
@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:

java
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:

java
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:

java
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

java
public interface DemoPublishedProductEditor extends    PublishedProductEditorContract<DemoPublishedProduct, DemoPublishedProductEditor>,    DemoPublishedProductEditorContract {}

Krok 5: Servisa

Vytvořte projektovou servisu pro přístup k datům:

java
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:

java
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:

java
@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:

java
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():

java
@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 anotacemi
  • PublishedMyEntityEditor - rozhraní editoru (pro nahrazení na projektu)
  • PublishedMyEntityEditorContract<READ_MODEL, WRITE_MODEL> - contract editoru

2. Contract musí dědit z PublishedEntityBase:

java
@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:

java
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:

java
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:

java
evitaPublishingService.registerConversionSnippet(new MyEntityConversionSnippet());evitaPublishingService.getPublishingDescriptorFor(MY_ENTITY)    .registerPublishedServiceClass(PublishedMyEntityService.class);

7. CatalogEntityType:

Nezapomeňte přidat nový typ do enum CatalogEntityType:

java
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

AspektStandardní entitaEntity bez CatalogEntityType
CatalogEntityTypeVyžaduje přidání do enumNepotřebuje
Dědičnost ContractPublishedEntityBaseWithEntitySchema, TraitSupport, Serializable
Registrace schématuAutomatickáManuální (defineEntitySchemaFromModelClass)
Publikace datPřes PublishingJournalServiceManuální přes evitaDB session
AktualizaceAutomatickéVlastní change listener

Krok 1: Contract rozhraní

Contract definuje schéma entity pomocí anotací. Nedědí z PublishedEntityBase:

java
@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

java
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í

java
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():

java
@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:

java
@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:

java
@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:

java
@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 CatalogEntityType enum 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

  1. 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)    ));
  2. Ú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émPopisŘešení
Indexy na starých datechZměny v indexech se nepromítají na již zaindexovaná data (issue #409)Provést plnou reindexaci
Staré indexy v pamětiPůvodní indexy zůstávají v DB a paměti i po změně schématuRestart + reindexace
Prázdné nové prvkyNově založené atributy/reference nemají dataInkrementální indexace

Reference na kód

⚠️ 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í:

java
@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:

java
@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:

java
@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:

java
@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í):

java
@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:

java
@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:

java
@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ů

java
// 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:

java
// 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:

java
@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:

java
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:

java
@Attribute(name = "codeShort", ...)  // ŠPATNĚ!

6.2 Dokumentace

✅ Vždy vyplňte description:

java
@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.

java
/** * 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:

java
public interface PublishedProductContract<READ_MODEL, WRITE_MODEL> extends    PublishedEntityBase,    WithPublishedLocalization,  // místo reimplementace    WithPublishedPrice,    WithPublishedURL {}

❌ Neduplikujte funkcionalitu:

java
// Š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ů:

  1. Přidejte atribut do schématu
  2. Nasaďte změny
  3. 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:

graphql
query {  listProduct(filterBy: { codeShort_equals: "PROD123" }) {    recordPage {      data {        primaryKey        codeShort        name      }    }  }}

✅ Ověřte schema:

graphql
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.

java
// 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:

java
@Attribute(name = "catalogNumber", representative = true)String getCatalogNumber();

❌ Ne všechno:

java
// Š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:

java
/** * @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:

java
// DOBŘE@Attribute(name = "name")String getName() throws ContextMissingException;
// ŠPATNĚ - chybí throws@Attribute(name = "name")String getName();

*✅ Používejte IfAvailable metody:

java
// 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:

java
// 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:

java
// 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í description a 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.

java
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).

java
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.

java
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).

java
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.

java
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.

java
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.

java
public interface WithPublishedPrice {    @Price    List<PriceContract> getPrices();
    @PriceForSale    PriceContract getPriceForSale() throws ContextMissingException;}

Používají: Product, ProductBundle, Voucher

WithPublishedCategories

Přidává reference na kategorie.

java
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ů.

java
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).

java
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.

java
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

java
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