Введение: когда документация по БД перестаёт быть мёртвым грузом
Представьте: вы заходите в репозиторий, открываете папку 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 кажутся вам наиболее полезными. А может, у вас есть предложения по улучшению языка?
