Интегрируйте статически типизированные API с GraphQL

В этом посте вы узнаете, как реализовать сервер GraphQL с использованием ent и gqlgen.

Пример репо

Вот ссылка на окончательную кодовую базу:



Зависимости

Требуемые зависимости следующие:

Что такое Энт?

ent — это ORM-фреймворк для Go, который упрощает поддержку приложений с базой данных и позволяет нам:

  • Выполняйте запросы и легко просматривайте любую структуру графа
  • Используйте 100% статически типизированный API посредством генерации кода
  • Используйте несколько драйверов хранилища — MySQL, PostgreSQL, SQLite и Gremlin.
  • Используйте GraphQL с gqlgen

Что касается статически типизированных API, sqlboiler имеет аналогичную функциональность, но разница в том, что ent — это инструмент, основанный на схеме, который создает базу данных и типизированный API из файла схемы. sqlboilder — это ORM, ориентированный на базу данных, который создает типизированный API из базы данных.

Обзор реализации

Этапы реализации следующие:

  • Установить зависимости
  • Настроить эхо-сервер
  • Настроить
  • Настроить gqlgen

Установить зависимости

В этом посте требуются некоторые зависимости, поэтому сначала установите их:

go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/[email protected]
go get github.com/go-sql-driver/mysql
go install entgo.io/ent/cmd/ent

Настроить эхо-сервер

Чтобы быстро начать, мы настроим сервер с помощью пакета echo.

Создайте main.go и напишите код:

И вы можете увидеть страницу по адресу http://localhost:8080:

Настроить

Далее мы настроим ent .

Создать схему пользователя

В этом посте мы создадим таблицу users в нашей базе данных.

Перед этим нам нужно создать файл схемы с именем User:

ent init User

Сгенерированные файлы должны выглядеть так:

ent
├── generate.go
└── schema
    └── user.go

user.go имеет схему, включающую Fields и Edges:

  • Fields — Атрибуты узла. Это эквивалентно столбцу в базе данных, например name, age и created_at.
  • Edges—Отношения сущностей. Это позволяет вам определить отношение каждой таблицы, например O2O (один к одному), O2M (один ко многим) и M2M (многие ко многим).

Итак, мы установим name , age и created_at в Fields следующим образом:

После добавления полей создайте активы на основе приведенной выше схемы, чтобы они работали в вашем проекте.

Запустите go generate в корневом проекте:

go generate ./ent

Это создаст файлы следующим образом:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── enttest
│   └── enttest.go
├── generate.go
├── hook
│   └── hook.go
├── migrate
│   ├── migrate.go
│   └── schema.go
├── mutation.go
├── predicate
│   └── predicate.go
├── runtime
│   └── runtime.go
├── runtime.go
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

Этот актив включает в себя:

  • Код, который работает с CRUD в таблице (user_create.go, user_delete.go, user_query.go и user_update.go)
  • Пакет миграции для SQL
  • Сущностный объект
  • Пакет ловушек для добавления промежуточного программного обеспечения для мутаций.
  • и так далее

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

Миграция базы данных

ent также обеспечивает поддержку миграции базы данных для обеспечения согласованности схемы в вашем проекте.

Чтобы запустить миграцию, напишите код в инициализации приложения.

Напишите логику миграции в main.go :

После завершения инициализации вы увидите, что таблица users успешно создана.

По умолчанию field.Time использует тип TIMESTAMP в MySQL, который имеет диапазон от 1970–01–01 00:00:01 UTC до 2038–01–19 03:14:07 UTC.

Если TIMESTAMP не работает для вашего приложения, вы можете изменить тип на DATETIME следующим образом:

Создать пользователя

Теперь мы готовы создать пользователя в нашей таблице.

Как упоминалось выше, у нас уже были методы, которые имеют CRUD API для таблицы.

Позвоним Create в ent/user_create.go:

Когда вы отправляете запрос POST на http://localhost:8080/users, это создаст пользователя в таблице users и впоследствии ответит данными пользователя:

Запросить пользователя

Чтобы получить пользовательские данные из пакета ent, вы можете использовать Query() следующим образом:

Это ответит всем пользователям из таблицы:

Вы можете выбрать частичные объекты с помощью функции Select следующим образом:

Поле age исключается из ответа следующим образом:

Вы также можете использовать Where следующим образом:

Функция Only возвращает один пользовательский объект, а когда пользовательские объекты не найдены, возвращает NotFoundError.

Когда SQL возвращает несколько пользователей:

us, err := client.User.
   Query().
   Where(user.AgeEQ(21)).
   Only(c.Request().Context())

ent предупредит, что запись не одиночная.

user not singular

Создать край

Далее мы создадим еще один объект и объявим его ребро в схеме.

Во-первых, давайте создадим объект с именем Article.

ent init Artcile

И он сгенерирует article.go в папке схемы.

ent
├── schema
│   ├── article.go
│   └── user.go

Затем мы добавим несколько полей, например:

Предположим, что у User может быть несколько статей, а у статьи есть только один пользователь, поэтому отношение будет one-to-many.

Итак, давайте создадим ребро от User до Artcie.

Сначала добавьте ребро articles к схеме User в ent/schema/user.go:

И запустите его:

go generate ./ent

Когда вы запустите сервер, вы увидите, что таблица articles создана:

Итак, давайте создадим статью и добавим ее в User.

Мы продолжаем использовать конечную точку, /users в main.go, и помещаем такой код:

Функция AddArticles была сгенерирована путем добавления ребра в схему, поэтому мы можем использовать ее для добавления статьи пользователю.

Отправив запрос на http://localhost:8080/users, вы увидите, что запись создана:

Обратный край

