В этом домашнем задании вам предстоит реализовать интеграцию с базой данных в рамках сервиса library.
Для простоты понимания описание этого ДЗ сделано в императивном, а не декларативном стиле
Ниже описана одна из возможных реализаций схемы базы данных. Вы можете сделать свою, объяснив выбор в комментариях PR
Сперва вам необходимо написать миграции к вашей базе данных.
Создайте директорию db/migrations с вашими миграциями, а
также db/migrations/migrate.go
для их применения
Создайте таблицу author
-- +goose Up
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE author
(
id ...,
name ...,
created_at ...,
updated_at ...
);
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION update_author_timestamp() RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
CREATE OR REPLACE TRIGGER trigger_update_author_timestamp
BEFORE UPDATE
ON ...
FOR EACH ROW
EXECUTE FUNCTION update_author_timestamp();
-- +goose Down
DROP TABLE ...;
Отдельной миграцией создайте индекс на имя автора
-- +goose Up
CREATE INDEX ...;
-- +goose Down
DROP INDEX ...;
Создайте таблицу book
-- +goose Up
CREATE TABLE book
(
id ...,
name ...,
created_at ...,
updated_at ...
);
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION update_book_timestamp() RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
CREATE OR REPLACE TRIGGER trigger_update_book_timestamp
BEFORE UPDATE
ON ...
FOR EACH ROW
EXECUTE FUNCTION update_book_timestamp();
-- +goose Down
DROP TABLE ...;
Отдельной миграцией создайте индекс на имя книги
-- +goose Up
CREATE INDEX ...;
-- +goose Down
DROP INDEX ...;
Создайте таблицу author_book
-- +goose Up
CREATE TABLE author_book
(
author_id ...,
book_id ...,
PRIMARY KEY (.. .)
);
-- +goose Down
DROP TABLE author_book;
foreign key для author_id и book_id.ON DELETE CASCADE, в случае удаления автора или книги в этой таблице не должныPRIMARY KEY, состоящий из author_id и book_idКомпозитный PRIMARY KEY по умолчанию добавляет индекс на свои части, однако
его эффективность для каждого атрибута разная.
Отдельной миграцией добавьте индекс для book_id
В файле db/migrations/migrate.go напишите код, который будет накатывать миграции.
Используйте библиотеки ниже, а также //go:embed migrations/*.sql для загрузки
миграций - пример
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"github.com/project/library/config"
Попробуйте поднять базу данных и проверить, что ваши миграции корректно накатываются
docker volumes
docker volume ls // если нужно удалить старый volume
docker volume rm ... // если нужно удалить старый volume
docker-compose up -d
docker ps -a // посмотреть контейнеры
docker stop / docker rm - для остановки и удаления контейнера
2025/03/06 15:03:14 OK 001_create_author_table.sql (5.89ms)
2025/03/06 15:03:14 OK 002_create_author_name_index.sql (8.83ms)
2025/03/06 15:03:14 OK 003_create_book_table.sql (9.78ms)
2025/03/06 15:03:14 OK 004_create_book_name_index.sql (2.51ms)
2025/03/06 15:03:14 OK 005_create_author_book_table.sql (3.28ms)
2025/03/06 15:03:14 OK 006_create_author_book_book_id_index.sql (2.99ms)
2025/03/06 15:03:14 goose: successfully migrated database to version: 6
Поддержите в вашем конфиге параметры для подключения к базе данных
type (
Config struct {
GRPC
PG
}
GRPC struct {
Port string `env:"GRPC_PORT"`
GatewayPort string `env:"GRPC_GATEWAY_PORT"`
}
PG struct {
URL string
Host string `env:"POSTGRES_HOST"`
Port string `env:"POSTGRES_PORT"`
DB string `env:"POSTGRES_DB"`
User string `env:"POSTGRES_USER"`
Password string `env:"POSTGRES_PASSWORD"`
MaxConn string `env:"POSTGRES_MAX_CONN"`
}
)
Пример URL:
postgres://user:password@host:port/dbname?sslmode=disable&pool_max_conns=10
Добавьте новую реализацию репозитория вашего сервиса, используя поднятую базу данных.
Не забывайте про консистентность и атомарность операций. Пример:
func (r *PostgresRepository) CreateBook(ctx context.Context, book entity.Book) (entity.Book, error) {
tx, err := r.db.Begin(ctx)
if err != nil {
return entity.Book{}, err
}
defer tx.Rollback(ctx)
const queryBook = `INSERT INTO book (name) VALUES ($1) RETURNING id, created_at, updated_at`
err = tx.QueryRow(ctx, queryBook, book.Name).Scan(&book.ID, &book.CreatedAt, &book.UpdatedAt)
if err != nil {
return entity.Book{}, err
}
const queryAuthorBooks = `INSERT INTO author_book (author_id, book_id) VALUES ($1, $2)`
for _, authorID := range book.AuthorIDs {
_, err := tx.Exec(ctx, queryAuthorBooks, authorID, book.ID)
if err != nil {
return entity.Book{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return entity.Book{}, err
}
return book, nil
}
DEFAULT uuid_generate_v4()Добавьте в API для Book поля created_at и updated_at
import "google/protobuf/timestamp.proto";
message Book {
...
google.protobuf.Timestamp created_at = ...;
google.protobuf.Timestamp updated_at = ...;
}
С этой части начинается ДЗ outbox. Ветка с решением должна иметь название outbox. Важно, чтобы в PR не было diff'a старого ДЗ.
Вы можете добиться этого, сделав rebase на main после проверки предыдущего ДЗ
Реализуйте паттерн outbox, который обсуждался на лекции
Создайте таблицу outbox
CREATE TYPE outbox_status as ENUM ('CREATED', 'IN_PROGRESS', 'SUCCESS');
CREATE TABLE outbox
(
idempotency_key TEXT PRIMARY KEY,
data JSONB NOT NULL,
status outbox_status NOT NULL,
kind INT NOT NULL,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
Поддержите транзакции на уровне доменной логики
type Transactor interface {
WithTx(ctx context.Context, function func(ctx context.Context) error) error
}
func extractTx(ctx context.Context) (pgx.Tx, error) {}
func injectTx(ctx context.Context, pool *pgxpool.Pool) (context.Context, error, pgx.Tx) {}
Например:
func (l *libraryImpl) RegisterBook(ctx context.Context, name string, authorIDs []string) (*library.AddBookResponse, error) {
var book entity.Book
err := l.transactor.WithTx(ctx, func(ctx context.Context) error {
book, txErr = l.booksRepository.CreateBook(ctx, entity.Book{
Name: name,
AuthorIDs: authorIDs,
})
...
l.outboxRepository.SendMessage(ctx, idempotencyKey, repository.OutboxKindBook, serialized)
})
...
}
Поддержите конфиг для Outbox
type Outbox struct {
Enabled bool `env:"OUTBOX_ENABLED"`
Workers int `env:"OUTBOX_WORKERS"`
BatchSize int `env:"OUTBOX_BATCH_SIZE"`
WaitTimeMS time.Duration `env:"OUTBOX_WAIT_TIME_MS"`
InProgressTTLMS time.Duration `env:"OUTBOX_IN_PROGRESS_TTL_MS"`
AuthorSendURL string `env:"OUTBOX_AUTHOR_SEND_URL"`
BookSendURL string `env:"OUTBOX_BOOK_SEND_URL"`
}
При создании книги или автора вам необходимо асинхронно отправить POST запрос c AuthorID или BookID на OUTBOX_AUTHOR_SEND_URL
или OUTBOX_BOOK_SEND_URL, соответственно.
Для удобства выполнения и проверки дз вводится ряд правил, унифицирующих используемые технологии
Код тестов можно посмотреть в файле integration_test.go
Важно, чтобы ваш сервис умел корректно обрабатывать SIGINT и SIGTERM, иначе тесты могут работать некорректно
В рамках вашего сервиса вы должны реализовать конфиг, который будет работать с переменными окружения
Необходимо сгенерировать моки и написать свои тесты, степень покрытия будет проверяться в CI
Вам необходимо своими словами написать README.md в ./docs к своему сервису library
Поскольку количество попыток сдачи ограничено, вы можете написать дополнительные комментарии в PR. Если ваше
обоснование будет достаточно разумным, это может быть учтено при выставлении баллов. Например,
описать, почему вы написали именно такие интерфейсы
описать, почему вы сделали именно такую валидацию
описать, почему вы сделали именно такую схему в базе данных
Открыть pull request из ветки задания в ветку main вашего репозитория.
В описании PR заполнить количество часов, которые вы потратили на это задание.
Отправить заявку на ревью в соответствующей форме.
Время дедлайна фиксируется отправкой формы.
Изменять файлы в ветке main без PR запрещено.
Изменять файл CI workflow запрещено.
Для удобств локальной разработки сделан Makefile. Имеются следующие команды:
Запустить полный цикл (линтер, тесты):
make all
Запустить только тесты:
make test
Запустить линтер:
make lint
Подтянуть новые тесты:
make update
При разработке на Windows рекомендуется использовать WSL, чтобы
была возможность пользоваться вспомогательными скриптами.