Product — Pozycja katalogowa, którą można zamówić (SKU, cena, opis)
ProductInstance — Fizyczny/serializowany przedmiot w magazynie
Można to sobie wyobrazić jako:
ProductType = „Laptop" (definiuje jakie atrybuty istnieją)
Product = „ThinkPad X1 Carbon Gen 11" (to, co zamawiasz)
ProductInstance = „Nr seryjny #ABC123" (to, co otrzymujesz)
Struktura
Atrybuty
ProductType
Atrybut
Typ
Wymagane
Opis
id
UUID
Tak
Unikalny identyfikator
code
String
Tak
Unikalny kod (LAPTOP, TSHIRT)
name
String
Tak
Nazwa wyświetlana
description
String
Nie
Opis kategorii
parent
ProductType
Nie
Kategoria nadrzędna (null = korzeń)
attributeDefinitions
AttributeDef[]
Nie
Atrybuty dla tego typu
active
Boolean
Tak
Czy kategoria jest aktywna?
Product
Atrybut
Typ
Wymagane
Opis
id
UUID
Tak
Unikalny identyfikator
sku
String
Tak
Stock Keeping Unit (unikalny)
name
String
Tak
Nazwa produktu
description
String
Nie
Szczegółowy opis
productType
ProductType
Tak
Kategoria, do której należy
basePrice
Money
Tak
Bazowa cena sprzedaży
costPrice
Money
Nie
Cena kosztu/zakupu
attributes
Map
Nie
Atrybuty specyficzne dla typu
status
ProductStatus
Tak
Draft, Active, Discontinued
weight
Decimal
Nie
Waga w kg
dimensions
Dimensions
Nie
D × S × W w cm
ProductInstance
Atrybut
Typ
Wymagane
Opis
id
UUID
Tak
Unikalny identyfikator
product
Product
Tak
Którego produktu dotyczy
serialNumber
String
Nie
Unikalny nr seryjny (dla serializowanych)
batchNumber
String
Nie
Numer partii/serii
manufacturedDate
Date
Nie
Data produkcji
expiryDate
Date
Nie
Data ważności (towary łatwo psujące się)
status
InstanceStatus
Tak
Available, Sold, Damaged, itd.
AttributeDefinition
Atrybut
Typ
Wymagane
Opis
code
String
Tak
Kod atrybutu (RAM, SIZE)
name
String
Tak
Nazwa wyświetlana
dataType
Enum
Tak
String, Integer, Decimal, Boolean, Date, Enum
required
Boolean
Tak
Czy musi być ustawiony na produkcie?
options
String[]
Nie
Dozwolone wartości (dla typu Enum)
unit
String
Nie
Jednostka miary (GB, cm)
minValue
Decimal
Nie
Minimalna dozwolona wartość
maxValue
Decimal
Nie
Maksymalna dozwolona wartość
Zachowania
Entity ProductType:
function isLeaf(): Boolean
return children is empty
function isRoot(): Boolean
return parent is None
function getPath(): String[]
// Returns ["Electronics", "Computers", "Laptops"]
if parent is None:
return [name]
return parent.getPath() + [name]
function getAllAttributeDefinitions(): AttributeDefinition[]
// Inherit from ancestors + own
inherited = parent?.getAllAttributeDefinitions() or []
return inherited + attributeDefinitions
function canHaveProducts(): Boolean
// Only leaf types can have products directly
return isLeaf()
function getDescendants(): ProductType[]
result = children.copy()
for child in children:
result += child.getDescendants()
return result
Entity Product:
function isAvailable(): Boolean
return status == ProductStatus.Active
function getAttribute(code: String): Any
return attributes.get(code)
function setAttribute(code: String, value: Any):
definition = productType.getAllAttributeDefinitions()
.find(d => d.code == code)
require definition is not None // Attribute must be defined
require definition.validate(value) // Value must be valid
attributes[code] = value
function validate(): ValidationResult
errors = []
for definition in productType.getAllAttributeDefinitions():
value = attributes.get(definition.code)
if definition.required and value is None:
errors.add("Missing required attribute: " + definition.code)
if value is not None and not definition.validate(value):
errors.add("Invalid value for " + definition.code)
return ValidationResult(isValid: errors.isEmpty(), errors: errors)
function calculatePrice(customer: Customer): Money
price = basePrice
// Apply customer-specific pricing
if customer.loyaltyTier == "Gold":
price = price * 0.95 // 5% discount
// Apply quantity discounts, promotions, etc.
return price
function discontinue():
require status == ProductStatus.Active
status = ProductStatus.Discontinued
emit ProductDiscontinued(this)
Entity ProductInstance:
function isAvailable(): Boolean
return status == InstanceStatus.Available
function isExpired(): Boolean
return expiryDate is not None and expiryDate < today()
function sell(order: Order):
require isAvailable()
require not isExpired()
status = InstanceStatus.Sold
soldTo = order
soldAt = now()
emit InstanceSold(this, order)
function markDamaged(reason: String):
status = InstanceStatus.Damaged
damageReason = reason
damagedAt = now()
Entity AttributeDefinition:
function validate(value: Any): Boolean
if value is None:
return not required
// Type check
match dataType:
case DataType.String:
if not (value is String): return false
case DataType.Integer:
if not (value is Integer): return false
case DataType.Decimal:
if not (value is Number): return false
case DataType.Boolean:
if not (value is Boolean): return false
case DataType.Enum:
if value not in options: return false
// Range check
if minValue is not None and value < minValue:
return false
if maxValue is not None and value > maxValue:
return false
return true
sql-- Product type hierarchyCREATE TABLE product_type (
id UUID PRIMARY KEY,
code VARCHAR(50) NOT NULLUNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
parent_id UUID REFERENCES product_type(id),
active BOOLEANNOT NULLDEFAULTTRUE,
created_at TIMESTAMPNOT NULLDEFAULTCURRENT_TIMESTAMP
);
-- Attribute definitions for product typesCREATE TABLE attribute_definition (
id UUID PRIMARY KEY,
product_type_id UUID NOT NULLREFERENCES product_type(id) ONDELETE CASCADE,
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
data_type VARCHAR(20) NOT NULL, -- 'String', 'Integer', 'Decimal', 'Boolean', 'Enum'
required BOOLEANNOT NULLDEFAULTFALSE,
options TEXT, -- JSON array for Enum type: '["S", "M", "L", "XL"]'
unit VARCHAR(20),
min_value DECIMAL,
max_value DECIMAL,
sort_order INTEGERNOT NULLDEFAULT0,
UNIQUE(product_type_id, code)
);
-- Products (catalog items)CREATE TABLE product (
id UUID PRIMARY KEY,
sku VARCHAR(50) NOT NULLUNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
product_type_id UUID NOT NULLREFERENCES product_type(id),
base_price_cents INTEGERNOT NULL,
base_price_currency CHAR(3) NOT NULLDEFAULT'PLN',
cost_price_cents INTEGER,
cost_price_currency CHAR(3),
status VARCHAR(20) NOT NULLDEFAULT'Draft',
weight_kg DECIMAL(10,3),
length_cm DECIMAL(10,2),
width_cm DECIMAL(10,2),
height_cm DECIMAL(10,2),
created_at TIMESTAMPNOT NULLDEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPNOT NULLDEFAULTCURRENT_TIMESTAMP,
CONSTRAINT chk_product_status CHECK (status IN ('Draft', 'Active', 'Discontinued', 'Deleted'))
);
-- Product attribute values (EAV pattern)CREATE TABLE product_attribute (
product_id UUID NOT NULLREFERENCES product(id) ONDELETE CASCADE,
attribute_code VARCHAR(50) NOT NULL,
value_string VARCHAR(255),
value_integer INTEGER,
value_decimal DECIMAL(15,4),
value_boolean BOOLEAN,
value_date DATE,
PRIMARY KEY (product_id, attribute_code)
);
-- Product instances (serialized items)CREATE TABLE product_instance (
id UUID PRIMARY KEY,
product_id UUID NOT NULLREFERENCES product(id),
serial_number VARCHAR(100) UNIQUE,
batch_number VARCHAR(50),
manufactured_date DATE,
expiry_date DATE,
status VARCHAR(20) NOT NULLDEFAULT'Available',
created_at TIMESTAMPNOT NULLDEFAULTCURRENT_TIMESTAMP,
CONSTRAINT chk_instance_status CHECK (
status IN ('Available', 'Reserved', 'Sold', 'Damaged', 'Expired', 'Returned')
)
);
-- IndexesCREATE INDEX idx_product_type ON product(product_type_id);
CREATE INDEX idx_product_status ON product(status);
CREATE INDEX idx_product_type_parent ON product_type(parent_id);
CREATE INDEX idx_instance_product ON product_instance(product_id);
CREATE INDEX idx_instance_status ON product_instance(status);
CREATE INDEX idx_instance_expiry ON product_instance(expiry_date) WHERE expiry_date ISNOT NULL;
Typowe zapytania
sql-- Get product with all attributesSELECT
p.*,
json_object_agg(pa.attribute_code,
COALESCE(pa.value_string, pa.value_integer::text,
pa.value_decimal::text, pa.value_boolean::text,
pa.value_date::text)
) as attributes
FROM product p
LEFTJOIN product_attribute pa ON pa.product_id = p.id
WHERE p.id = :productId
GROUPBY p.id;
-- Get products by type (including subtypes)WITHRECURSIVE type_tree AS (
SELECT id FROM product_type WHERE id = :typeId
UNIONALLSELECT pt.id FROM product_type pt
JOIN type_tree tt ON pt.parent_id = tt.id
)
SELECT p.*FROM product p
WHERE p.product_type_id IN (SELECT id FROM type_tree)
AND p.status ='Active';
-- Get type hierarchy pathWITHRECURSIVE type_path AS (
SELECT id, name, parent_id, 1as level
FROM product_type WHERE id = :typeId
UNIONALLSELECT pt.id, pt.name, pt.parent_id, tp.level +1FROM product_type pt
JOIN type_path tp ON pt.id = tp.parent_id
)
SELECT name FROM type_path ORDERBY level DESC;
-- Find available instances with earliest expiry (FEFO)SELECT pi.*FROM product_instance pi
WHERE pi.product_id = :productId
AND pi.status ='Available'AND (pi.expiry_date ISNULLOR pi.expiry_date >CURRENT_DATE)
ORDERBY pi.expiry_date NULLS LAST
LIMIT :quantity;
Zagadnienia projektowe
EAV vs JSON vs szerokie tabele
Podejście
Zalety
Wady
EAV
Elastyczne, rzadkie atrybuty
Złożone zapytania, brak bezpieczeństwa typów
Kolumna JSON
Elastyczne, jedna kolumna
Ograniczone indeksowanie, walidacja
Szerokie tabele
Szybkie, typowane, indeksowalne
Zmiany schematu, wiele NULL-i
Zalecenie:
EAV dla naprawdę dynamicznych atrybutów
JSON dla danych semi-strukturalnych
Dedykowane kolumny dla często wyszukiwanych/filtrowanych atrybutów
Kiedy stosować Product Instance
Scenariusz
Użyć Product Instance?
Dlaczego
Serializowana elektronika
Tak
Śledzenie poszczególnych sztuk
Towary łatwo psujące się
Tak
Śledzenie dat ważności
Towary masowe (śruby)
Nie
Wystarczy śledzić ilość
Product cyfrowe
Nie
Brak fizycznego egzemplarza
Produkcja na Order
Tak
Śledzenie poszczególnych sztuk
Strategie cenowe
Archetyp Product obsługuje różne modele cenowe:
Cena bazowa — W encji Product
Ceny według poziomu klienta — Obliczane w czasie rzeczywistym