Теперь, когда мы можем получить доступ к объекту Article через объект User, но в некоторых случаях вы хотите получить пользователя из Article. Для этого ent предоставляет функцию edge.From.

Итак, давайте добавим обратное ребро с именем users к схеме Article в ent/schema/artcile.go:

Поскольку схема User может иметь более одной ссылки помимо Article, метод Ref описывает, на какое ребро пользовательской схемы ссылается.

И запустить генерацию:

go generate ./ent

Итак, давайте запросим пользователя из объекта статьи:

В журнале консоли вы можете увидеть фактическое выполнение SQL:

SELECT DISTINCT 
 `users`.`id`, 
 `users`.`name`, 
 `users`.`age`, 
 `users`.`created_at` 
FROM 
 `users` 
JOIN 
 (SELECT `articles`.`user_articles` FROM `articles` WHERE `articles`.`id` = ?) AS `t1` 
ON 
 `users`.`id` = `t1`.`user_articles` 
LIMIT 2 args=[1]

И это отвечает на пользовательские данные так:

Нетерпеливая загрузка

ent предоставляет возможность запрашивать объекты с их ассоциациями через Eager loading.

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

И ответ включает данные о статьях в edges :

Выполненный журнал выглядит следующим образом:

SELECT DISTINCT 
 `users`.`id`, 
 `users`.`name`, 
 `users`.`age`, 
 `users`.`created_at` 
FROM 
 `users` 
WHERE 
 `users`.`id` = ? 
LIMIT 
 2
args=[7]
SELECT DISTINCT 
 `articles`.`id`, 
 `articles`.`title`, 
 `articles`.`description`, 
 `articles`.`user_articles` 
FROM 
 `articles` 
WHERE 
 `user_articles` 
IN (?) 
args=[7]

Пользовательский столбец внешнего ключа

При создании ребра ent по умолчанию автоматически создает столбец внешнего ключа.

Например, при создании Article ent создал столбец user_articles как внешний ключ.

Чтобы настроить это имя, вы можете использовать StorageKey в ent/schema/user.go следующим образом:

И запустите go generate ./ent, он добавит столбец user_id в таблицу articles:

ПРИМЕЧАНИЕ: ent делает внешний ключ обнуляемым по умолчанию. После объединения PR вы можете установить NOT NULL в качестве внешнего ключа в качестве требуемого края.

Настроить GraphQL

Далее мы подключим ent к GraphQL, используя gqlgen.

Настроить gqlgen

Во-первых, давайте установим gqlgen:

go get github.com/99designs/gqlgen

Чтобы настроить пакет, выполните эту команду:

go run github.com/99designs/gqlgen init

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

├── gqlgen.yml
├── graph
│   ├── generated
│   │   └── generated.go
│   ├── model
│   │   └── models_gen.go
│   ├── resolver.go
│   ├── schema.graphqls
│   └── schema.resolvers.go
  • gqlgen.yml — Конфиг-файл для управления сгенерированным файлом
  • graph/generated — Пакет для выполнения (автогенерируется)
  • model/models_gen.go — Пакет для всех графовых моделей (автогенерируется)
  • resolver.go — корень преобразователя графа
  • schema.graphqls — файл схемы, который вы можете настроить по своему усмотрению
  • schema.resolvers.go — Реализация резолвера (сгенерирована автоматически)

gqlgen.yml необходимо изменить на ent, поэтому вставьте код:

autobind ищет любые имена типов, совпадающие в папке ent. В этом случае в резолвере будет использоваться ent.User.

Далее мы изменим schema.graphqls на user.graphqls и вставим такой код:

И удалите существующий преобразователь, schema.resolvers.go :

rm -r ./graph/schema.resolvers.go

Запустите генерацию кода gqlgen:

go run github.com/99designs/gqlgen

Итак, каталог проекта должен выглядеть так:

graph
├── generated
│   └── generated.go
├── model
│   └── models_gen.go
├── resolver.go
├── user.graphqls
└── user.resolvers.go

Подключить ent к gqlgen

И расширение GraphQL для ent:

go get entgo.io/contrib/entgql

Чтобы использовать gqlgen в проекте ent, в нашем проекте необходимо включить расширение ent.

Для этого создайте новый файл с именем ent/entc.go и добавьте следующий код:

Затем откройте файл ent/generate.go и измените код на:

package ent

//go:generate go run -mod=mod entc.go

Запустите кодеген:

go generate ./ent

Итак, выполнив эту команду, в наш проект добавятся некоторые надстройки.

Наконец, чтобы использовать преобразователь ent.Client в gelgen, откройте graph/resolver.go и передайте его в схему как дополнительную зависимость:

Запустите сервер GraphQL

Теперь, когда мы готовы запустить сервер GraphQL.

Давайте изменим main.go для настройки сервера GraphQL.

И откройте http://localhost:8080/playground :

Запросить пользователя

Так как преобразователь для пользователя еще не реализован, давайте добавим преобразователь в graph/user.resolvers.go :

Запустите запрос на игровой площадке, и он должен вернуть данные пользователя:

Заключение

Вот и все! Мы реализовали сервер GraphQL, используя пакеты ent и gqlgen. Как видите, ent предоставляет нам действительно полезный API со строго типизированной базой данных.

Gorm был стандартным инструментом ORM для Go и предоставляет полнофункциональный пакет. Что касается статических типов, то gorm берет интерфейс и использует внутреннее отражение, поэтому иногда он теряет свои типы и возникает ошибка времени выполнения.

С другой стороны, ent использует codegen для генерации типов и API, чтобы уведомлять нас о возможных ошибках во время компиляции.

Если вам нужно использовать статически типизированный API и интеграцию с GraphQL, ent будет одним из вариантов для вас.