Введение: когда документация по БД перестаёт быть мёртвым грузом
Представьте: вы заходите в репозиторий, открываете папку schemas и через пять минут понимаете, как устроена база во всём проекте, со всеми связями. Никаких устаревших диаграмм в Confluence, никаких гаданий по коду миграций. Схема базы данных становится частью кодовой базы — её можно версионировать, ревьюить, тестировать. Модель в формате ArchDB становится единым источником истины, из которого автоматически генерируются документация, DDL-скрипты и даже ORM-сущности. Звучит как мечта? Для нас с командой это стало реальностью, когда мы перешли на ArchDB.
А теперь вспомните свои «боли»:
-
Вы тратите два часа на code review, потому что миграцию в Liquibase написал стажёр и невозможно понять, что он хотел сделать.
-
Синхронизация ORM-сущностей (JPA/Hibernate) с реальной схемой превращается в ад, если у вас больше пятидесяти таблиц.
-
Каждое изменение модели запускает рутину: обновить диаграмму, поправить документацию, переписать DDL, не забыть про тестовые данные.
-
Одинаковые колонки
created_at,updated_byкочуют из таблицы в таблицу через copy-paste, а когда приходит задача добавить ещё одно поле аудита, приходится лезть во все сущности.
Конечно, сегодня существуют инструменты, которые автоматизируют создание миграций или позволяют рисовать схемы в веб-интерфейсе. Но они решают лишь часть проблем. ArchDB предлагает принципиально иной подход: модульное, декларативное описание схемы как кода. Это не просто DSL, а своего рода рецепт, превращающий хаотичный процесс проектирования данных в управляемый и автоматизированный поток. Вы описываете «что», а не «как» — декларативность фиксирует желаемое состояние («замороженное» состояние), из которого инструменты могут сгенерировать всё необходимое: от DDL-скриптов до визуальных диаграмм.
Но главное — ArchDB позволяет кодифицировать лучшие практики и типовые требования. Вместо того чтобы раз за разом описывать в документации «во всех таблицах должны быть поля created_at и updated_at», вы создаёте шаблон Auditable, который затем просто подключаете к нужным таблицам. Этот шаблон живёт в общей библиотеке и автоматически распространяется на все команды проекта или компании. Требования перестают быть просто словами — они становятся исполняемым кодом.
Фактически ArchDB меняет парадигму: от ручного сопровождения — к автоматизации на всех этапах, от концептуальной модели до промышленной схемы.
Эта статья для тех, кто устал от рутины: Team Lead’ов, аналитиков, архитекторов данных и senior‑разработчиков, сотрудников сопровождения, которым надоело наступать на одни и те же грабли. Мы разберём, как ArchDB позволяет:
-
реально ускорить проектирование — на практике время согласования изменений в схеме из сотни таблиц сокращается с дней до нескольких часов;
-
исключить дублирование через механизм шаблонов (Template);
-
декомпозировать монолитную модель на логические модули (Package и Schema), которые можно разрабатывать и версионировать независимо.
Дальше — никакой абстрактной философии. Только код, примеры и конкретные приёмы, которые вы сможете применить уже сегодня.
Взгляд за горизонт
Бывает, что хочется заглянуть в конец книги и посмотреть, чем всё это кончится. Этот раздел именно на такой случай.
Core.adb
// ==============================================
// Файл: Core.adb
// Полная модель интернет-магазина на ArchDB
// ==============================================
Package com.example.ecommerce.Core
// ----- Проект -----
Project ECommercePlatform [
database: "postgresql",
version: 2.5.0,
author: "E-Commerce Team",
description: "Производственная модель данных интернет-магазина",
default_schema: "public"
]
// ----- Глобальные метаданные файла -----
Metadata [
author: "Архитектурная команда",
version: 3.4.11,
description: "Шаблоны и основные бизнес-сущности",
license: "Internal use only"
]
export Core as "com.example.ecommerce.Core" version 3.4.0
// ----- Схема основных данных -----
// ----- Базовые шаблоны для повторного использования -----
Template IdentifiableUuid
[note: "Шаблон для сущностей с UUID идентификатором"] {
id uuid [pk, default: `gen_random_uuid()`, note: "Уникальный идентификатор"]
}
Template Auditable
[note: "Шаблон с полями аудита (кто и когда создал/обновил)"] {
created_at timestamp [not_null, default: `now()`]
created_by uuid [note: "ID пользователя-создателя", ref: > User.id, nullable]
updated_at timestamp [default: `now()`]
updated_by uuid [note: "ID пользователя, обновившего запись", ref: > User.id, nullable]
indexes {
idx_created_at (created_at) [note: "Для фильтрации по дате создания"]
idx_updated_at (updated_at) [note: "Для поиска недавно измененных"]
}
}
Template SoftDeletable
[note: "Шаблон для мягкого удаления"] {
is_deleted boolean [default: false, note: "Флаг удаления"]
deleted_at timestamp [note: "Когда удалено"]
deleted_by uuid [note: "Кто удалил", ref: > User.id]
}
Template BusinessEntity extends IdentifiableUuid, Auditable, SoftDeletable
[note: "Составной базовый шаблон для бизнес-сущностей"] {
version_no integer [default: 1, note: "Версия для оптимистичной блокировки"]
status varchar(50) [default: 'active', note: "Статус активности"]
}
Table User extends BusinessEntity
[note: "Пользовательская система"] {
email varchar(255) [unique, not_null, note: "Основной email"]
username varchar(100) [unique, note: "Публичное имя"]
password_hash text [not_null]
phone varchar(50)
date_of_birth date
indexes {
idx_user_email (email)
idx_user_phone (phone) [unique, name: "uidx_user_phone"]
}
checks {
`char_length(password_hash) >= 8` [name: "ck_password_length"]
}
}
// ----- Логическая группировка для документации -----
Group Core
[note: "Шаблоны и основные бизнес-сущности", color: #e8f5e8] {
Core.IdentifiableUuid
Core.Auditable
Core.SoftDeletable
Core.BusinessEntity
Core.User
}Product.adb
// ==============================================
// Файл: Product.adb
// Полная модель интернет-магазина на ArchDB
// ==============================================
Package com.example.ecommerce.Product
// ----- Проект -----
Project ECommercePlatform [
database: "postgresql",
version: 2.5.0,
author: "E-Commerce Team",
description: "Производственная модель данных интернет-магазина",
default_schema: "public"
]
// ----- Глобальные метаданные файла -----
Metadata [
author: "Архитектурная команда",
version: 3.6.3,
description: "Продукты и категории",
license: "Internal use only"
]
import "com.example.ecommerce.Core.adb" version 3.4.0
export Product as "com.example.ecommerce.Product" version 3.6.0
// ----- Схема продуктов и категорий -----
Table Product extends Core.BusinessEntity
[note: "Каталог товаров"] {
sku varchar(50) [unique, not_null, note: "Артикул"]
name varchar(500) [not_null]
description text
price decimal(12, 2) [not_null]
cost_price decimal(12, 2) [note: "Себестоимость"]
weight_kg decimal(8, 3)
is_available boolean [default: true]
stock_quantity integer [not_null, default: 0]
reserved_quantity integer [not_null, default: 0, note: "Зарезервировано в незавершенных заказах"]
available_quantity integer [generated: `stock_quantity - reserved_quantity`]
indexes {
idx_product_sku (sku)
idx_product_name (name)
(name, is_available) [name: "idx_product_search"]
}
checks {
`price >= 0` [name: "ck_positive_price"]
`cost_price >= 0` [name: "ck_positive_cost"]
`price >= cost_price` [name: "ck_profit_margin"]
`reserved_quantity <= stock_quantity` [name: "ck_valid_reservation"]
`reserved_quantity >= 0` [name: "ck_nonnegative_reserved"]
}
}
Table Category extends Core.IdentifiableUuid
[note: "Категория товаров"] {
parent_id uuid [note: "Для иерархии", ref: > Category.id, on_delete: cascade]
name varchar(500) [not_null]
slug varchar(500) [unique, note: "Для URL"]
sort_order integer [default: 0]
}
// Связь многие-ко-многим для категорий товаров (через Relation)
Relation ProductCategory
[note: "Связь товаров с категориями"] {
Product.id <-> Category.id
}
// ----- Логическая группировка для документации -----
Group Product
[note: "Продукты и категории", color: #e8f5e8] {
Product.Product
Product.Category
}Order.adb
// ==============================================
// Файл: Order.adb
// Полная модель интернет-магазина на ArchDB
// ==============================================
Package com.example.ecommerce.Order
// ----- Проект -----
Project ECommercePlatform [
database: "postgresql",
version: 2.5.0,
author: "E-Commerce Team",
description: "Производственная модель данных интернет-магазина",
default_schema: "public"
]
// ----- Глобальные метаданные файла -----
Metadata [
author: "Архитектурная команда",
version: 3.7.8,
description: "Заказы и позиции заказов",
license: "Internal use only"
]
import "com.example.ecommerce.Core.adb" version 3.4.0
import "com.example.ecommerce.Product.adb" version 3.6.0
export Order as "com.example.ecommerce.Order" version 3.7.0
// ----- Схема заказов -----
Enum AddressType
[note: "Тип адреса"] {
shipping
billing
both
}
Table UserAddress extends Core.BusinessEntity
[note: "Таблица адресов"] {
user_id uuid [not_null, ref: > Core.User.id, on_delete: cascade]
address_type AddressType [not_null]
full_name varchar(255) [not_null]
street text [not_null]
city varchar(100) [not_null]
postal_code varchar(20) [not_null]
country_code char(2) [not_null]
}
Enum OrderStatus
[note: "Статусы заказа", color: #e3f2fd] {
draft [color: #bbdefb, note: "Черновик"]
pending [color: #90caf9, note: "Ожидает оплаты"]
paid [color: #64b5f6, note: "Оплачен"]
processing [color: #42a5f5, note: "В обработке"]
shipped [color: #2196f3, note: "Отправлен"]
delivered [color: #1e88e5, note: "Доставлен"]
cancelled [color: #ffcdd2, note: "Отменен"]
refunded [color: #ef9a9a, note: "Возвращен"]
}
Table Order extends Core.BusinessEntity
[note: "Заказы"] {
user_id uuid [not_null]
order_number varchar(50) [unique, not_null]
total_amount decimal(12, 2) [not_null]
tax_amount decimal(12, 2) [default: 0]
discount_amount decimal(12, 2) [default: 0]
status OrderStatus [not_null, default: 'pending']
notes text
shipping_address_id uuid [ref: > UserAddress.id]
billing_address_id uuid [ref: > UserAddress.id]
shipping_cost decimal(12,2) [default: 0]
estimated_delivery_date date
indexes {
idx_order_user (user_id, created_at desc)
idx_order_number (order_number)
idx_order_status (status, created_at)
}
checks {
`total_amount >= 0` [name: "ck_positive_total"]
`tax_amount >= 0` [name: "ck_positive_tax"]
}
}
Table OrderItem extends Core.IdentifiableUuid
[note: "Позиции в заказе"] {
order_id uuid [not_null]
product_id uuid [not_null]
quantity integer [not_null]
unit_price decimal(12, 2) [not_null]
subtotal decimal(12, 2) [not_null, generated: `quantity * unit_price`]
indexes {
idx_order_item_order (order_id)
idx_order_item_product (product_id)
(order_id, product_id) [unique, name: "uidx_order_product"]
}
checks {
`quantity > 0` [name: "ck_positive_quantity"]
`unit_price >= 0` [name: "ck_positive_unit_price"]
}
}
Ref: Order.user_id -> Core.User.id [on_delete: restrict, note: "Заказ принадлежит пользователю"]
Ref: OrderItem.order_id -> Order.id [on_delete: cascade, note: "Позиция принадлежит заказу"]
Ref: OrderItem.product_id -> Product.Product.id [on_delete: restrict, note: "Позиция ссылается на товар"]
// ----- Логическая группировка для документации -----
Group Order
[note: "Заказы", color: #e8f5e8] {
Order.AddressType
Order.UserAddress
Order.OrderStatus
Order.Order
Order.OrderItem
}Finance.adb
// ==============================================
// Файл: Finance.adb
// Полная модель интернет-магазина на ArchDB
// ==============================================
Package com.example.ecommerce.Finance
// ----- Проект -----
Project ECommercePlatform [
database: "postgresql",
version: 2.5.0,
author: "E-Commerce Team",
description: "Производственная модель данных интернет-магазина",
default_schema: "public"
]
// ----- Глобальные метаданные файла -----
Metadata [
author: "Архитектурная команда",
version: 3.3.17,
description: "Финансовые операции и платежи",
license: "Internal use only"
]
import "com.example.ecommerce.Core.adb" version 3.4.0
import "com.example.ecommerce.Order.adb" version 3.7.0
export Finance as "com.example.ecommerce.Finance" version 3.3.0
// ----- Схема финансов -----
Enum PaymentType
[note: "Способ оплаты"] {
credit_card
debit_card
paypal
bank_transfer
cash_on_delivery
}
Table Payment extends Core.BusinessEntity
[note: "Платеж"] {
order_id uuid [not_null, ref: > Order.Order.id, on_delete: restrict, note: "Платеж для заказа"]
amount decimal(12, 2) [not_null]
payment_type PaymentType [not_null]
transaction_id varchar(100) [unique, note: "ID транзакции в платежной системе"]
is_successful boolean [default: false]
processed_at timestamp
gateway_response jsonb [note: "Ответ платежного шлюза для аудита"]
refunded_amount decimal(12,2) [default: 0]
indexes {
idx_payment_order (order_id)
idx_payment_transaction (transaction_id)
(payment_type, is_successful) [name: "idx_payment_stats"]
(transaction_id) [unique, distinct]
}
checks {
`amount > 0` [name: "ck_positive_payment"]
}
}
Table Invoice extends Core.IdentifiableUuid
[note: "Счет-фактура"] {
order_id uuid [not_null, unique, ref: > Order.Order.id, on_delete: restrict, note: "Счет для заказа"]
invoice_number varchar(50) [unique, not_null]
issued_date date [not_null, default: `current_date`]
due_date date [not_null]
total_amount decimal(12, 2) [not_null]
indexes {
idx_invoice_order (order_id)
idx_invoice_number (invoice_number)
(due_date, issued_date) [name: "idx_invoice_dates"]
}
checks {
`due_date >= issued_date` [name: "ck_valid_due_date"]
}
}
// ----- Логическая группировка для документации -----
Group Finance
[note: "Финансовые операции и платежи", color: #e8f5e8] {
Finance.PaymentType
Finance.Payment
Finance.Invoice
}
Диаграмма модели данных
Вот так эта модель данных будет выглядеть на диаграмме.

Знакомство с ArchDB: от формальной основы до промышленной модели
При разработке ArchDB я взял за основу DBML — лаконичный язык описания схем, знакомый многим по сервису dbdiagram.io. Его синтаксис прост и интуитивен, и я сохранил этот «дух простоты» везде, где это возможно. Но для решения задач, с которыми сталкиваются крупные проекты, DBML оказалось недостаточно. Поэтому, отталкиваясь от прототипа, я сделал несколько ключевых шагов вперёд. Причём нулевой шаг — фундаментальный — был сделан ещё до написания первой строки кода.
Шаг нулевой: строгая основа
Разработка ArchDB началась не с реализации парсера, а с создания формального описания грамматики в нотации BNF. Этого DBML полностью лишён, и именно это закладывает основу для всего остального. Благодаря строгой грамматике мы получаем:
-
однозначный парсинг моделей;
-
возможность автоматической валидации;
-
фундамент для инструментов: генераторов DDL, визуализаторов, плагинов для IDE;
-
чёткую семантику каждой конструкции, исключающую разночтения.
Формальная спецификация — это своего рода «конституция» языка, которая гарантирует, что все будущие инструменты будут говорить на одном языке.
Шаг первый: от процедурности к декларативности
В DBML вы часто описываете, как должна выглядеть таблица, но не можете чётко отделить намерение от реализации. ArchDB предлагает pure-декларативный подход: вы фиксируете желаемое состояние модели (то самое «замороженное» состояние, о котором мы говорили во введении), а всё остальное — генерация DDL, миграций, документации — ложится на инструменты. Это не просто удобно — это основа парадигмы Data as Code.
Шаг второй: от монолита к модульности
DBML поощряет хранение всей модели в одном файле. Пока таблиц двадцать — это терпимо. Когда их становится двести, а над разными частями модели работают разные команды, монолит превращается в тормоз. ArchDB вводит полноценную модульную систему: вы можете декомпозировать модель на логические схемы (Schema), выносить общие части в библиотеки, импортировать их с контролем версий и даже публиковать как внутренние пакеты. Фактически вы получаете возможность управлять схемой БД теми же инструментами, что и обычным кодом.
Шаг третий: от копипасты к повторному использованию
Это, пожалуй, самое важное нововведение. В DBML есть конструкция TablePartial, но она крайне ограничена и допускает лишь один уровень включения. В ArchDB я ввел полноценные шаблоны (Template) — абстрактные сущности, которые могут содержать колонки, индексы, проверки и наследовать от других шаблонов (и даже от таблиц). Причём поддерживается множественное наследование: таблица или шаблон могут расширять сразу несколько предков, комбинируя их функциональность.
Взгляните на разницу:
TablePartial в DBML (ограничен)
TablePartial audit_fields {
created_at datetime
updated_at datetime
}
Table users {
id integer
...
// Как подключить audit_fields? Через include? Это неочевидно.
}
Template в ArchDB (полноценное наследование)
Template Auditable {
created_at timestamp [not_null, default: `now()`]
updated_at timestamp [default: `now()`]
}
Template Trackable {
updated_by integer [ref: > User.id]
}
// Множественное наследование
Table User extends Auditable, Trackable {
id integer [pk, increment]
email varchar [unique]
}
В результате таблица User автоматически получает все поля из обоих шаблонов. А если потребуется добавить во все аудируемые таблицы ещё одно поле — достаточно изменить один шаблон, и изменения применятся везде. Именно так лучшие практики перестают быть просто строчками в документе «Стандарты разработки» и становятся исполняемым кодом, который автоматически распространяется на все команды.
Шаг четвёртый: от жёстких конструкций к гибким свойствам В DBML атрибуты (например, primary key, unique) жёстко привязаны к колонкам. ArchDB делает следующий шаг: свойства (properties) могут быть добавлены к любому элементу языка — таблице, колонке, связи, индексу, перечислению. Свойства бывают двух видов: флаги (pk, unique, not_null) и пары «ключ‑значение» (default: 0, note: "комментарий", color: #ff0000).
Что это даёт?
-
Расширяемость: вы можете вводить собственные свойства, которые будут нести семантику для ваших генераторов кода или инструментов визуализации.
-
Документирование: свойство
noteпозволяет оставлять комментарии прямо в модели, делая её самодокументируемой. -
Управление генерацией: через свойства можно управлять поведением генераторов (например,
name: "order_id"задаёт физическое имя колонки в БД). -
Вложенность: свойства могут быть вложенными, что позволяет создавать сложные конфигурации (например,
java: [type: "Long", annotation: "@Id"]привязывает колонку модели ORM в java-коде).
Пример:
Table Order [note: "Основная таблица заказов", color: #e3f2fd] {
id integer [pk, increment, name: "order_id", java: [type: "Long", annotation: "@Id"]]
status varchar [default: 'new', note: "Статус заказа"]
}
Подведём итог. ArchDB — это не просто очередной DSL. Это язык, построенный на формальном фундаменте, предлагающий декларативность, модульность, мощное повторное использование через множественное наследование и гибкую систему свойств. Всё это делает его пригодным для проектов индустриального масштаба, где важны стандартизация, контролируемость и возможность автоматизации.
Кстати, о миграции. Если у вас уже есть модели в DBML, переход на ArchDB не потребует переписывания всего с нуля. Синтаксис описания таблиц, колонок и связей (Ref) максимально близок. Вы сможете конвертировать существующие .dbml-файлы с минимальными правками и сразу получить доступ ко всем описанным возможностям. Позже я покажу это на конкретном примере.
А теперь — к практике. Следующий раздел займёт у вас не больше минуты.
Быстрый старт: минимальная схема за 60 секунд
Теория — это хорошо, но ArchDB создан для действия. Давайте за 60 секунд напишем работающую модель интернет-магазина. Никакой магии — только код.
Минимальная схема: покупатели и заказы
Создайте файл shop.adb и поместите в него следующее:
shop.adb — первая модель на ArchDB
// Покупатель
Table Customer {
id integer [pk, increment]
email varchar(255) [unique, not_null]
name varchar(100)
}
// Заказ
Table Order {
id integer [pk, increment]
customer_id integer
order_date timestamp [not_null, default: `now()`]
total decimal(10, 2)
discount decimal(10, 2) [default: 0, check: `discount >= 0 AND discount <= total`] (1)
}
// Внешний ключ
Ref: Order.customer_id -> Customer.id [on_delete: restrict]
-
Нестандартное требование: скидка не может быть отрицательной и не может превышать итоговую сумму заказа. ArchDB позволяет выразить это прямо в модели.
Что получилось:

-
Две таблицы с первичными ключами и автоинкрементом.
-
Уникальный и обязательный email покупателя.
-
Дата заказа по умолчанию — текущий момент.
-
Поле
discountсо сложной проверкой (скидка не больше суммы заказа). -
Связь «один-ко-многим» с правилом
ON DELETE RESTRICT(не даст удалить покупателя, если у него есть заказы).
Уже на этом примере видно, как ArchDB сочетает простоту базового синтаксиса с возможностью выразить бизнес-правила (проверка check) прямо в схеме.
Ключевые концепции за 5 минут
Разберём элементы, из которых строится любая модель.
Колонка (Column)
Колонка — это атомарная единица данных. Её определение в ArchDB строится по простому шаблону, который имеет строгую формальную основу:
column_def ::= column_name type_name properties?
То есть: имя_колонки тип_данных необязательные_свойства.
На практике это выглядит так:
title varchar(255) [not_null, default: ''] (1)
created_at timestamp [default: `now()`] (2)
is_active boolean [default: false] (3)
price decimal(10,2)[check: `price >= 0`, note: "Цена должна быть неотрицательной"] (4)
order_id integer [not_null, ref: -> Order.id, on_delete: cascade] (5)
-
Строка с ограничением длины, обязательная, значение по умолчанию — пустая строка.
-
Дата создания, по умолчанию — текущее время.
-
Логический флаг, по умолчанию
false. -
Числовое поле с проверкой и комментарием (свойство
note). -
Колонка-ссылка: свойство
refзадаёт внешний ключ прямо в определении колонки. Это краткая форма, эквивалентная отдельной конструкцииRef.
Обратите внимание: default: `now()` — это выражение SQL, поэтому оно заключено в обратные кавычки. ArchDB отличает строки-литералы от выражений.
Свойство ref позволяет описать связь максимально компактно, но если вам нужно задать имя внешнеключевому ограничению или добавить несколько свойств, удобнее использовать отдельную конструкцию Ref, о которой мы поговорим чуть ниже.
Таблица (Table) и шаблон (Template)
Таблица — контейнер для колонок. Шаблон — многократно используемый фрагмент таблицы. Шаблоны поддерживают множественное наследование.
template-table.adb
// Шаблон для аудита
Template Auditable {
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
created_by integer
}
// Шаблон для мягкого удаления
Template SoftDeletable {
is_deleted boolean [default: false]
deleted_at timestamp
}
// Таблица использует оба шаблона
Table User extends Auditable, SoftDeletable {
id integer [pk, increment]
username varchar [unique, not_null]
}

Таблица User автоматически получит все поля из обоих шаблонов: created_at, updated_at, created_by, is_deleted, deleted_at, плюс свои id и username. Именно так лучшие практики (аудит, мягкое удаление) превращаются в код и перестают быть просто пунктами в документе «Стандарты разработки».
Связи (Ref и Relation)
Для связи таблиц используются две конструкции: Ref (простой внешний ключ) и Relation (именованное отношение, в том числе «многие-ко-многим»).
// Простой внешний ключ (отдельная конструкция)
Ref: Order.customer_id -> Customer.id [on_delete: cascade]
// Альтернативный способ — свойство ref у колонки (показан выше)
// OrderItem.product_id integer [ref: -> Product.id]
// Отношение "многие-ко-многим"
Relation UserRoles {
User.id <-> Role.id
}
Ref обычно достаточно для большинства связей. Relation удобен, когда нужно подчеркнуть бизнес-смысл (например, Enrollment между студентом и курсом) или когда связь реализуется через отдельную таблицу.
Какой способ выбрать? Если связь простая и не требует дополнительных свойств (имени, своего блока документации), можно использовать ref прямо у колонки. Если же связь сложная или вы хотите сделать её более заметной в модели, лучше вынести в отдельный Ref или даже в Relation.
Собираем всё вместе: модель интернет-магазина
Теперь, зная основы, давайте построим небольшую, но реалистичную схему интернет-магазина с товарами, заказами и статусами.
shop2.adb
// Перечисление для статуса заказа
Enum OrderStatus {
pending [note: "Ожидает оплаты"]
paid [note: "Оплачен"]
shipped [note: "Отправлен"]
delivered [note: "Доставлен"]
cancelled [note: "Отменён"]
}
// Шаблон для аудита
Template Auditable {
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
}
// Пользователи
Table User extends Auditable {
id integer [pk, increment]
email varchar(255) [unique, not_null]
full_name varchar
}
// Товары
Table Product extends Auditable {
id integer [pk, increment]
sku varchar(50) [unique, not_null]
name varchar(255) [not_null]
price decimal(10, 2) [check: `price >= 0`]
stock integer [default: 0]
}
// Заказы
Table Order {
id integer [pk, increment]
user_id integer [not_null]
status OrderStatus [not_null, default: 'pending']
total decimal(12, 2) [check: `total >= 0`]
order_date timestamp [default: `now()`]
}
// Позиции заказа
Table OrderItem {
id integer [pk, increment]
order_id integer [not_null]
product_id integer [not_null]
quantity integer [not_null, check: `quantity > 0`]
price decimal(10, 2) [not_null] // цена на момент заказа
}
// Связи
Ref: Order.user_id -> User.id [on_delete: restrict]
Ref: OrderItem.order_id -> Order.id [on_delete: cascade]
Ref: OrderItem.product_id -> Product.id [on_delete: restrict]

Этот пример уже использует:
-
перечисление (
Enum) для типизированного статуса; -
шаблон
Auditableдля полей аудита; -
проверки (
check) для бизнес-правил; -
внешние ключи с явными действиями при удалении.
И всё это — в одном файле, готовом к версионированию, ревью и дальнейшей генерации DDL или диаграмм.
Что дальше? Вы только что увидели базовый синтаксис ArchDB. Но настоящая сила языка раскрывается, когда модель начинает расти. В следующем разделе мы сравним два подхода к организации кода: монолит, который быстро становится неуправляемым, и модульную структуру, которая позволяет работать над разными частями системы независимо. Но сначала…
Как-то это всё ванильно, или как «зачитить» и «пропатчить»
Пример с интернет-магазином хорош, но в реальной жизни базы данных редко выглядят так же чисто, как на картинке. Чаще всего перед нами — результат многолетней эволюции, десятков разработчиков, срочных патчей и «временных» решений, которые стали постоянными. В такой базе поля аудита могут называться как угодно, а их набор отличается от таблицы к таблице. Одни команды любили created_at, другие — create_dt, третьи вообще обходились без updated_by, потому что «нам не надо». Давайте посмотрим, как ArchDB помогает навести порядок в этом хаосе.
Разнобой в аудиторских полях
Вот типичный фрагмент такой «органической» базы (назовём её legacy):
// Таблица заказов — писала команда А
Table orders {
id integer [pk]
order_date date
created_ts timestamp // создано
modified_ts timestamp // обновлено
created_by integer
}
// Таблица клиентов — команда Б
Table customers {
id integer [pk]
name varchar
create_date timestamp // создано
update_date timestamp // обновлено
updated_by integer
}
// Таблица товаров — команда В (спешили, поэтому только created)
Table products {
id integer [pk]
sku varchar
created timestamp
updated timestamp
// updated_by нет — «и так сойдёт»
}
// Таблица счетов — команда Г, любители длинных имён
Table invoices {
id integer [pk]
inv_num varchar
ins_time timestamp // inserted
upd_time timestamp // updated
ins_user integer
upd_user integer
}
Знакомая картина? Если нужно добавить во все таблицы поле deleted_at для мягкого удаления, придётся лезть в каждую таблицу и вручную дописывать колонку. А если ещё и имена разные — начинается настоящий ад.
ArchDB позволяет привести этот хаос к единому стандарту, не ломая существующую базу. Секрет в гибких шаблонах и возможности переопределять имена колонок через свойство name.
Сначала определим эталонные шаблоны, которые отражают наши лучшие практики:
// Базовый шаблон для идентификатора
Template Identifiable {
id integer [pk, increment]
}
// Шаблон для времени создания
Template Created {
created_at timestamp [default: `now()`]
}
// Шаблон для времени обновления
Template Updated {
updated_at timestamp [default: `now()`]
}
// Шаблон для информации о создателе
Template Creator {
created_by integer
}
// Шаблон для информации о редакторе
Template Updater {
updated_by integer
}
Теперь «подключаем» существующие таблицы к этим шаблонам, используя наследование и свойство name, чтобы сопоставить локальные имена колонок с эталонными:
// Таблица orders приводится к стандарту
Table orders extends Identifiable, Created, Updated, Creator {
// Локальное имя created_ts отображаем на created_at
created_at timestamp [name: "created_ts"]
updated_at timestamp [name: "modified_ts"]
created_by integer [name: "created_by"] // имя совпадает - можно не писать, но можно и вписать
// остальные поля как есть
order_date date
}
// Таблица customers
Table customers extends Identifiable, Created, Updated, Updater {
created_at timestamp [name: "create_date"]
updated_at timestamp [name: "update_date"]
updated_by integer [name: "updated_by"]
name varchar
}
// Таблица products — здесь нет updated_by, поэтому наследуем только нужное
Table products extends Identifiable, Created, Updated {
created_at timestamp [name: "created"]
updated_at timestamp [name: "updated"]
sku varchar
}
// Таблица invoices
Table invoices extends Identifiable, Created, Updated, Creator, Updater {
created_at timestamp [name: "ins_time"]
updated_at timestamp [name: "upd_time"]
created_by integer [name: "ins_user"]
updated_by integer [name: "upd_user"]
inv_num varchar
}
Что мы получили?

-
Все таблицы теперь «знают» о едином стандарте. Генераторы DDL, документации или кода могут обращаться к полям
created_at,updated_atи т.д., даже если в реальной базе они называются иначе. -
Если потребуется добавить новое общее поле (например,
deleted_at), достаточно добавить его в соответствующий шаблон — и оно автоматически появится во всех таблицах, которые этот шаблон наследуют. При этом физическое имя можно будет переопределить черезname, если в какой-то таблице оно уже занято. -
Мы не потеряли старые данные и не сломали существующие запросы — физическая схема осталась прежней.
-
Мы получили единообразную логическую модель, ее легко читать и понимать.
Мягкое удаление: флаги и даты
В легаси-базах мягкое удаление может быть реализовано по-разному: где-то флаг deleted, где-то is_deleted, а где-то вообще дата удаления, по которой понимают, удалена запись или нет.
Пусть у нас есть:
-
orders— полеdeleted_flag(1/0) -
customers— полеis_deleted(boolean) -
products— полеdeleted_date(timestamp, если не NULL — удалено) -
invoices— вообще без мягкого удаления
Мы хотим ввести единый интерфейс: все таблицы должны иметь возможность проверять, удалена ли запись, и при необходимости получать дату удаления. Но менять физическую схему нельзя (пока?..).
Решение — создать два шаблона: SoftDeleteFlag и SoftDeleteDate, и подключать их выборочно, переопределяя имена.
Template SoftDeleteFlag {
is_deleted boolean [default: false]
}
Template SoftDeleteDate {
deleted_at timestamp
}
// В orders есть флаг, но нет даты
Table orders extends SoftDeleteFlag {
is_deleted boolean [name: "deleted_flag"]
}
// В customers есть флаг
Table customers extends SoftDeleteFlag {
is_deleted boolean [name: "is_deleted"] // имя совпадает
}
// В products есть дата удаления
Table products extends SoftDeleteDate {
deleted_at timestamp [name: "deleted_date"]
}
// В invoices мягкого удаления нет — шаблоны не подключаем
Table invoices {
id integer [pk]
// ...
}
Теперь в коде модели можно единообразно работать с is_deleted и deleted_at, а генераторы будут создавать правильные DDL с учётом физических имён.
Композитные ключи с разным порядком и именами
Составные первичные ключи — ещё одна зона хаоса. В одной таблице ключ может быть (order_id, line_number), в другой — (product_id, attribute_id), и ограничения названы как попало.
ArchDB позволяет явно описать ключ через индекс со свойством pk и задать ему имя.
Table order_items {
order_id integer
line_number integer
quantity integer
indexes {
(order_id, line_number) [pk, name: "pk_order_items"]
}
}
Table product_attributes {
product_id integer
attribute_id integer
value text
indexes {
(product_id, attribute_id) [pk, name: "pk_product_attrs"]
}
}
Если захочется унифицировать имена ключей, можно создать шаблон, но проще оставить как есть — главное, что структура ключа описана декларативно и не потеряется.
Вычисляемые поля с разной логикой
Современные СУБД поддерживают виртуальные/сохранённые вычисляемые колонки. В легаси такое могли реализовать через триггеры или на стороне приложения. ArchDB позволяет декларировать вычисляемые поля, а при генерации DDL они превратятся в соответствующие конструкции.
Предположим, в таблице invoices раньше tax_amount вычислялось в коде, а мы хотим перенести логику в БД:
Table invoices {
id integer [pk]
subtotal decimal
tax_rate decimal
tax_amount decimal [generated: `subtotal * tax_rate`]
total decimal [generated: `subtotal + tax_amount`]
}
А в таблице orders скидка рассчитывается иначе:
Table orders {
id integer [pk]
total_before decimal
discount_percent decimal
discount_amount decimal [generated: `total_before * discount_percent / 100`]
total decimal [generated: `total_before - discount_amount`]
}
Если в дальнейшем мы решим привести названия к единому виду, опять поможет name.
Индексы с условиями для разных бизнес-правил
Часто индексы создают под конкретные запросы, и условия могут быть самыми разными. ArchDB поддерживает свойство where для индексов.
Table orders {
id integer [pk]
status varchar
created_at timestamp
indexes {
(created_at) [where: `status = 'pending'`, name: "idx_pending_orders"]
(created_at) [where: `status = 'shipped'`, name: "idx_shipped_orders"]
}
}
А если в разных таблицах похожие индексы называются по-разному, можно через name привести к единому стилю.
Вложенные свойства для мета-информации
ArchDB допускает вложенные свойства, что можно использовать для передачи дополнительных указаний генераторам. Например, пометить поля, которые должны маппиться на определённые типы в Java.
Table user {
id integer [pk, java: [ type: "Long", annotation: "@Id" ]]
email varchar [ java: [ type: "String", annotation: "@Email" ]]
}
Это уже экзотика, но пример показывает, что язык можно расширять под свои нужды.
Небольшой итог
ArchDB даёт возможность навести порядок в базе, не устраивая «большой взрыв» и не переписывая всё с нуля. Постепенно, шаг за шагом, можно привести любую, самую запущенную схему к единообразию. Единообразию на уровне логического описания модели в формате ArchDB, всё богатство и разнообразие физических БД проекта останется нетронутым. Нетронутым пока? Вам начинают приходить в голову мысли о приведении в порядок и физических БД? Это всё из-за ArchDB, есть у него такое свойство — наталкивать на мысли.
Этот подход особенно ценен при работе с унаследованными системами, где полная миграция на новый стандарт за один день невозможна.
От простого к сложному: монолит или модули?
Вы только что написали свою первую модель на ArchDB. В маленьком проекте можно хранить всё в одном файле — это удобно и просто. Но что произойдёт, когда таблиц станет пятьдесят, а над разными частями системы (каталог, заказы, финансы) будут работать разные команды? Монолитная модель быстро превращается в проблему.
Давайте посмотрим на примере того же интернет-магазина.
Монолит: всё в одной куче
Представьте, что все таблицы — User, Product, Order, Payment, Invoice — описаны в одном огромном файле ecommerce.adb. Связи между ними переплетены, и чтобы понять, как устроен, скажем, модуль финансов, приходится продираться через десятки посторонних таблиц.
Проще говоря: навигация превращается в квест, высокая связанность не позволяет выделить поддомен, а параллельная работа нескольких команд над одной моделью становится практически невозможной — конфликты слияния гарантированы.
|
Преимущества монолита |
Недостатки |
|
Простота на старте |
Сложность навигации и восприятия при росте |
|
Все связи «на виду» |
Высокая связанность, сложно выделить поддомен |
|
|
Затруднено параллельное развитие разными командами |
Модули: разбиваем на части
ArchDB предлагает другой путь — модульную организацию. Модель разбивается на логические схемы (Schema), каждая из которых соответствует бизнес-поддомену: Core, Product, Order, Finance. Общие шаблоны (например, Auditable) выносятся в ядро и импортируются в другие модули.
Вот как может выглядеть файл модуля заказов (Order.adb):
Package com.mycompany.ecommerce.Order
import "com.example.ecommerce.Core.adb" (1)
import "com.example.ecommerce.Product.adb" (1)
Table Order extends Core.IdentifiableUuid, Core.Auditable { (2)
user_id uuid [ref: > Core.User.id]
total decimal
}
Table OrderItem {
order_id uuid [ref: > Order.id]
product_id uuid [ref: > Catalog.Product.id] (3)
quantity integer
}
-
Импорт общей библиотеки (ядро) и модуля продуктов, соответственно из файлов
Core.adbиProduct.adb. -
Наследование от шаблонов
IdentifiableUuidиAuditable, определённых вCore.adb. -
Ссылка на сущность из импортированного модуля.
Что мы получили:
-
Независимость команд. Команда «Заказы» может менять свой модуль, не затрагивая «Каталог» или «Финансы», пока не меняет публичный интерфейс.
-
Чёткие границы. Каждый модуль явно импортирует только то, что ему нужно.
-
Возможность версионирования. Модули можно развивать независимо и даже публиковать как внутренние библиотеки.
-
Упрощённое понимание. Заглянув в папку
order, вы сразу видите все сущности, относящиеся к заказам, и их зависимости.
Этот подход масштабируется на проекты любого размера. В спецификации ArchDB вы найдёте более полный пример с разделением на четыре модуля и экспортом общих шаблонов.
Вывод: модульность — не просто «красивая опция», а необходимость для промышленной разработки. ArchDB даёт для этого все инструменты: схемы, импорт/экспорт, пакеты и контроль версий.
Организация кода: как управлять моделью, когда она перестаёт помещаться в один файл
Модульность — это не только про разбиение на файлы, но и про правила, по которым эти файлы общаются друг с другом. ArchDB даёт целый арсенал средств, чтобы большая модель оставалась прозрачной, управляемой и удобной для командной работы.
Пространства имён: Package
Когда вы начинаете делить модель на модули, неизбежно возникает риск конфликта имён. Что, если в модуле Finance и в модуле Product есть таблица Transaction? Чтобы избежать путаницы, ArchDB предлагает механизм пакетов (Package).
Объявление пакета помещается в начале файла и задаёт пространство имён для всех определений в этом файле:
Package com.mycompany.ecommerce.Order
// Теперь полное имя таблицы Order — com.mycompany.ecommerce.Order.Order
Table Order { ... }
Пакеты обычно соответствуют обратному доменному имени компании и названию модуля — это стандартная практика для больших проектов. При импорте из другого пакета вы можете использовать полное квалифицированное имя или задать псевдоним.
Проект: глобальные настройки
Конструкция Project задаёт параметры, общие для всей модели: целевую СУБД, версию, автора, описание. Обычно Project помещают в главный файл проекта (например, ecommerce.adb).
Project ECommercePlatform [
database: "postgresql",
version: 2.5.0,
author: "Platform Team",
note: "Модель данных интернет-магазина"
]
Эти настройки могут использоваться генераторами DDL и другими инструментами.
Схемы: логические модули
Схема (Schema) — это основной контейнер для группировки сущностей по смыслу. В отличие от просто разнесения по файлам, схема создаёт чёткие границы: всё, что объявлено внутри Schema, логически принадлежит одному модулю. Схемы могут соответствовать схемам в базе данных (например, finance в PostgreSQL), а могут быть чисто организационными.
Схема создает вложенное в пакет пространство имен.
Пример схемы Core:
Schema Core [note: "Общие шаблоны и базовые сущности"] {
Template Auditable { ... }
Table User { ... }
}
Импорт и экспорт: повторное использование как в коде
Самый мощный механизм модульности — директивы import и export. Они позволяют создавать настоящие библиотеки многократно используемых компонентов.
Допустим, у вас есть централизованный репозиторий «Стандарты компании» с шаблонами Auditable, SoftDelete, IdentifiableUuid. Вы оформляете его как экспортируемый модуль:
// Файл com/company/standards.adb
Package com.company.standards
export Auditable as "com.company.standards.Auditable" version 1.2.0
export SoftDelete as "com.company.standards.SoftDelete"
Template Auditable { ... }
Template SoftDelete { ... }
А в любом другом проекте просто импортируете нужную версию:
import "com.company.standards.adb" version 1.2.0
Table Order extends Auditable { ... }
Это кардинально меняет способ распространения лучших практик. Вместо многостраничных PDF-документов «Требования к проектированию» разработчики получают работающий код, который можно сразу использовать, и который гарантирует соблюдение стандартов.
Метаданные: документируем файл
Блок Metadata позволяет добавить к файлу служебную информацию: автора, дату создания, описание, лицензию. Это особенно полезно для библиотек, распространяемых между командами.
Metadata [
author: "Иванов И.И.",
version: 1.0.0,
description: "Шаблоны аудита для всех проектов компании",
license: "Internal"
]
Собираем всё вместе
Вот как может выглядеть структура типичного проекта, использующего все эти возможности:
com/example/ecommerce/
├── Core.adb // Пакет com.example.ecommerce.Core, экспорт шаблонов
├── Product.adb // Пакет com.example.ecommerce.Product, импорт Core и экспорт Product
├── Order.adb // Пакет com.example.ecommerce.Order, импорт Core и Product
└── Finance.adb // Пакет com.example.ecommerce.Finance, импорт Order и Core
Каждый модуль имеет собственное пространство имён, чёткие зависимости (через импорт), и все они собираются в единую модель без конфликтов.
Резюме
-
Package — пространство имён, предотвращает конфликты.
-
Project — глобальные настройки модели.
-
Schema — логический контейнер для сущностей одного домена, углубление пространств имен.
-
Import/Export — механизм повторного использования и версионирования.
-
Metadata — самодокументирование файлов.
Используя эти инструменты, вы сможете строить модели любого масштаба, сохраняя их понятными и поддерживаемыми — точно так же, как вы пишете обычный код.
В следующем разделе мы на практике покажем, как мигрировать существующую модель из DBML в ArchDB и сразу получить все описанные преимущества.
Миграция с DBML: переносим существующую модель без боли
У вас уже есть модель на DBML? Отлично — вы не начинаете с нуля. ArchDB спроектирован так, чтобы переход был максимально плавным. Синтаксис описания таблиц, колонок и базовых связей практически идентичен. Достаточно заменить расширение файла с .dbml на .adb и поправить пару мелочей — и вы уже в мире модульности и шаблонов.
Давайте посмотрим на конкретном примере. Возьмём типичную DBML-схему блога:
Исходный файл blog.dbml
TablePartial blogEntity {
id integer [primary key]
created_at timestamp
updated_at timestamp
}
Table users {
username varchar
email varchar [unique]
~blogEntity
}
Table posts {
user_id integer [ref: > users.id]
title varchar
body text
status varchar [default: 'draft']
~blogEntity
}
Table tags {
name varchar [unique]
~blogEntity
}
Table post_tags {
id integer [primary key]
post_id integer [ref: > posts.id]
tag_id integer [ref: > tags.id]
}
Шаг 1. Механический перенос
Просто переименуйте файл в blog.adb и замените [primary key] на [pk]. ArchDB понимает тот же синтаксис для связей (Ref:), так что внешние ключи останутся рабочими. На этом этапе модель уже будет валидной.
Шаг 2. Добавляем шаблоны
Обратите внимание: поля created_at и updated_at повторяются в таблицах users, posts и tags. В DBML они описаны в TablePartial. В ArchDB мы выносим их в шаблон:
Template Identifiable {
id integer [pk, increment]
}
Template BlogEntity extends Identifiable {
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
}
Теперь подключаем шаблон к таблицам через наследование:
Table users extends BlogEntity {
username varchar [not_null]
email varchar [unique, not_null]
}
Table posts extends BlogEntity {
user_id integer [not_null]
title varchar(500) [not_null]
body text
status varchar [default: 'draft']
}
Table tags extends BlogEntity {
name varchar [unique]
}
Сразу становится чище, и добавление новых общих полей (например, deleted_at) потребует правки только в одном месте.
Шаг 3. Типизируем статусы
ArchDB, как и DBML, предлагает перечисления:
Enum PostStatus {
draft
published
archived
}
И задаём тип колонки:
Table posts extends BlogEntity {
// ...
status PostStatus [default: 'draft']
}
Теперь ошибиться невозможно: база данных (или генератор) гарантирует, что попадёт только одно из трёх значений.
Шаг 4. Улучшаем связи
Связь многие-ко-многим между постами и тегами в DBML выражена через явную таблицу post_tags. Это рабочий, но «механический» способ. ArchDB позволяет описать ту же связь более декларативно через Relation:
Relation PostTagging {
posts.id <-> tags.id
}
При генерации DDL такая конструкция может превратиться в ту же таблицу PostTagging, но в модели она занимает одну строку и явно передаёт намерение «у поста может быть много тегов, у тега — много постов».
Итог: что мы получили
Сравните исходный DBML-файл и получившуюся ArchDB-модель:
-
Кода стало меньше — наследование шаблонов позволяет лаконично записывать повторяющиеся элементы.
-
Модель безопаснее — перечисления исключают «магические строки».
-
Появилась семантика —
Relationявно говорит о типе связи. -
Мы заложили фундамент для модульности — теперь можно разнести таблицы по разным схемам и подключать общие шаблоны из библиотек.
И главное — на каждом шаге модель оставалась работоспособной. Вы можете проводить миграцию постепенно: сначала просто переименовать файл, потом добавлять шаблоны, потом перечисления. ArchDB не требует «big bang»-перехода.
Резюме
|
Что было в DBML |
Что стало в ArchDB |
Выгода |
|
|
|
Простая замена |
|
Фрагменты таблиц ( |
Шаблоны ( |
Гибкий механизм множественного наследования |
|
Перечисления ( |
Перечисления ( |
Типобезопасность |
|
Таблицы-связки |
|
Явное выражение намерения |
|
Один файл |
Модули + импорты |
Масштабирование |
Теперь, когда модель готова, можно подключать инструменты: генераторы DDL, визуализаторы, плагины для IDE. Но это уже тема следующей статьи.
Что дальше? Подводим итоги и заглядываем в будущее
Мы прошли путь от первых строк кода до понимания того, как ArchDB превращает описание данных из рутинной документации в живой, управляемый актив проекта. Давайте быстро пробежимся по тому, что узнали:
-
Декларативность и Data as Code — вы фиксируете желаемое состояние модели, а генераторы делают всю черновую работу.
-
Модульность — даже огромную схему можно разбить на независимые модули (схемы), которые развиваются разными командами без конфликтов.
-
Шаблоны и множественное наследование — лучшие практики (аудит, мягкое удаление) перестают быть словами в PDF и становятся кодом, который автоматически распространяется на все таблицы.
-
Гибкие свойства —
note,color,nameи даже пользовательские атрибуты позволяют добавить семантику любому элементу модели. -
Плавная миграция с DBML — вы можете начать пользоваться ArchDB уже сегодня, просто переименовав файлы, и постепенно внедрять более мощные возможности.
Если вы ещё не пробовали ArchDB в деле — самое время открыть редактор, создать файл .adb и описать пару сущностей вашего текущего проекта. Полная спецификация языка поможет не заблудиться в деталях.
Что в следующей серии? В этой статье мы сознательно обошли стороной инструментальную экосистему. В следующий раз поговорим о том, как настроить IDE (IntelliJ IDEA, OpenIDE, GigaIDE) для максимальной продуктивности: подсветка синтаксиса, автодополнение, инспекции, генерация диаграмм прямо из кода и даже встраивание диаграмм в AsciiDoc-документацию через include::foo.adb[]. Это будет разговор о том, как сделать ArchDB вашим основным рабочим инструментом.
А пока — жду ваших вопросов и замечаний в комментариях. Расскажите, с какими проблемами в проектировании БД сталкиваетесь вы и какие возможности ArchDB кажутся вам наиболее полезными. А может, у вас есть предложения по улучшению языка?
Автор: andrbars
