Eshop — návrh datového modelu v3

Inspirační zdroje

Oblast Inspirace
Katalog Vendure
Pricing Medusa
Inventory Medusa
Attribute/Option rozlišení Sylius
API vlastní REST

Rozhodnutí z review (souhrn všech verzí)

PK strategie

UUID pro veřejnou identitu (API, URL, externí systémy). bigint generated always as identity pro interní PK joinovacích a překladových tabulek.

Typy pro peníze a sazby

Pole Typ
amount (ceny, poplatky) int — v haléřích/centech
exchange_rate numeric(18,8)
tax_rate numeric(5,2)
discount_value numeric(12,4)
weight numeric(10,3)
width, height, length numeric(10,2)

Stavový model produktu

publication_status vyjadřuje aktuální stav produktu. Flags vyjadřují původ nebo důvod stavu. Není to duplicita — je to záměrná vrstva.

publication_status Bedeutung
draft Rozpracovaný, nezveřejněný
published Aktivní na webu
hidden Dočasně skrytý (viz hide_until)
blocked Blokovaný — viz flag is_blocked_by_editor
discontinued Vyřazený z nabídky
deleted Označen ke smazání — viz flag is_deleted_by_import

Flags říkají proč: - is_blocked_by_editor — editor ručně zablokoval - is_deleted_by_import — import označil ke smazání - is_clearance — doprodej (může být published i discontinued) - hide_until — dočasně skrýt do data (systém automaticky přepne na hidden/published)

is_new — odstraněno

is_new jako boolean nedává smysl — "nový" je časově závislý stav. Nahrazeno: - new_until timestamptz — produkt je "nový" dokud toto datum neuplyne - nebo odvozovat z created_at v aplikační vrstvě

Slug unikátnost

Vždy složená UNIQUE(locale, slug) pro unikátnost napříč jazyky. Plus UNIQUE(entity_id, locale) aby jedna entita neměla dva slugy pro stejný jazyk.

Audit log entity_id

Různé entity mají různé typy PK. Řešeno jako entity_id text — univerzální, bez JOIN problémů.


Vrstva 1 — produkt, identita, stavy, dodavatel

product

Pole Typ Popis
id uuid PK Veřejná identita
code varchar UK Obchodní kód — hlavní klíč pro import a párování
ean varchar EAN kód
brand_id uuid FK
web_id bigint Mapovací klíč na starý systém
publication_status enum draft, published, hidden, blocked, discontinued, deleted
is_blocked_by_editor boolean Původ stavu blocked
is_deleted_by_import boolean Původ stavu deleted
is_clearance boolean Doprodej — nezávislé na publication_status
new_until timestamptz Produkt je "nový" do tohoto data
hide_until timestamptz Dočasně skrýt do tohoto data
created_at timestamptz
updated_at timestamptz

brand

Pole Typ Popis
id uuid PK
name varchar
slug varchar UK
website varchar
logo varchar
is_local boolean
web_id bigint Mapovací klíč na starý systém

product_slug

Pole Typ Popis
id bigint PK
product_id uuid FK
locale varchar cs, sk, de, hu, pl
slug varchar
UNIQUE(product_id, locale) — jeden slug per jazyk per produkt
UNIQUE(locale, slug) — globální unikátnost slugu v jazyce

supplier

Pole Typ Popis
id uuid PK
name varchar
code varchar UK

product_supplier_feed

Pole Typ Popis
id uuid PK
product_id uuid FK
supplier_id uuid FK
supplier_code varchar Kód produktu u dodavatele
supplier_ean varchar
lock_content boolean Nepřepisovat textová data (neprepsat)
last_imported_at timestamptz

Import logika: dodavatel insertuje pokud neexistuje code/ean → produkt vznikne jako draft. Po prvním insertu přepisuje pouze ceny a skladovou dostupnost. lock_content = true chrání textová data.

product_supplier_feed_raw

Pole Typ Popis
id bigint PK
supplier_feed_id uuid FK
raw_data jsonb Surová data z importu
imported_at timestamptz

Vrstva 2 — lokalizace

Fallback na výchozí jazyk cs řeší aplikační vrstva.

product_translation

Pole Typ Popis
id bigint PK
product_id uuid FK
locale varchar cs, sk, de, hu, pl
name varchar
name_heureka varchar Název pro Heureka feed
description_short text
description_long text
description_tech text
out_of_stock_text text Obchodní sdělení pro zákazníka
is_machine_translated boolean Příznak pro editorial workflow
created_at timestamptz
updated_at timestamptz
UNIQUE(product_id, locale)

