Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

Генерируем CRUD для gRPC по схеме БД следуя Google AIP

Как с помощью db-exporter автоматизировать процесс генерации CRUD-операций для gRPC, соблюдая стандарт Google AIP

255 открытий3К показов
Генерируем CRUD для gRPC по схеме БД следуя Google AIP

К сожалению, жизнь разработчика не всегда полна сложными и интересными задачами, время от времени мы все-таки пишем CRUD'ы.

Задача в целом проста:

  1. Открыть структуру таблицы
  2. Скопировать названия колонок
  3. Соотнести типы, которые существуют в спецификации (gRPC, OpenAPI, и т.п.)
  4. Описать API метод
  5. Проверить на соответствие стайл-гайду

Но это утомительно и отнимает драгоценное время разработчиков. Данную инструкцию за нас может выполнить инструмент db-exporter. В этой статье рассмотрим, как с его помощью автоматизировать процесс генерации CRUD-операций для gRPC, соблюдая стандарт Google AIP по ресурсно-ориентированному дизайну.

В последующих разделах мы подробно рассмотрим:

  • Принцип работы db-exporter
  • Процесс настройки и конфигурации
  • Примеры генерации различных типов операций
  • Валидацию сгенерированных proto файлов
  • Как внедрить инструмент для постоянной работы с ним

Знакомимся с инструментом

db-exporter — это Open Source утилита для генерации кода и документации к схеме базы данных. «Экспортировать» можно в различные распространенные форматы: диаграмма классов, protobuf, код на Go и другие. На данный момент инструмент поддерживает PostgreSQL и MySQL. Реализована поддержка основных операционных систем. Инструмент достаточно молодой и активно развивается.

Генерируем CRUD для gRPC по схеме БД следуя Google AIP 1
Принцип работы db-exporter

Для начала работы с db-exporter необходимо скачать его со страницы релизов. Далее разархивировать и переместить в /usr/local/bin. Или выполнить всё одной командой.

Mac OS

			curl https://github.com/ArtARTs36/db-exporter/releases/download/v0.6.0/db-exporter-darwin-arm64.zip -L -o db-exporter.zip && unzip db-exporter.zip db-exporter && sudo mv db-exporter /usr/local/bin/db-exporter
		

Linux

			curl https://github.com/ArtARTs36/db-exporter/releases/download/v0.6.0/db-exporter-linux-amd64.zip -L -o db-exporter.zip && unzip db-exporter.zip db-exporter && sudo mv db-exporter /usr/local/bin/db-exporter
		

Теперь у нас установлен инструмент и мы почти готовы его запускать, но перед этим разберемся, составим пример таблицы, над которой будем проводить эксперименты.

Составим пример

Договоримся, что нашим примером для генерации кода будет являться таблица пользователей. У пользователей есть: id, имя, отчество, дата создания, статус (активен, заблокирован) и дата удаления.

Итого, схема выглядит так:

			CREATE TYPE user_status AS ENUM ('active', 'blocked');

CREATE TABLE users
(
    id           uuid NOT NULL PRIMARY KEY default uuid_generate_v4(),
    first_name   character varying NOT NULL,
    middle_name  character varying,
    created_at   timestamp without time zone NOT NULL default now(),
    status       user_status NOT NULL,
    deleted_at   timestamp without time zone 
);
		

Данного примера достаточно для того, чтобы рассмотреть ключевые моменты:

  • Генерация первичных ключей (поле id)
  • Генерация опциональных полей (поле middle_name)
  • Генерация временных меток (поля created_at, deleted_at)
  • Генерация енамов (поле status, енам user_status)
  • Работа с soft-delete

Генерируем protobuf

Для того, чтобы запустить db-exporter необходимо описать конфигурацию —делается это декларативно, в yaml-файле. db-exporter оперируют задачами, в которых описаны:

  • Формат, в котором нужно выполнить генерацию. В нашем случае — это grpc-crud
  • Директория, в которую сохранить файлы
  • Специфичные формату параметры (далее: спека задачи)

Также важно не забыть указать подключение к базе данных. В нашем примере, используется PostgreSQL на 5432 порту. Для успешного запуска стоит убедиться, что БД запущена и в ней есть схема с таблицами.

Итого, конфигурация, описывающая перечисленные требования, выглядит следующим образом:

			databases:
  default:
    driver: postgres
    dsn: "host=localhost port=5432 user=test password=test dbname=users sslmode=disable" # DSN к базе данных Postgres

tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        table_per_file: true # Указываем, что для каждой таблицы должен генерироваться отдельный файл
        spec:
          package: 'org.user_manager'
          with:
            google.api.field_behavior: true # Говорим, что нам нужно помечать обязательность полей
        out:
          dir: './contracts'
		

