Ответ
Процесс проектирования БД для интернет-магазина:
- Анализ требований: Выделение сущностей (Пользователь, Товар, Заказ, Категория) и их атрибутов.
- Концептуальное моделирование: Диаграмма сущность-связь (ERD).
- Нормализация: Приведение к 3NF для устранения аномалий вставки, обновления, удаления.
- Определение типов данных, ключей и индексов.
Логическая схема (основные таблицы):
-- 1. Пользователи
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. Категории товаров (иерархия может быть реализована через parent_id)
CREATE TABLE categories (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
parent_id BIGINT REFERENCES categories(id) ON DELETE SET NULL
);
-- 3. Товары
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
category_id BIGINT NOT NULL REFERENCES categories(id) ON DELETE RESTRICT,
stock_quantity INTEGER NOT NULL DEFAULT 0 CHECK (stock_quantity >= 0),
is_active BOOLEAN DEFAULT true
);
-- 4. Заказы (транзакционная сущность)
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'NEW', -- NEW, PROCESSING, SHIPPED, DELIVERED, CANCELLED
total_amount DECIMAL(10, 2) NOT NULL,
shipping_address JSONB, -- Гибкая структура для адреса
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. Позиции заказа (связь многие-ко-многим с атрибутами)
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10, 2) NOT NULL, -- Цена на момент заказа (историческая)
UNIQUE(order_id, product_id) -- Уникальность товара в заказе
);
Отображение в Java (JPA Entity):
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount;
@Type(JsonType.class) // Использование Hibernate Type для JSONB (PostgreSQL)
@Column(columnDefinition = "jsonb")
private Address shippingAddress;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
// Метод для расчета итога
public void calculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Ключевые решения:
- Нормализация: Отдельные таблицы для категорий и позиций заказа.
- Целостность: Использование
FOREIGN KEYиON DELETEправил. - Производительность: Индексы на
user_id,status,created_atв таблицеorders. - Гибкость: Поле
JSONBдля адреса,ENUM/VARCHARдля статуса. - Историчность: Сохранение
unit_priceвorder_itemsнезависимо от изменений вproducts.price.
Ответ 18+ 🔞
Да ты посмотри, какая красота, блядь! Сидишь себе, пыхтишь, проектируешь базу для интернет-магазина, а тут, на тебе, целая философия вырисовывается, ёпта! Как будто не таблицы пишешь, а судьбы решаешь, в рот меня чих-пых!
Ну ладно, поехали разбирать эту песочницу для данных.
Шаг первый: анализ требований. Это как прийти в бар и понять, кто тут кто. Кто у нас главный? Пользователь, конечно, этот вечно недовольный царь и бог. Что он хочет? Товар. А товар лежит в какой-то Категории, чтобы не валялся как попало. И всё это великолепие в итоге превращается в Заказ — священный акт купли-продажи, после которого всем обычно хочется выпить. Сущности выделили, молодцы, нехуй делать.
Шаг второй: концептуальная модель. Рисуем эти кружочки-квадратики, стрелочки между ними. Пользователь делает Заказ. Заказ содержит Товары. Товар лежит в Категории. Выглядит как схема преступной группировки, но это нормально. Главное — связи увидеть, а то потом окажется, что заказ привязан к категории, а пользователь купил сам себя. Бывало, блядь.
Шаг третий: нормализация. Вот тут начинается магия, или, как я это называю, «вынос мозгов в три этапа». Цель — чтобы не было аномалий. Представь: обновляешь цену в одном месте, а она в десяти других осталась старая. Или удаляешь товар, а заказ, в котором он был, превращается в тыкву. Это пиздец. Приводим всё к 3NF, чтобы каждая неключевая колонка зависела от ключа, целиком, и прямо. Не дохуя ли? Дохуя, но надо. Иначе потом будешь плакать, как Герасим над Муму.
А теперь смотри, как это в коде выглядит, ядрёна вошь!
-- 1. Пользователи. Без них нихуя не начнётся.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- Главный по тарелкам, уникальный и вечный
email VARCHAR(255) UNIQUE NOT NULL, -- Почта, она же логин. UNIQUE, а то один мудак десять актов наделает
password_hash VARCHAR(255) NOT NULL, -- Пароль, но хэшированный, ёба! В открытом виде не храним, мы не идиоты.
full_name VARCHAR(255), -- Может быть, а может и нет. "Хуй с горы" тоже прокатит.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Когда этот страдалец к нам прибился
);
-- 3. Товары. То, за чем все пришли.
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL, -- Артикул, ёпта! Без него — бардак.
name VARCHAR(255) NOT NULL, -- Название. "Хуй в пальто" не принимается, только если это не бренд.
description TEXT, -- Описание. Может быть пустым, как совесть у маркетолога.
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0), -- Цена. Отрицательной не бывает, CHECK следит, чтобы не наебнули.
category_id BIGINT NOT NULL REFERENCES categories(id) ON DELETE RESTRICT, -- Ссылка на категорию. RESTRICT — не даём удалить категорию, если в ней товары.
stock_quantity INTEGER NOT NULL DEFAULT 0 CHECK (stock_quantity >= 0), -- Остаток. Ноль — значит, нихуя нет.
is_active BOOLEAN DEFAULT true -- А активен ли товар? Или его уже спиздили со склада?
);
-- 4. Заказы. Здесь вся соль и кровь.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Кто заказчик. CASCADE — удалили пользователя, хуй с ним, и заказы его в пизду.
status VARCHAR(20) NOT NULL DEFAULT 'NEW', -- Статус. 'NEW', 'PROCESSING'... 'CANCELLED' — самый популярный, блядь.
total_amount DECIMAL(10, 2) NOT NULL, -- Итоговая сумма. Рассчитывается, блять, не вручную!
shipping_address JSONB, -- Адрес доставки в JSONB. Гибко, ёпта! Можем хоть координаты тайной поляны передать.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Когда в последний раз статус меняли.
);
-- 5. Позиции заказа. Самое важное, блядь!
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, -- Какому заказу принадлежит.
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE RESTRICT, -- Какой товар. RESTRICT — не даём удалить товар, который уже в истории заказов.
quantity INTEGER NOT NULL CHECK (quantity > 0), -- Сколько штук. Хотя бы одну, пидарас шерстяной!
unit_price DECIMAL(10, 2) NOT NULL, -- Цена НА МОМЕНТ ЗАКАЗА! Это архиважно, ёба! Цена в products может поменяться, а здесь — нет, это история.
UNIQUE(order_id, product_id) -- Чтобы один и тот же товар в заказ дважды не воткнули. Уникальность, мать её!
);
А теперь, внимание, фокус! Как эта база оживает в Java. Смотри на этот класс Order, просто песня!
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Наш родной BIGSERIAL
@ManyToOne(fetch = FetchType.LAZY) // Ленивая загрузка, чтобы не тащить всего пользователя, если не надо
@JoinColumn(name = "user_id", nullable = false)
private User user; // Ссылка на того самого страдальца
@Enumerated(EnumType.STRING) // Статус храним как строку в БД, а в коде как enum. Красота!
private OrderStatus status;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount; // Тот самый total_amount
@Type(JsonType.class) // Магия Hibernate для работы с JSONB в PostgreSQL
@Column(columnDefinition = "jsonb")
private Address shippingAddress; // И адрес мапится на наш класс Address. Ни хуя себе!
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>(); // Вот они, позиции заказа! cascade=ALL — сохраняем/удаляем вместе с заказом.
// А это, блядь, коронный метод! Рассчитывает итог.
public void calculateTotal() {
this.totalAmount = items.stream() // Берём поток позиций
.map(OrderItem::getSubtotal) // Каждая позиция знает свою сумму (quantity * unit_price)
.reduce(BigDecimal.ZERO, BigDecimal::add); // Всё складываем. И вуаля!
}
}
Итоговые ключевые решения, которые спасут тебе жизнь:
- Нормализация: Категории — отдельно, позиции заказа — отдельно. Не пытайся всё впихнуть в одну таблицу, получится говно.
- Целостность:
FOREIGN KEY— это святое.ON DELETE CASCADEдля заказов пользователя иRESTRICTдля товаров в истории — это мудрость предков. - Производительность: Не забудь про индексы на
user_id,status,created_atв заказах. Иначе запросы будут тормозить, как пьяный мудак в час пик. - Гибкость:
JSONBдля адреса — это сила. Захотел добавить «код домофона» — добавил в классAddress, и всё, блядь. Никаких ALTER TABLE. - Историчность:
unit_priceвorder_items— это золотое правило. Цена товара меняется, а история покупок должна оставаться неприкосновенной. Иначе — суд, блядь, и все твои заказы как у Герасима, в озеро.
Вот и весь сказ, ёпта. Спроектировал, реализовал, запустил. Главное — не забывай, что за каждой строчкой в таблице orders стоит живой человек, который, возможно, прямо сейчас матерится, что его посылка опять застряла. А твоя задача — чтобы в базе данных у него всё было чисто и на своих местах. Удачи, друг!