Eshop — návrh datového modelu v2
Inspirační zdroje
| Oblast |
Inspirace |
| Katalog |
Vendure |
| Pricing |
Medusa |
| Inventory |
Medusa |
| Attribute/Option rozlišení |
Sylius |
| API |
vlastní REST |
Rozhodnutí z review
PK strategie
UUID pro veřejnou identitu (API, URL, externí systémy), bigint generated always as identity pro interní PK u hodně joinovacích tabulek. Kompromis: joinovací tabulky (product_variant_option_value, product_category_assignment, category_closure) mají bigint, entity s veřejnou identitou mají UUID.
Typy pro peníze a sazby
float v databázi pro business data nepoužíváme.
| 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
Místo čtyř boolean příznaků jeden enum publication_status plus specifické flags kde mají jasnou logiku.
publication_status:
draft | published | hidden | blocked | discontinued | deleted
Vedle toho pouze:
- is_blocked_by_editor — zablokoval editor ručně
- is_deleted_by_import — označen ke smazání importem
- is_clearance — doprodej
- hide_until — dočasně skrýt do data
Slug unikátnost
Unikátnost slugů je vždy složená UNIQUE(locale, slug), ne globální. Platí pro product_slug, category_translation, collection_translation.
region_code → FK
V price je region_id uuid NULL FK místo textového region_code. Textový kód zůstává pouze na region.code.
Closure table
category_closure je zdroj pravdy pro stromové dotazy. parent_id na category je záměrně redundantní convenience cache. Musí platit konzistence přes procedury.
Search sync
Žádný těžký trigger na product_search_document. Trigger pouze označí dirty stav do product_search_dirty, worker/job provede reindex.
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 |
|
is_deleted_by_import |
boolean |
|
is_new |
boolean |
|
is_clearance |
boolean |
Doprodej |
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(locale, slug) |
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 |
|
product_supplier_feed_raw
Raw payload pro debugging importů.
| Pole |
Typ |
Popis |
id |
bigint PK |
|
supplier_feed_id |
uuid FK |
|
raw_data |
jsonb |
Surová data z importu |
imported_at |
timestamptz |
|
Vrstva 2 — lokalizace
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) |
Fallback na výchozí jazyk cs řeší aplikační vrstva.
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 |
|
option_group_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
option_group_id |
uuid FK |
|
locale |
varchar |
|
name |
varchar |
|
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 |
|
option_value_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
option_value_id |
uuid FK |
|
locale |
varchar |
|
name |
varchar |
|
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:
- 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 |
|
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é |
Vrstva 4 — ceny (Medusa pricing model)
Filosofie
Varianta nemá jednu cenu — má price_set ze kterého se vybírá cena podle kontextu. Vstupní ceny jsou odděleny od odvozených. 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 |
|
max_quantity |
int |
|
valid_from |
timestamptz |
|
valid_to |
timestamptz |
|
Překryvy platnosti (valid_from/to, min/max_quantity) musí být řešeny aplikačně nebo procedurou. Bez toho mohou existovat dvě platné ceny současně.
Mapování: cena_a → sale, cena_b → original, cena_c → cost, cena_montaze → install.
tax_rate
| Pole |
Typ |
Popis |
id |
uuid PK |
|
region_id |
uuid FK |
|
name |
varchar |
|
rate |
numeric(5,2) |
|
is_default |
boolean |
|
| 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) |
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 |
Vrstva 6 — kategorie a kolekce
Filosofie
Jediný canonical zdroj pravdy — žádný pipe-separated sloupec. Strom implementován jako closure table. parent_id je záměrně redundantní convenience cache — category_closure je zdroj pravdy pro stromové dotazy.
Closure table — procedury (povinné)
Bez procedur je closure table snadno rozbitelná.
category_insert_node(parent_id, ...)
category_move_subtree(node_id, new_parent_id)
category_delete_subtree(node_id)
category_rebuild_closure()
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(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
Pomocná tabulka — mapuje staré pipe-separated hodnoty na nové UUID.
| 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(locale, slug) |
collection_product
| Pole |
Typ |
Popis |
collection_id |
uuid FK |
|
product_id |
uuid FK |
|
sort_order |
int |
|
|
|
PRIMARY KEY (collection_id, product_id) |
| 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 |
|
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 |
|
product_accessory
Odděleno od relations — jiné chování v UI a košíku.
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK |
|
accessory_product_id |
uuid FK |
|
sort_order |
int |
|
product_merchandising
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK UNIQUE |
|
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 |
|
EAV pro feed-specific data. Mapuje google_param, popis_ga, kategorie_ga, gensace_*.
| 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
PHE a recyklační poplatky.
| 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 |
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 |
|
reason |
varchar |
Proč je dirty (price_changed, stock_changed, …) |
created_at |
timestamptz |
|
Audit log
Minimálně pro entity kde je auditovatelnost kritická.
entity_audit_log
| Pole |
Typ |
Popis |
id |
bigint PK |
|
entity_type |
varchar |
product, price, inventory_level, category, translation |
entity_id |
uuid |
|
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 — struktura záznamu v
order_item (vrstva 8): 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 — DB constraint nebo aplikační procedura? Závisí na zvoleném DB enginu.
- Closure table procedury — implementovat jako PL/pgSQL funkce nebo aplikační vrstvou?