Сохраняем этот файл, как .db-exporter.yaml.

Далее запускаем инструмент, выполняя команду db-exporter и получаем отчет о проделанной работе — db-exporter сообщает о том, какие файлы были созданы и какой они имеют вес.

Генерируем CRUD для gRPC по схеме БД следуя Google AIP 2
Результат выполнения db-exporter

Разбираемся, что получили на выходе

В результате выполнения команды был сгенерирован файл users.proto, в котором используется синтаксис соответствует proto3. Файл содержит:

  • UsersService с методами List, Get, Delete, UndeleteCreate, Patch
  • Сообщение User, отражающее таблицу users
  • Енам UserStatus
			// Service for working with users.
service UsersService {
  // Get all users.
  rpc List(ListUsersRequest) returns (ListUsersResponse) {};

  // Get a user.
  rpc Get(GetUserRequest) returns (User) {};

  // Delete a user.
  rpc Delete(DeleteUserRequest) returns (google.protobuf.Empty) {};
  
  // Restore a soft-deleted user.
  rpc Undelete(UndeleteUserRequest) returns (User) {};

  // Create a new user.
  rpc Create(CreateUserRequest) returns (User) {};

  // Update an existing user.
  rpc Update(UpdateUserRequest) returns (User) {};
}
		

Сообщение User и enum UserStatus

Сообщение User отражает структуру таблицы users и имеет вид:

			message User {
  string id = 1 [
    (google.api.field_behavior) = OUTPUT_ONLY
  ];

  string first_name = 2 [
    (google.api.field_behavior) = REQUIRED
  ];

  string middle_name = 3 [
    (google.api.field_behavior) = OPTIONAL
  ];

  google.protobuf.Timestamp created_at = 4 [
    (google.api.field_behavior) = OUTPUT_ONLY
  ];

  UserStatus status = 5 [
    (google.api.field_behavior) = REQUIRED
  ];

  google.protobuf.Timestamp deleted_at = 6 [
    (google.api.field_behavior) = OUTPUT_ONLY
  ];
}
		

Что здесь видим?

  1. id - uuid'ы генерируются, как строки. Первичный ключ помечается, как OUTPUT_ONLY, говоря о том, что поле используется только для ответов.
  2. name, email сгенерированы, как строки
  3. Поля created_at, deleted_at обернуто в гугловый Timestamp
  4. Для статуса пользователя сгенерирован енам UserStatus

UserStatus выглядит следующим образом

			enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_BLOCKED = 2;
}
		

Метод для получения списка пользователя

Метод List принимает на вход ListUsersRequest и возвращает ListUsersResponse

			rpc List(ListUsersRequest) returns (ListUsersResponse) {};
		

Запрос включает в себя поле с массивом идентификаторов пользователей. db-exporter добавляет в тело запроса первичный ключ, если первичный ключ состоит из одного поля. Также в запросе присутствует флаг show_deleted, как того требует стандарт AIP-132, при работе с сущностями, поддерживающими soft-delete: по умолчанию сервер возвращает список активных пользователей, например, используя sql-запрос с фильтром deleted_at is null.

			message ListUsersRequest {
  repeated string ids = 1;

  // If set to `true`, soft-deleted resources will be returned alongside active resources.
  bool show_deleted = 2;

  // Maximum number of results per page.
  uint64 page_size = 3 [
    (google.api.field_behavior) = OPTIONAL
  ];

  // Token of the requested results page.
  string page_token = 4 [
    (google.api.field_behavior) = OPTIONAL
  ];
}
		

По умолчанию db-exporter добавляет token-based пагинацию, описанную в AIP-158. Такая пагинация удобна для межсервисного взаимодействия. Но если основной клиент вашего API — это фронтенд, то вероятнее всего, вам нужна офсетная пагинация. Ее можно включить, добавив в спеку задачи pagination: offset. Если пагинация уж совсем не требуется, то pagination: none.

В ответе же список пользователей и токен следующей страницы.

			message ListUsersResponse {
  repeated User items = 1 [
    (google.api.field_behavior) = OPTIONAL
  ];

  // Next page token.
  string next_page_token = 2 [
    (google.api.field_behavior) = OPTIONAL
  ];
}
		

Метод получения пользователя

Стандарт по получению ресурса описывает достаточную простую конструкцию — Метод Get принимает на вход GetUserRequest и отдает на выход сообщение User.

			rpc Get(GetUserRequest) returns (User) {};
		

Тело запроса включает в себя поле id, так как оно является первичным ключом.

			message GetUserRequest {
  string id = 1 [
    (google.api.field_behavior) = REQUIRED
  ];
}
		

