Создание конечной точки управления проектом на базе GraphQL в Golang и MongoDB

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 с помощью тегов struct bson:"_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

Оцените статью
devanswers.ru
Добавить комментарий