Перейти к содержанию

Архитектура

Обзор архитектуры go-activerecord и принципов работы генератора кода.

Содержание раздела

Псевдо-Active Record

go-activerecord реализует модифицированный паттерн Active Record с важным отличием от классической реализации.

Классический Active Record

В классическом паттерне (Ruby on Rails, Eloquent) каждое изменение поля автоматически синхронизируется с БД:

# Ruby on Rails — запись в БД при каждом изменении
user.name = "John"  # → UPDATE users SET name='John' WHERE id=1
user.email = "j@x.com"  # → UPDATE users SET email='j@x.com' WHERE id=1

go-activerecord

В go-activerecord изменения накапливаются в памяти и отправляются в БД одним запросом при явном вызове Update():

// Изменения накапливаются в памяти
user.SetName("John")       // Только в памяти
user.SetEmail("j@x.com")   // Только в памяти
user.IncLoginCount(1)      // Только в памяти

// Один запрос к БД со всеми изменениями
user.Update(ctx)  // → UPDATE users SET name='John', email='j@x.com', login_count=login_count+1 WHERE id=1

Сравнение подходов

Аспект Классический AR go-activerecord
Запись в БД При каждом Set При вызове Update()
Количество запросов N (по числу изменений) 1
Контроль транзакций Неявный Явный
Атомарность Нет Да (один UPDATE)
Накладные расходы Высокие Низкие
Явность Неявная запись Явная запись

Почему всё же Active Record?

Несмотря на отличие в механизме персистенции, go-activerecord сохраняет ключевые принципы паттерна Active Record:

Принцип Active Record Реализация в go-activerecord
Объект = строка таблицы Структура User соответствует записи в таблице users
Поля объекта = колонки user.GetEmail() → колонка email
Объект знает о БД Методы Insert(), Update(), Delete() встроены в объект
CRUD в объекте user.Insert(ctx), user.Update(ctx), user.Delete(ctx)
Селекторы как статические методы user.SelectById(ctx, 123)
// Классическая структура Active Record
user := user.New(ctx)           // Новая запись
user.SetName("John")            // Поле = колонка
user.Insert(ctx)                // Объект сам себя сохраняет

found, _ := user.SelectById(ctx, 1)  // Статический finder
found.SetName("Jane")           // Изменение поля
found.Update(ctx)               // Объект сам себя обновляет
found.Delete(ctx)               // Объект сам себя удаляет

Отличие: явная персистенция

Единственное отличие — момент записи в БД:

  • Классический AR: неявно при изменении поля
  • go-activerecord: явно при вызове Update()

Это осознанный компромисс ради производительности и атомарности.

Почему такой выбор?

  1. Производительность — один запрос вместо N
  2. Атомарность — все изменения применяются вместе
  3. Контроль — разработчик явно решает, когда писать в БД
  4. Мутаторы — атомарные операции (inc, dec, set_bit) невозможны при автосохранении
  5. Предсказуемость — нет неожиданных запросов к БД

Гибридный подход

go-activerecord сочетает структуру Active Record (объект = запись, CRUD в объекте) с механизмом Unit of Work (накопление изменений, явный flush). Это даёт лучшее из обоих миров: интуитивный API и высокую производительность.

Обзор

go-activerecord — это генератор кода, реализующий модифицированный паттерн Active Record. В отличие от ORM с reflection, весь код генерируется на этапе сборки.

graph LR
    subgraph "Compile time"
        A[Декларации<br/>*.go] --> B[argen]
        B --> C[Сгенерированный<br/>код]
    end

    subgraph "Runtime"
        C --> D[pkg/activerecord<br/>Core]
        C --> E[pkg/postgres<br/>или pkg/octopus]
        E --> F[(База данных)]
    end

Компоненты системы

Генератор (argen)

Компонент Путь Описание
CLI cmd/argen/ Точка входа командной строки
Парсер internal/pkg/parser/ Чтение Go файлов и AR комментариев
Валидатор internal/pkg/checker/ Проверка корректности деклараций
Генератор internal/pkg/generator/ Генерация кода из шаблонов
Бэкенды internal/pkg/backend/ Специфичная логика для каждой БД

Runtime библиотеки

Пакет Описание
pkg/activerecord Ядро: интерфейсы, пул соединений, кластеры
pkg/postgres Реализация для PostgreSQL (pgx/v5)
pkg/octopus Реализация для Octopus/Tarantool 1.5
pkg/iproto Протокол iproto для Octopus
pkg/serializer Встроенные сериализаторы
pkg/logger Абстракции логирования

Поток данных

graph TB
    subgraph "Декларация"
        A[Fields*<br/>Поля] --> G[argen]
        B[Indexes*<br/>Индексы] --> G
        C[Serializers*<br/>Сериализаторы] --> G
    end

    subgraph "Генерация"
        G --> H[Валидация]
        H --> I[Шаблоны]
        I --> J[*.go файлы]
    end

    subgraph "Runtime"
        J --> K[CRUD<br/>Insert/Update/Delete]
        J --> L[Селекторы<br/>SelectBy*]
        J --> M[Мутаторы<br/>Inc*/SetBit*]
    end

Принципы проектирования

1. Zero-reflection

Весь код генерируется на этапе компиляции. Нет reflection в runtime → максимальная производительность.

2. Type safety

Типы проверяются компилятором Go. Ошибки обнаруживаются при сборке, а не в production.

3. Convention over configuration

Разумные значения по умолчанию. Минимум обязательных настроек для начала работы.

4. Backend agnostic

Единый API для разных баз данных. Смена бэкенда требует минимальных изменений в декларации.

Совет

Изучите раздел Генерация кода для понимания 3-фазного процесса.

Следующие шаги