GraphQL — это язык запросов для чтения и манипулирования данными для API. Его приоритетом является предоставление клиентам или серверам точных данных, обеспечивая гибкий и интуитивно понятный синтаксис для описания таких данных.
По сравнению с традиционным REST API, GraphQL предоставляет систему типов для описания схем данных и, в свою очередь, дает потребителям API возможность исследовать и запрашивать необходимые данные с помощью одной конечной точки.
В этом посте мы рассмотрим создание приложения для управления проектами на языке Golang с использованием библиотеки gqlgen и MongoDB. В конце этого руководства мы узнаем, как создать конечную точку GraphQL, поддерживающую чтение и манипулирование данными управления проектами, и сохранить наши данные с помощью MongoDB.
Репозиторий GitHub можно найти здесь.
Предварительные условия
Для полного понимания концепций, представленных в этом руководстве, необходим опыт работы с Golang. Опыт работы с MongoDB не является обязательным, но его желательно иметь.
Нам также понадобится следующее:
- Базовые знания GraphQL
- Учетная запись MongoDB для размещения базы данных. Регистрация совершенно бесплатна
Давайте писать
Начало работы
Для начала работы нам необходимо перейти в нужную директорию и выполнить в терминале следующую команду
mkdir project-mngt-golang-graphql && cd project-mngt-golang-graphql
Эта команда создает папку project-mngt-golang-graphql
и переходит в каталог проекта.
Далее нам нужно инициализировать модуль Go для управления зависимостями проекта, выполнив приведенную ниже команду:
go mod init project-mngt-golang-graphql
Эта команда создаст файл go.mod
для отслеживания зависимостей проекта.
Переходим к установке необходимых зависимостей:
go get github.com/99designs/gqlgen go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv
github.com/99designs/gqlgen
— библиотека для создания GraphQL-приложений на Go.
go.mongodb.org/mongo-driver/mongo
— драйвер для подключения к MongoDB.
github.com/joho/godotenv
— библиотека для управления переменными окружения.
Инициализация проекта
Библиотека gqlgen использует подход «сначала схема»; она позволяет нам определять наши API с помощью языка определения схем GraphQL. Библиотека также позволяет нам сосредоточиться на реализации путем генерации шаблона проекта.
Чтобы сгенерировать шаблон проекта, нам нужно выполнить приведенную ниже команду:
go run github.com/99designs/gqlgen init
Приведенная выше команда генерирует следующие файлы:
PS: Мы можем получить ошибку об отсутствующих зависимостях. Мы можем исправить это, переустановив пакеты, которые мы установили ранее.
go get github.com/99designs/gqlgen go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv
Настройка MongoDB
После этого нам нужно войти или зарегистрироваться в нашей учетной записи MongoDB. Щелкните на выпадающем меню проекта и нажмите на кнопку New Project.
Введите projectMngt
в качестве имени проекта, нажмите Next и нажмите Create Project.
Нажмите на кнопку Создать базу данных
Выберите Shared в качестве типа базы данных.
Нажмите кнопку Создать, чтобы настроить кластер. Настройка может занять некоторое время.
Далее нам нужно создать пользователя для внешнего доступа к базе данных, введя имя пользователя, пароль и нажав на кнопку Создать пользователя. Нам также нужно добавить наш IP-адрес для безопасного подключения к базе данных, нажав на кнопку Add My Current IP Address. Затем нажмите на Finish и Close, чтобы сохранить изменения.
После сохранения изменений мы должны увидеть экран Database Deployments, как показано ниже:
Подключение нашего приложения к MongoDB
После завершения конфигурации нам необходимо подключить наше приложение к созданной базе данных. Для этого нажмите на кнопку Connect
Нажмите на Connect your application, измените Driver на Go
и Version, как показано ниже. Затем нажмите на значок копирования, чтобы скопировать строку подключения.
Настройка переменной среды
Далее мы должны изменить скопированную строку подключения с паролем пользователя, который мы создали ранее, и изменить имя базы данных. Для этого сначала нужно создать файл .env
в корневом каталоге, и в этот файл добавить фрагмент, приведенный ниже:
MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
Образец правильно заполненной строки подключения ниже:
MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5ahghkf.mongodb.net/projectMngt?retryWrites=true&w=majority
Загрузка переменной окружения
После этого нам нужно создать вспомогательную функцию для загрузки переменной окружения с помощью библиотеки github.com/joho/godotenv
, которую мы установили ранее. Для этого нам нужно создать папку configs
в корневом каталоге; здесь создайте файл env.go
и добавьте фрагмент ниже:
package configs
import (
"log"
"os"
"github.com/joho/godotenv"
)
func EnvMongoURI() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("MONGOURI")
}
Приведенный выше сниппет делает следующее:
- Импортируйте необходимые зависимости.
- Создайте функцию
EnvMongoURI
, которая проверяет, правильно ли загружена переменная окружения, и возвращает переменную окружения.
Определение нашей схемы
Для этого нужно перейти в папку graph
и в этой папке обновить файл schema.graphqls
, как показано ниже:
type Owner {
_id: String!
name: String!
email: String!
phone: String!
}
type Project {
_id: String!
ownerId: ID!
name: String!
description: String!
status: Status!
}
enum Status {
NOT_STARTED
IN_PROGRESS
COMPLETED
}
input FetchOwner {
id: String!
}
input FetchProject {
id: String!
}
input NewOwner {
name: String!
email: String!
phone: String!
}
input NewProject {
ownerId: ID!
name: String!
description: String!
status: Status!
}
type Query {
owners: [Owner!]!
projects: [Project!]!
owner(input: FetchOwner): Owner!
project(input: FetchProject): Project!
}
type Mutation {
createProject(input: NewProject!): Project!
createOwner(input: NewOwner!): Owner!
}
Приведенный выше фрагмент определяет схему, необходимую для нашего API, создавая два типа: Project
и Owner
. Мы также определяем Query
для выполнения операций над типами, input
для определения свойств создания и Mutation
для создания Project
и Owner
.
Создание логики приложения
Далее нам нужно сгенерировать логику для нашей вновь созданной схемы, используя библиотеку gqlgen
. Для этого в терминале нужно выполнить приведенную ниже команду:
go run github.com/99designs/gqlgen generate
После выполнения приведенной выше команды мы получим ошибку об отсутствии моделей Todo
в файле schema.resolvers.go
; это связано с тем, что мы изменили модель по умолчанию. Мы можем исправить ошибку, удалив функции CreateTodo
и Todo
. После удаления наш код должен выглядеть так, как показано в приведенном ниже фрагменте:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"project-mngt-golang-graphql/graph/generated"
"project-mngt-golang-graphql/graph/model"
)
// CreateProject is the resolver for the createProject field.
func (r *mutationResolver) CreateProject(ctx context.Context, input model.NewProject) (*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// CreateOwner is the resolver for the createOwner field.
func (r *mutationResolver) CreateOwner(ctx context.Context, input model.NewOwner) (*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Owners is the resolver for the owners field.
func (r *queryResolver) Owners(ctx context.Context) ([]*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Projects is the resolver for the projects field.
func (r *queryResolver) Projects(ctx context.Context) ([]*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// Owner is the resolver for the owner field.
func (r *queryResolver) Owner(ctx context.Context, input *model.FetchOwner) (*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Project is the resolver for the project field.
func (r *queryResolver) Project(ctx context.Context, input *model.FetchProject) (*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Создание логики базы данных
Сгенерировав логику GraphQL, нам нужно создать соответствующую логику базы данных. Для этого нам нужно перейти в папку configs
, здесь создать файл db.go
и добавить фрагмент, приведенный ниже:
package configs
import (
"context"
"fmt"
"log"
"project-mngt-golang-graphql/graph/model"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type DB struct {
client *mongo.Client
}
func ConnectDB() *DB {
client, err := mongo.NewClient(options.Client().ApplyURI(EnvMongoURI()))
if err != nil {
log.Fatal(err)
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
//ping the database
err = client.Ping(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to MongoDB")
return &DB{client: client}
}
func colHelper(db *DB, collectionName string) *mongo.Collection {
return db.client.Database("projectMngt").Collection(collectionName)
}
func (db *DB) CreateProject(input *model.NewProject) (*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := collection.InsertOne(ctx, input)
if err != nil {
return nil, err
}
project := &model.Project{
ID: res.InsertedID.(primitive.ObjectID).Hex(),
OwnerID: input.OwnerID,
Name: input.Name,
Description: input.Description,
Status: model.StatusNotStarted,
}
return project, err
}
func (db *DB) CreateOwner(input *model.NewOwner) (*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := collection.InsertOne(ctx, input)
if err != nil {
return nil, err
}
owner := &model.Owner{
ID: res.InsertedID.(primitive.ObjectID).Hex(),
Name: input.Name,
Email: input.Email,
Phone: input.Phone,
}
return owner, err
}
func (db *DB) GetOwners() ([]*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var owners []*model.Owner
defer cancel()
res, err := collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer res.Close(ctx)
for res.Next(ctx) {
var singleOwner *model.Owner
if err = res.Decode(&singleOwner); err != nil {
log.Fatal(err)
}
owners = append(owners, singleOwner)
}
return owners, err
}
func (db *DB) GetProjects() ([]*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var projects []*model.Project
defer cancel()
res, err := collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer res.Close(ctx)
for res.Next(ctx) {
var singleProject *model.Project
if err = res.Decode(&singleProject); err != nil {
log.Fatal(err)
}
projects = append(projects, singleProject)
}
return projects, err
}
func (db *DB) SingleOwner(ID string) (*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var owner *model.Owner
defer cancel()
objId, _ := primitive.ObjectIDFromHex(ID)
err := collection.FindOne(ctx, bson.M{"_id": objId}).Decode(&owner)
return owner, err
}
func (db *DB) SingleProject(ID string) (*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var project *model.Project
defer cancel()
objId, _ := primitive.ObjectIDFromHex(ID)
err := collection.FindOne(ctx, bson.M{"_id": objId}).Decode(&project)
return project, err
}
Приведенный выше фрагмент делает следующее:
- Импортирует необходимые зависимости
- Создает структуру
DB
с полемclient
для доступа к MongoDB. - Создает функцию
ConnectDB
, которая, во-первых, настраивает клиента на использование правильного URI и проверяет его на наличие ошибок. Во-вторых, мы определили таймаут в 10 секунд, который мы хотим использовать при попытке подключения. В-третьих, проверка наличия ошибки при подключении к базе данных и отмена соединения, если период соединения превышает 10 секунд. Наконец, мы пинговали базу данных для проверки нашего соединения и вернули указатель на структуруDB
. - Создает функцию
colHelper
для создания коллекции. - Создает функцию
CreateProject
, которая принимаетDB
struct в качестве приемника указателя и возвращает либо созданныйProject
, либоError
. Внутри функции мы также создали коллекциюproject
, определили таймаут в 10 секунд при вставке данных в коллекцию и использовали функциюInsertOne
для вставкиinput
. - Создали функцию
CreateOwner
, которая принимаетDB
struct в качестве приемника указателя и возвращает либо созданногоOwner
, либоError
. Внутри функции мы также создали коллекциюowner
, определили тайм-аут в 10 секунд при вставке данных в коллекцию и использовали функциюInsertOne
для вставкиinput
. - Создает функцию
GetOwners
, которая принимаетDB
struct в качестве приемника указателя и возвращает либо списокOwners
, либоError
. Функция выполняет предыдущие шаги, получая список владельцев с помощью функцииFind
. Мы также оптимально читаем полученный список, используя метод атрибутаNext
для циклического просмотра возвращенного списка владельцев. - Создается функция
GetProjects
, которая принимает структуруDB
в качестве приемника указателя и возвращает либо списокProjects
, либоError
. Функция выполняет предыдущие шаги, получая список проектов с помощью функцииFind
. Мы также оптимально читаем полученный список, используя метод атрибутаNext
для циклического просмотра возвращенного списка проектов. - Создает функцию
SingleOwner
, которая принимает структуруDB
в качестве приемника указателя и возвращает либо найденногоOwner
с помощью функцииFindOne
, либоError
. - Создает функцию
SingleProject
, которая принимает структуруDB
в качестве приемника указателя и возвращает либо найденныйProject
с помощью функцииFindOne
, либоError
.
Обновление логики приложения
Далее нам необходимо обновить логику приложения с помощью функций базы данных. Для этого нужно обновить файл schema.resolvers.go
, как показано ниже:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"project-mngt-golang-graphql/configs" //add this
"project-mngt-golang-graphql/graph/generated"
"project-mngt-golang-graphql/graph/model"
)
//add this
var (
db = configs.ConnectDB()
)
// CreateProject is the resolver for the createProject field.
func (r *mutationResolver) CreateProject(ctx context.Context, input model.NewProject) (*model.Project, error) {
//modify here
project, err := db.CreateProject(&input)
return project, err
}
// CreateOwner is the resolver for the createOwner field.
func (r *mutationResolver) CreateOwner(ctx context.Context, input model.NewOwner) (*model.Owner, error) {
//modify here
owner, err := db.CreateOwner(&input)
return owner, err
}
// Owners is the resolver for the owners field.
func (r *queryResolver) Owners(ctx context.Context) ([]*model.Owner, error) {
//modify here
owners, err := db.GetOwners()
return owners, err
}
// Projects is the resolver for the projects field.
func (r *queryResolver) Projects(ctx context.Context) ([]*model.Project, error) {
//modify here
projects, err := db.GetProjects()
return projects, err
}
// Owner is the resolver for the owner field.
func (r *queryResolver) Owner(ctx context.Context, input *model.FetchOwner) (*model.Owner, error) {
//modify here
owner, err := db.SingleOwner(input.ID)
return owner, err
}
// Project is the resolver for the project field.
func (r *queryResolver) Project(ctx context.Context, input *model.FetchProject) (*model.Project, error) {
//modify here
project, err := db.SingleProject(input.ID)
return project, err
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Приведенный выше фрагмент делает следующее:
- Импортирует необходимую зависимость
- Создает переменную
db
для инициализации MongoDB с помощью функцииConnectDB
. -
Модифицирует функции
CreateProject
,CreateOwner
,Owners
,Projects
,Owner
, иProject
, используя соответствующую функцию из логики базы данных.Наконец, нам нужно изменить сгенерированные идентификаторы моделей в файле
models_gen.go
с помощью тегов structbson:"_id"
. Мы используем теги struct для переформатирования JSON_id
, возвращаемого MongoDB.
//The remaining part of the code goes here
type FetchOwner struct {
ID string `json:"id" bson:"_id"` //modify here
}
type FetchProject struct {
ID string `json:"id" bson:"_id"` //modify here
}
type NewOwner struct {
//code goes here
}
type NewProject struct {
//code goes here
}
type Owner struct {
ID string `json:"_id" bson:"_id"` //modify here
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
}
type Project struct {
ID string `json:"_id" bson:"_id"` //modify here
OwnerID string `json:"ownerId"`
Name string `json:"name"`
Description string `json:"description"`
Status Status `json:"status"`
}
//The remaining part of the code goes here
После этого мы можем запустить сервер разработки с помощью следующей команды:
go run server.go
Затем перейдите по адресу 127.0.0.1:8080
в веб-браузере.
Мы также можем проверить работу на MongoDB.
Заключение
В этой статье мы рассмотрели, как построить приложение для управления проектами на Golang с использованием библиотеки gqlgen и MongoDB.
Эти ресурсы могут быть полезны:
- Официальная страница GraphQL
- библиотека gqlgen GraphQL
- Драйвер MongoDB Go
- Создание REST API с помощью Golang и MongoDB