brand_translation

Pole Typ Popis
id bigint PK
brand_id uuid FK
locale varchar
name varchar
UNIQUE(brand_id, locale)

Vrstva 3 — varianty a atributy

Rozlišení Option vs. Attribute (Sylius)

Typ Definice Vliv na SKU
Option Osa varianty — velikost, barva Ano
Attribute Popisná vlastnost — materiál, váha Ne

Historické typy varianty a barvy mapují jako option_group.type = variant a option_group.type = color.

product_variant

Pole Typ Popis
id uuid PK
product_id uuid FK
code varchar UK
ean varchar
is_active boolean
is_default boolean
sort_order int
created_at timestamptz
updated_at timestamptz

product_variant_translation

Pole Typ Popis
id bigint PK
variant_id uuid FK
locale varchar
name varchar
UNIQUE(variant_id, locale)

option_group

Pole Typ Popis
id uuid PK
product_id uuid FK
code varchar
type varchar variant, color
sort_order int
UNIQUE(product_id, code)

option_group_translation

Pole Typ Popis
id bigint PK
option_group_id uuid FK
locale varchar
name varchar
UNIQUE(option_group_id, locale)

option_value

Pole Typ Popis
id uuid PK
option_group_id uuid FK
code varchar
color_hex varchar Pro type = color
image varchar Vzorník
sort_order int
UNIQUE(option_group_id, code)

option_value_translation

Pole Typ Popis
id bigint PK
option_value_id uuid FK
locale varchar
name varchar
UNIQUE(option_value_id, locale)

product_variant_option_value

Pole Typ Popis
variant_id uuid FK
option_value_id uuid FK
PRIMARY KEY (variant_id, option_value_id)

Constrainty aplikační vrstvou / procedurou: - Jedna varianta nesmí mít dvě hodnoty ze stejné option_group. - Jedna kombinace option values nesmí existovat dvakrát pro stejný produkt.

attribute

Pole Typ Popis
id uuid PK
code varchar UK
type varchar text, number, boolean

attribute_translation

Pole Typ Popis
id bigint PK
attribute_id uuid FK
locale varchar
name varchar
UNIQUE(attribute_id, locale)

product_attribute_value

Pole Typ Popis
id bigint PK
product_id uuid FK
attribute_id uuid FK
value varchar
locale varchar Atributy mohou být lokalizované
UNIQUE(product_id, attribute_id, locale)

Vrstva 4 — ceny (Medusa pricing model)

Filosofie

Varianta nemá jednu cenu — má price_set ze kterého se vybírá cena podle kontextu. Všechny peněžní hodnoty jsou int v haléřích/centech.

region

Pole Typ Popis
id uuid PK
code varchar UK CZ, SK, DE, HU, PL
name varchar
currency_code varchar CZK, EUR
tax_mode varchar inclusive, exclusive
exchange_rate numeric(18,8) Přepočet z CZK

price_set

Pole Typ Popis
id uuid PK
variant_id uuid FK UNIQUE 1:1 s variantou
created_at timestamptz

price

Pole Typ Popis
id uuid PK
price_set_id uuid FK
region_id uuid FK NULL NULL = platí pro všechny regiony
currency_code varchar
amount int V haléřích/centech
type varchar sale, original, cost, install
min_quantity int NULL
max_quantity int NULL
valid_from timestamptz NULL
valid_to timestamptz NULL
CHECK (amount >= 0)
CHECK (min_quantity IS NULL OR min_quantity > 0)
CHECK (max_quantity IS NULL OR max_quantity >= min_quantity)
CHECK (valid_to IS NULL OR valid_to > valid_from)

Překryvy platnosti musí být řešeny aplikačně nebo procedurou — DB constraint by byl příliš komplexní.

Mapování: cena_asale, cena_boriginal, cena_ccost, cena_montazeinstall.

tax_rate

Pole Typ Popis
id uuid PK
region_id uuid FK
name varchar
rate numeric(5,2)
is_default boolean

promotion

Pole Typ Popis
id uuid PK
code varchar UK
name varchar
target_type varchar product, brand, category
target_id uuid ID cílové entity
discount_type varchar percent, fixed
discount_value numeric(12,4)
priority int Řeší konflikt více akcí
only_in_stock boolean Akce jen pokud skladem
valid_from timestamptz
valid_to timestamptz
is_active boolean