Методы удаления и восстановления пользователя

Стандарт AIP-164 гласит: мало того, что сущность удаляется методом Delete, так еще нужно и мочь ее восстанавливать с помощью метода Undelete, если таблица поддерживает soft-delete.

Поэтому в результате генерации мы получаем методы Delete и Undelete: db-exporter самостоятельно определяет поддержку soft-delete у таблиц, смотря на поля deleted_at и delete_time.

Метод Delete принимает на вход DeleteUserRequest и отдает на выход DeleteUserResponse. Метод Undelete принимает UndeleteUserRequest и возвращает восстановленный ресурс User.

			rpc Delete(DeleteUserRequest) returns (google.protobuf.Empty) {};

rpc Undelete(UndeleteUserRequest) returns (User) {};

		

Тела запросов также, как и для метода Get, содержат поле первичного ключа — id.

			message DeleteUserRequest {
  string id = 1 [
    (google.api.field_behavior) = REQUIRED
  ];
}

message UndeleteUserRequest {
  string id = 1 [
    (google.api.field_behavior) = REQUIRED
  ];
}
		

По умолчанию возвращается google.protobuf.Empty, как требуется в стандарте. Возможна генерация пустого сообщения DeleteUserResponse — для этого нужно добавить параметр в спеку:

			.........
spec:
  package: 'org.user_manager'
  rpc:
    delete:
      returns: wrapper
..........
		

Методы создания и обновления пользователя

По стандартам AIP-133 и AIP-134 методы Create и Update имеют похожие сигнатуры.

			rpc Create(CreateUserRequest) returns (User) {}
rpc Update(UpdateUserRequest) returns (User) {}

		

Тела запросов также схожи, включают в себя ресурс User и возвращают его обновленную версию.

			message CreateUserRequest {
  User user = 1 [
    (google.api.field_behavior) = REQUIRED
  ];
}

message UpdateUserRequest {
  User user = 1 [
    (google.api.field_behavior) = REQUIRED
  ];
  
  // The list of fields to update.
  google.protobuf.FieldMask update_mask = 2 [
    (google.api.field_behavior) = OPTIONAL
  ];
}

		

По стандарту поле update_mask предполагается использовать для частичного обновления сущности. FieldMask содержит в себе список полей, которые необходимо обновить у сущности User.

Добавляем HTTP пути к методам

В gRPC есть полезная опция google.api.http, с помощью которой можно указать HTTP адрес для метода. Ее полезно использовать, если вы используете REST поверх gRPC сервисов — например, с помощью grpc-gateway.

Наверняка, мы хорошие разработчики и версионируем свой API: установим версию v1 в параметре path_prefix.

			databases:
  default:
    driver: postgres
    dsn: "host=localhost port=5432 user=test password=test dbname=users sslmode=disable" # DSN к базе данных Postgres

tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        table_per_file: true # Этот параметр указывает, что для каждой таблицы должен генерироваться отдельный файл
        spec:
          package: 'org.user_manager'
          with:
            google.api.field_behavior: true # Генерируем опцию field_behavior для полей запросов
            google.api.http: # Генерируем опцию google.api.http для заполнения адреса для методов
              path_prefix: /v1
        out:
          dir: './contracts'
		

И получаем вот такой результат:

			// Service for working with users.
service UsersService {
  // Get all users.
  rpc List(ListUsersRequest) returns (ListUsersResponse) {
    option (google.api.http) = {
      get: "/v1/users"
    };
  }

  // Get a user.
  rpc Get(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{id}"
    };
  }

  // Delete a user.
  rpc Delete(DeleteUserRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      delete: "/v1/users/{id}"
    };
  }
  
  // Restore a soft-deleted user.
  rpc Undelete(UndeleteUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/v1/users/{id}:undelete"
    };
  }

  // Create a new user.
  rpc Create(CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/v1/users"
    };
  }

  // Update an existing user.
  rpc Update(UpdateUserRequest) returns (User) {
    option (google.api.http) = {
      patch: "/v1/users/{id}"
    };
  }
}
		

Валидируем результат

Теперь, когда у нас есть файл users.proto — важно проверить, что он корректен синтаксически. Для этого соберем go-клиент, используя утилиту protoc.

Указываем Go пакет

Выше мы обсуждали gRPC контракты без привязки к конкретному стеку. Ну что ж, время пришло!

Для генерации Go-клиента в .proto файле необходимо указать опцию go_package. Мы можем сами поправить .proto файл, но при частом использовании db-exporter это будет не так удобно, поэтому скажем экспортеру, какой пакет нам нужен. Для этого нужно указать опцию в спеке задачи .db-exporter.yaml:

			databases:
  default:
    driver: postgres
    dsn: "host=localhost port=5432 user=test password=test dbname=users sslmode=disable" # DSN к базе данных Postgres

tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        table_per_file: true # Этот параметр указывает, что для каждой таблицы должен генерироваться отдельный файл
        spec:
          package: 'org.user_manager'
          options:
            go_package: github.com/my-org/user-manager/pkg/api # Опция указания Go пакета в .proto
          with:
            google.api.field_behavior: true # Генерируем опцию field_behavior для полей запросов
            google.api.http: # Генерируем опцию google.api.http для заполнения адреса для методов
                path_prefix: /v1 # Указываем версию API
        out:
          dir: './contracts'
		

Нам остается еще раз сгенерировать users.proto, используя уже знакомую команду db-exporter.

Собираем зависимости

Сгенерированный users.proto импортирует зависимости google.api для указания HTTP методов/путей, обязательности полей. Их можно скачать с GitHub googleapis: http.proto, field_behavior.proto и annotations.proto.

Скачать зависимости можем следующим скриптом:

			mkdir -p vendor/google/api && \
curl -L https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/http.proto -o vendor/google/api/http.proto && \
curl -L https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/field_behavior.proto -o vendor/google/api/field_behavior.proto && \
curl -L https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/annotations.proto -o vendor/google/api/annotations.proto

		

Итого мы должны были организовать следующую структуру файлов:

			contracts
├── users.proto # Файл, сгенерированный db-export
├── vendor # Сторонние зависимости
├       google
│       └── api
│           ├── annotations.proto
├           ├── field_behavior.proto
│           └── http.proto
pkg
├── api # Папка, в которую будет сохранен Go код, сгенерированный через protoc

		

Теперь запустим генерацию гошного кода:

			
protoc -I ./contracts \
  --go_out=./pkg/api --go_opt=paths=source_relative \
  --go-grpc_out=./pkg/api --go-grpc_opt=paths=source_relative \
  --proto_path=./contracts/vendor \
  ./contracts/*.proto

		

В результате выполнения команды в наш проект добавились 2 новых файла:

  • pkg/api/users.pb.go — структуры для ресурса, запросов и ответов
  • pkg/api/users_grpc.pb.go — rpc-сервис UserService и клиент к нему

Вывод: из сгенерированного db-экспортером .proto файла можно сгенерировать валидный програмнный код. Считаем проверку успешно выполненной

Непрерывная генерация

Перезапись файлов

Собранная нами конфигурация отлично подходит для «одноразовых генераций», но со временем, когда сервис обрастет новыми методами, использовать генератор будет неудобно из-за того, что он по умолчанию перезаписывает уже существующие файлы. Чтобы избежать такой ситуации, необходимо указать генератору, что мы не хотим перезаписывать файлы. Сделать это можно, добавив опцию skip_exists.

			..........
tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        skip_exists: true # Не перезаписываем уже существующие файлы
..........
		

Теперь db-exporter не будет перезаписывать уже существующие файлы и будет генерировать только новые.

Генерация лишних таблиц

По умолчанию db-exporter генерирует файлы по всем таблицам из схемы БД. В проекте может десяток или десятки таблиц, не для всех из них нужна генерация CRUD-операций. Генератору можно передать список таблиц, по которым необходимо генерировать файлы.

			..........
tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        tables:
          list: users # Генерируем файлы только по таблице users
..........

		

Но править постоянно конфиг не очень удобно. Укажем переменную.

			..........
tasks:
  my-grpc-crud:
    activities:
      - format: grpc-crud
        tables:
          list: $TABLE
..........
		

Теперь мы можем запускать генерацию по конкретным таблицам, например вот так:

			TABLE=users,phones db-exporter
		

Или обернуть это в Makefile:

			grpc-crud:
	TABLE=${TABLE} db-exporter --tasks=my-grpc-crud
		

Использовать эту конструкцию можно следующим образом:

			make grpc-crud TABLE=users
		

Что в итоге?

В итоге мы собрали рабочую конфигурацию db-exporter, которая позволяет генерировать CRUD по схеме БД в валидный .proto файл, включая полезные опции: google.api.field_behavior и google.api.http. Собранную конфигурацию и необходимое окружение вы сможете найти по этой ссылке.

Надеюсь, этот пост был для вас полезным и поможет ускорить разработку крудов. Если у вас есть предложения по улучшению/расширению кодо-генератора, добро пожаловать в репозиторий.

В репозитории вы также сможете найти примеры генерации диаграмм классов, mermaid, markdown и так далее.

Следите за новыми постами
Следите за новыми постами по любимым темам
255 открытий3К показов