Опирается на правила:
R-REP-1…R-REP-5иR-REP-X1…R-REP-X3из DDD Tactical Style Guide → раздел 5. Repository.
Важно знать
- Порт репозитория — interface в
core/<bc>/port/. Домен импортирует только этот interface; реализацию не видит.- Реализация — в
adapters/out/persistence/на sqlc-сгенерированных запросах +pgxpool.Saveсохраняет агрегат целиком в транзакции. ЧастичныеUpdateStatus/UpdateField— нарушениеR-REP-4.- После успешного commit репозиторий вызывает
order.PullEvents()и публикует события. Для критичных эффектов — Outbox в той же транзакции.- Методы порта — в терминах домена:
ByID,Save,ActiveByCustomer. НеSelectFromDB, неUpdateStatusInDB.- Один репозиторий = один корень агрегата.
OrderRepositoryне возвращаетCustomer.sqlcgen.Order(row-struct из sqlc) не должен выходить за пределыadapters/out/persistence/.
Repository — абстракция над persistence, которая позволяет домену не знать про SQL. Граница: порт в core/, реализация в adapters/. Раскрытие раздела 5 гайда на Go-стеке.
Порт — interface в core
R-REP-1, R-REP-3, R-REP-5:
// core/order/port/order_repository.go
package port
import (
"context"
"github.com/google/uuid"
"example.com/svc/core/order/aggregate"
)
type OrderRepository interface {
ByID(ctx context.Context, id uuid.UUID) (*aggregate.Order, error)
Save(ctx context.Context, order *aggregate.Order) error
ActiveByCustomer(ctx context.Context, customerID uuid.UUID) ([]*aggregate.Order, error)
}
Методы названы в терминах домена:
ByID— найти агрегат по идентификатору.Save— создать или обновить агрегат целиком.ActiveByCustomer— бизнес-запрос, неSelectWhereStatusAndCustomerID.
Домен использует port.OrderRepository через конструкторную инъекцию:
type ConfirmOrderHandler struct {
orders port.OrderRepository
clock func() time.Time
}
Реализация — adapters/out/persistence
R-REP-2: реализация в adapters/out/persistence/. Использует sqlc-сгенерированные запросы и pgxpool:
// adapters/out/persistence/order_repository.go
package persistence
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"example.com/svc/adapters/out/persistence/sqlcgen"
"example.com/svc/core/order/aggregate"
"example.com/svc/core/order/port"
)
type pgOrderRepository struct {
pool *pgxpool.Pool
queries *sqlcgen.Queries
publisher DomainEventPublisher
}
func NewOrderRepository(pool *pgxpool.Pool, publisher DomainEventPublisher) port.OrderRepository {
return &pgOrderRepository{
pool: pool,
queries: sqlcgen.New(pool),
publisher: publisher,
}
}
Конструктор возвращает port.OrderRepository (interface), не *pgOrderRepository. Это гарантирует, что клиенты работают только с портом.
ByID — маппинг из row в агрегат
func (r *pgOrderRepository) ByID(ctx context.Context, id uuid.UUID) (*aggregate.Order, error) {
row, err := r.queries.GetOrder(ctx, id)
if err != nil {
return nil, fmt.Errorf("order by id %s: %w", id, err)
}
lines, err := r.queries.GetOrderLines(ctx, id)
if err != nil {
return nil, fmt.Errorf("order lines for %s: %w", id, err)
}
return toAggregate(row, lines), nil
}
toAggregate — приватная функция маппинга из sqlcgen-типов в агрегат. Возвращает *aggregate.Order. sqlcgen.Order наружу не выходит (R-REP-X1).
Save — целиком в транзакции
R-REP-4: Save сохраняет агрегат целиком. Никаких UPDATE orders SET status = $1:
func (r *pgOrderRepository) Save(ctx context.Context, order *aggregate.Order) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
q := r.queries.WithTx(tx)
if err := upsertOrder(ctx, q, order); err != nil {
return err
}
if err := upsertOrderLines(ctx, q, order); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit order %s: %w", order.ID(), err)
}
for _, ev := range order.PullEvents() {
r.publisher.Publish(ctx, ev)
}
return nil
}
upsertOrder использует INSERT … ON CONFLICT DO UPDATE — один вызов для create и update. Детали SQL скрыты внутри adapters/, домен не знает про них.
Маппинг domain ↔ sqlcgen
func upsertOrder(ctx context.Context, q *sqlcgen.Queries, o *aggregate.Order) error {
return q.UpsertOrder(ctx, sqlcgen.UpsertOrderParams{
ID: o.ID(),
CustomerID: o.CustomerID().Value(),
Status: int32(o.Status()),
})
}
func toAggregate(row sqlcgen.Order, lines []sqlcgen.OrderLine) *aggregate.Order {
// восстанавливает агрегат из row-структур sqlcgen
// детали зависят от конструктора восстановления на агрегате
return aggregate.Restore(
row.ID,
vo.NewCustomerID(row.CustomerID),
aggregate.OrderStatus(row.Status),
toEntityLines(lines),
)
}
Агрегат имеет два конструктора: NewOrder (создание с событием) и Restore (восстановление из persistence без события). Restore — пакетная функция, не экспортируется за пределы aggregate/.
ActiveByCustomer — бизнес-запрос
func (r *pgOrderRepository) ActiveByCustomer(ctx context.Context, customerID uuid.UUID) ([]*aggregate.Order, error) {
rows, err := r.queries.GetActiveOrdersByCustomer(ctx, customerID)
if err != nil {
return nil, fmt.Errorf("active orders for customer %s: %w", customerID, err)
}
result := make([]*aggregate.Order, 0, len(rows))
for _, row := range rows {
lines, _ := r.queries.GetOrderLines(ctx, row.ID)
result = append(result, toAggregate(row, lines))
}
return result, nil
}
Для read-only сценариев с проекциями (не полный агрегат) используется отдельный ViewRepository — port.OrderViewRepository с методами вроде SummaryByCustomer. Это CQRS read-side, не write-side Repository.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Возвращать sqlcgen.Order из методов порта | R-REP-X1 | Только доменные типы из core/ |
Метод UpdateStatusInDB(id, status) | R-REP-X2 | Save(order) — агрегат целиком |
Specification в порте для генерации SQL | R-REP-X3 | ViewRepository с конкретными методами на read-side |
OrderRepository возвращает []*Customer | R-REP-3 | Один Repository = один агрегат |
Реализация репозитория в core/ | R-REP-2 | Только interface в core/; реализация в adapters/out/persistence/ |
Куда дальше
- DDD Tactical → раздел 5. Repository — нормативные формулировки
R-REP-*. - go/aggregate-root.md —
PullEventsи структура агрегата. - go/domain-event.md — публикация событий и Outbox.
- go/module-structure.md — куда кладётся
port/иadapters/out/persistence/. - CQRS Style Guide —
ViewRepositoryи read-side запросы.