Vrstva 5 — sklad a dostupnost (Medusa inventory model)

Filosofie

Fyzický stock je oddělen od obchodní dostupnosti. Zákazník vidí obchodní sdělení, systém pracuje s fyzickými zásobami per lokace.

inventory_item

Pole Typ Popis
id uuid PK
variant_id uuid FK UNIQUE 1:1 s variantou
sku varchar UK
requires_shipping boolean
weight numeric(10,3) kg
width numeric(10,2) cm
height numeric(10,2) cm
length numeric(10,2) cm
created_at timestamptz
updated_at timestamptz

stock_location

Pole Typ Popis
id uuid PK
code varchar UK
name varchar
is_central boolean Mapuje eshop_produkty_sklad_centrala

inventory_level

Pole Typ Popis
id bigint PK
inventory_item_id uuid FK
location_id uuid FK
stocked_quantity int Fyzicky na skladě
reserved_quantity int Rezervováno v otevřených objednávkách
incoming_quantity int Na cestě (na_ceste)
UNIQUE(inventory_item_id, location_id)
CHECK (stocked_quantity >= 0)
CHECK (reserved_quantity >= 0)
CHECK (incoming_quantity >= 0)

CHECK (reserved_quantity <= stocked_quantity) záměrně chybí. V reálném provozu nastane situace kdy inventura nebo vrácení dodavateli sníží stocked_quantity pod aktuální reserved_quantity. DB constraint by způsobil selhání legitimní operace. Oversell a backorder jsou obchodní rozhodnutí — hlídají se aplikačně při vytváření rezervace, ne jako DB invariant.

Dostupné množství = stocked_quantity - reserved_quantity

supplier_availability

Pole Typ Popis
id bigint PK
inventory_item_id uuid FK
supplier_id uuid FK
availability_code varchar in_stock, on_order, unavailable
availability_text varchar Textový popis od dodavatele
eta timestamptz Očekávané datum naskladnění
updated_at timestamptz

availability_text

Pole Typ Popis
id bigint PK
inventory_item_id uuid FK
locale varchar
text varchar Text zobrazený zákazníkovi
display_type varchar in_stock, low_stock, out_of_stock, on_order
UNIQUE(inventory_item_id, locale)

Vrstva 6 — kategorie a kolekce

Filosofie

category_closure je zdroj pravdy pro stromové dotazy. parent_id je záměrně redundantní convenience cache pro rychlý přístup k přímému rodiči. Konzistence musí být zajištěna procedurou — nikdy přímým zápisem do category_closure.

Closure table — povinné procedury

category_insert_node(parent_id uuid, ...) → uuid
category_move_subtree(node_id uuid, new_parent_id uuid)
category_delete_subtree(node_id uuid)
category_rebuild_closure()  -- záchranná funkce pro případ corruption

category

Pole Typ Popis
id uuid PK
parent_id uuid FK NULL Convenience cache — zdroj pravdy je closure table
code varchar UK
legacy_id bigint Staré číselné ID pro migraci
depth int Hloubka v stromu
sort_order int
is_active boolean
is_internal boolean Nezobrazuje se na webu
created_at timestamptz
updated_at timestamptz

category_closure

Pole Typ Popis
ancestor_id uuid FK
descendant_id uuid FK
depth int 0 = uzel sám na sebe
PRIMARY KEY (ancestor_id, descendant_id)
CHECK (depth >= 0)

category_translation

Pole Typ Popis
id bigint PK
category_id uuid FK
locale varchar
name varchar
description text
slug varchar
meta_title varchar
meta_description varchar
UNIQUE(category_id, locale)
UNIQUE(locale, slug)

product_category_assignment

Pole Typ Popis
id bigint PK
product_id uuid FK
category_id uuid FK
is_primary boolean Hlavní kategorie pro breadcrumb a URL
sort_order int
UNIQUE(product_id, category_id)

category_migration_map

Pole Typ Popis
id bigint PK
legacy_id bigint Staré číselné ID
new_category_id uuid FK
legacy_pipe_value varchar Hodnota jak vypadala v pipe listu (\|123\|)
is_mapped boolean

collection

Pole Typ Popis
id uuid PK
code varchar UK
type varchar manual, automated, seasonal
is_active boolean
sort_order int

collection_translation

Pole Typ Popis
id bigint PK
collection_id uuid FK
locale varchar
name varchar
slug varchar
description text
UNIQUE(collection_id, locale)
UNIQUE(locale, slug)

