Ваши данные имеют иерархию. Потом на разных уровнях появляются разные типы сущностей. И что теперь?
Представьте крупную корпорацию. Несколько штаб-квартир. Региональные офисы под каждой.
Отделы в каждом офисе. Склады при отделах. Розничные точки. Шоу-румы.
Товары на складах. И вот... товар переводят в штаб-квартиру для ревью.
Подождите — товар был на складе, но теперь он в... шоу-руме? В штаб-квартире?
А завтра он может оказаться в розничной точке в другом городе?
Как вы смоделируете ЭТО в базе данных?
Традиционный подход
-- Кошмар «Корпоративной иерархии»
--
-- Сначала таблицы для каждого уровня:
CREATE TABLE Headquarters (id, name, ...);
CREATE TABLE RegionalOffices (id, hq_id, name, ...);
CREATE TABLE Departments (id, office_id, name, ...);
CREATE TABLE Warehouses (id, department_id, ...);
CREATE TABLE RetailPoints (id, department_id, ...);
CREATE TABLE Showrooms (id, office_id, ...);
CREATE TABLE Products (id, name, sku, ...);
-- А теперь самое интересное: ГДЕ находится товар?
-- Вариант A: Несколько FK-колонок
ALTER TABLE Products ADD warehouse_id INT NULL;
ALTER TABLE Products ADD retail_point_id INT NULL;
ALTER TABLE Products ADD showroom_id INT NULL;
ALTER TABLE Products ADD hq_showroom_id INT NULL;
-- Только ОДНА должна быть не NULL. Удачи с контролем.
-- Вариант B: Полиморфная ассоциация (подход «Rails»)
ALTER TABLE Products ADD location_type VARCHAR(50);
ALTER TABLE Products ADD location_id INT;
-- FK-ограничение невозможно. Осиротевшие записи гарантированы.
-- Вариант C: Связующие таблицы для каждой комбинации
CREATE TABLE Product_Warehouse (product_id, warehouse_id);
CREATE TABLE Product_RetailPoint (product_id, point_id);
CREATE TABLE Product_Showroom (product_id, showroom_id);
-- Теперь отслеживаем историю перемещений...
CREATE TABLE ProductMovementHistory (...);
-- Запрос: «Все товары под London HQ (любой уровень)»
-- 250 строк рекурсивных CTE по 7 таблицам
-- Производительность: 3-5 секунд на 100K товаров
-- Переместить товар в другое место?
-- BEGIN TRANSACTION;
-- DELETE FROM Product_Warehouse WHERE...
-- INSERT INTO Product_Showroom VALUES...
-- INSERT INTO ProductMovementHistory...
-- COMMIT;
-- Надеемся, что порядок правильный!
RedBase
// Определите сущности — просто C#-классы с атрибутом
[RedbScheme] public class Headquarters { ... }
[RedbScheme] public class Office { ... }
[RedbScheme] public class Department { ... }
[RedbScheme] public class Warehouse { ... }
[RedbScheme] public class Showroom { ... }
[RedbScheme] public class Product { ... }
// Создайте иерархию — любая сущность под любым родителем
await redb.CreateChildAsync(product, warehouse);
// Товар теперь на складе. Готово.
// Переместить товар в шоу-рум HQ? Одна строка:
await redb.MoveObjectAsync(product, hqShowroom);
// История отслеживается автоматически. Готово.
// Найти ВСЕ товары под London HQ (любая глубина)
var products = await redb
.TreeQuery<Product>(londonHQ.Id, maxDepth: 10)
.Where(p => p.InStock)
.ToListAsync();
// Все товары со всех складов, шоу-румов,
// розничных точек под London. 15мс.
// Найти все отделы на уровне 3
var level3 = await redb
.TreeQuery<Department>(hq.Id)
.WhereLevel(3)
.ToListAsync();
// Построить полное дерево со всеми дочерними элементами
var tree = await redb
.LoadTreeAsync<Headquarters>(hq, maxDepth: 5);
// Навигация: tree.Children[0].Children[1].Props
Секрет: В RedBase иерархия встроена. У каждого объекта есть родитель.
Разные типы сущностей могут располагаться на любом уровне. Товар сегодня может быть на складе,
а завтра в шоу-руме — тот же API, та же таблица, ноль изменений схемы.
Но подождите, дальше ещё хуже...
MacBook стоит на полке склада. Перед отправкой в HQ на ревью директора
кто-то кладёт в коробку набор аксессуаров. Кабель, чехол, может AirPods — для шефа.
Теперь коробка едет в HQ. В HQ могут достать аксессуар и оставить его там.
Или весь комплект отправится в розницу — MacBook вместе с аксессуаром.
В традиционной БД: как смоделировать «аксессуар внутри коробки внутри склада»?
Потом переместить коробку? Потом опционально отцепить аксессуар?
Чую баги. Много багов.
Традиционный подход
-- Аксессуар внутри коробки? Ещё связующие таблицы!
CREATE TABLE ProductContents (
container_id INT,
content_id INT,
added_date DATETIME,
-- Стоп, а если содержимое имеет своё содержимое?
-- Начинается рекурсивный кошмар...
);
-- Переместить коробку в HQ
UPDATE Products SET location_id = @hqId
WHERE id = @boxId;
-- ОЙ! Аксессуар всё ещё ссылается на склад!
-- Нужен ручной фикс:
UPDATE Products SET location_id = @hqId
WHERE id IN (SELECT content_id FROM ProductContents
WHERE container_id = @boxId);
-- А вложенное содержимое? Ещё рекурсия...
-- Отсоединить аксессуар в HQ?
DELETE FROM ProductContents
WHERE container_id = @boxId AND content_id = @accId;
UPDATE Products SET location_id = @hqId
WHERE id = @accId;
-- История? Аудит? Удачи.
RedBase
// Положить аксессуар внутрь коробки MacBook
await redb.CreateChildAsync(accessory, macbookBox);
// Готово. Аксессуар теперь ВНУТРИ коробки.
// Переместить всю коробку в HQ
await redb.MoveObjectAsync(macbookBox, hqShowroom);
// Аксессуар едет ВМЕСТЕ с коробкой. Автоматически.
// Никакого доп. кода. Никаких забытых обновлений.
// Достать аксессуар, оставить в HQ
await redb.MoveObjectAsync(accessory, hqShowroom);
// Аксессуар теперь напрямую под HQ.
// Коробка MacBook может ехать в розницу одна.
// Запрос: что внутри контейнера?
var contents = await redb
.TreeQuery<Product>(macbookBox.Id)
.ToListAsync();
// Полный аудит: где был аксессуар?
var path = await redb
.GetPathToRootAsync<Product>(accessory);
// [Аксессуар] → [Коробка] → [Шоу-рум] → [HQ]
Суть: Связи «родитель-потомок» работают автоматически.
Перемещаете родителя — потомки следуют за ним. Отцепляете потомка — он остаётся там, где вы его положили.
Запросы на любом уровне. Никаких сирот. Никаких забытых обновлений. Никаких багов в 2 часа ночи.
Растите дальше. Вкладывайте глубже. Всё так же пара строк кода.
Завтра бизнес скажет: «Нужно отслеживать, какой сотрудник упаковал каждую коробку.»
Добавьте свойство. Готово.
Через неделю: «Коробки могут содержать другие коробки.»
Уже работает. Любой объект может быть родителем.
Через месяц: «Нам нужны транспортные контейнеры, содержащие несколько коробок с разных складов,
едущие в разные пункты назначения, с прикреплёнными таможенными документами.»
CreateChildAsync. MoveObjectAsync. TreeQuery. Тот же API. Ноль миграций.
Ваши конкуренты всё ещё пишут скрипты ALTER TABLE. А вы выпускаете фичи.
Но подождите... у объектов есть не только родители. Они ссылаются на ДРУГИЕ объекты.
Order ссылается на Customer. И на несколько Products. И на адрес доставки.
И на историю платежей. И на купоны со скидками. И на менеджера по продажам, совершившего сделку.
А ещё у Customer свой Address. И список предыдущих Orders (циклическая ссылка!).
А у Products есть Categories. И Suppliers. И Reviews от других Customers.
Теперь сохраните всё это. В правильном порядке. С правильными FK.
Без сирот. Без дедлоков. Добро пожаловать в Ад Графов.
Традиционный подход
-- Order ссылается на Customer, Products, Payments...
CREATE TABLE Orders (
id INT PRIMARY KEY,
customer_id INT REFERENCES Customers(id),
shipping_address_id INT REFERENCES Addresses(id),
sales_manager_id INT REFERENCES Employees(id),
discount_id INT NULL REFERENCES Discounts(id),
-- Стоп, а как насчёт нескольких товаров?
);
-- Связующая таблица для Order-Product (many-to-many)
CREATE TABLE OrderProducts (
order_id INT REFERENCES Orders(id),
product_id INT REFERENCES Products(id),
quantity INT,
price_at_order DECIMAL,
PRIMARY KEY (order_id, product_id)
);
-- Ещё одна связующая для истории платежей
CREATE TABLE OrderPayments (
order_id INT REFERENCES Orders(id),
payment_id INT REFERENCES Payments(id),
-- ...
);
-- Сохранить заказ? Вот кошмар:
BEGIN TRANSACTION;
-- 1. Убедиться что Customer существует (или создать)
-- 2. Убедиться что Address существует (или создать)
-- 3. INSERT Order (сначала нужен customer_id!)
-- 4. INSERT OrderProducts для каждого товара
-- 5. INSERT Payments
-- 6. INSERT OrderPayments-связи
-- 7. UPDATE Order с итогами оплаты
-- Надеемся, что ничего не упало между шагами!
COMMIT;
-- Удалить заказ? Ужас CASCADE ждёт.
-- Загрузить заказ со всеми связями? 15 JOIN-ов.
RedBase
// Просто опишите бизнес-модель
[RedbScheme]
public class OrderProps
{
public Customer Customer { get; set; }
public Address ShippingAddress { get; set; }
public Employee SalesManager { get; set; }
// Массив объектов? Просто объявите.
public Product[] Products { get; set; }
// Массив RedbObjects со своими Props!
public RedbObject<PaymentProps>[] Payments { get; set; }
// Словарь объектов? Почему нет!
public Dictionary<string, RedbObject<CouponProps>>
Coupons { get; set; }
}
// Сохранить весь граф объектов
var order = new RedbObject<OrderProps>
{
Props = new OrderProps
{
Customer = customer,
Products = new[] { laptop, mouse, keyboard },
Payments = new[] { payment1, payment2 },
Coupons = new Dictionary<string, ...>
{
["SUMMER20"] = summerDiscount
}
}
};
await redb.SaveAsync(order);
// ВСЕ вложенные объекты сохранены. Все связи созданы.
// Одна строка. Одна транзакция. Готово.
// Загрузить с полным графом
var loaded = await redb.LoadAsync<OrderProps>(id);
// loaded.Props.Customer — готово
// loaded.Props.Products[0] — готово
// loaded.Props.Payments[1].Props — готово
// loaded.Props.Coupons["SUMMER20"] — готово
Графы объектов — полноценные граждане первого класса.
Объявляйте ссылки, массивы, словари объектов в Props.
Сохраните один раз — весь граф персистируется. Загрузите один раз — весь граф материализуется.
Никаких связующих таблиц. Никакого кошмара FK. Никаких сирот. Никогда.
Customer Customer
Ссылка на один объект
Product[] Products
Массив бизнес-классов
RedbObject<T>[] Items
Массив полных объектов с ID
Dictionary<K, T>
Коллекции чего угодно по ключу