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_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)

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)

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

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

product_external_metadata

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?