collection_product

Pole Typ Popis
collection_id uuid FK
product_id uuid FK
sort_order int
PRIMARY KEY (collection_id, product_id)

Vrstva 7 — média, vazby, merchandising, metadata

product_media

Pole Typ Popis
id uuid PK
product_id uuid FK
variant_id uuid FK NULL Nullable — médium může patřit variantě
type varchar image, video, document
url varchar
alt varchar
sort_order int
is_primary boolean

variant_id musí patřit ke stejnému product_id. Hlídáno procedurou nebo triggerem — DB FK to sám o sobě nezaručí.

product_relation

Pole Typ Popis
id bigint PK
product_id uuid FK
related_product_id uuid FK
relation_type varchar related, category_related
sort_order int
UNIQUE(product_id, related_product_id, relation_type)

product_accessory

Pole Typ Popis
id bigint PK
product_id uuid FK
accessory_product_id uuid FK
sort_order int
UNIQUE(product_id, accessory_product_id)

product_merchandising

Pole Typ Popis
id bigint PK
product_id uuid FK UNIQUE 1:1 s produktem
is_featured boolean Doporučujeme
is_bestseller boolean
is_recommended boolean
is_sale boolean Akční nabídka
priority int
badge_label varchar Štítek (stitek)
in_slider_1 boolean
in_slider_2 boolean
slider_1_priority int
slider_2_priority int

product_external_metadata

Pole Typ Popis
id bigint PK
product_id uuid FK
channel varchar google, heureka, zbozi
key varchar
value text
UNIQUE(product_id, channel, key)

product_compliance

Pole Typ Popis
id bigint PK
product_id uuid FK
fee_type varchar phe, recycling
fee_code varchar skupina_phe, phe_kod
linked_product_id uuid FK NULL Přidružená položka do košíku
fee_amount int V haléřích
CHECK (fee_amount >= 0)

product_search_document

Pole Typ Popis
id bigint PK
product_id uuid FK
locale varchar
search_text text Fulltext — název, kód, EAN, výrobce
structured_data jsonb Cena, dostupnost, facety — vše bez joinů
indexed_at timestamptz
UNIQUE(product_id, locale)

product_search_dirty

Dirty queue pro async reindex. Trigger pouze zapíše sem, worker provede reindex.

Pole Typ Popis
id bigint PK
product_id uuid FK
locale varchar
reasons varchar[] {price_changed, stock_changed, content_changed, …} — array, ne jeden řádek per reason
created_at timestamptz
processed_at timestamptz NULL NULL = čeká na zpracování
UNIQUE(product_id, locale) WHERE processed_at IS NULL — partial unique index

reasons je array aby jeden pending job per produkt per locale pokryl všechny důvody najednou. Při vícenásobné změně před zpracováním se reasons doplní do existujícího záznamu (INSERT ... ON CONFLICT DO UPDATE SET reasons = reasons || EXCLUDED.reasons), ne vytvoří nový řádek.


Audit log

entity_audit_log

Pole Typ Popis
id bigint PK
entity_type varchar product, price, inventory_level, category, translation
entity_id text Univerzální — pokryje uuid i bigint PK
action varchar insert, update, delete
old_data jsonb
new_data jsonb
actor_type varchar editor, import, system
actor_id uuid NULL
created_at timestamptz

Doporučené pořadí implementace

1.  product, brand, supplier, product_supplier_feed, product_supplier_feed_raw
2.  product_translation, brand_translation, product_slug
3.  product_variant, option_group, option_value, product_variant_option_value
    attribute, product_attribute_value
4.  region, price_set, price, tax_rate, promotion
5.  inventory_item, stock_location, inventory_level
    supplier_availability, availability_text
6.  category + category_closure + procedury
    category_translation, product_category_assignment, category_migration_map
7.  collection, product_media, product_relation, product_accessory
    product_merchandising, product_external_metadata, product_compliance
8.  product_search_document, product_search_dirty
9.  entity_audit_log
10. order item snapshot (samostatná vrstva)

Otevřené otázky

  • Order item snapshot — které pole se snapshotuje a v jakém formátu (flat sloupce vs. jsonb blob)?
  • Facety — samostatná vrstva nad atributy nebo odvozeno z product_attribute_value při indexaci do search dokumentu?
  • Constraint překryvu cen — aplikační procedura nebo DB trigger?
  • Closure table procedury — PL/pgSQL funkce nebo aplikační vrstvou?