如何使用Golang和MongoDB实现GraphQL API服务器

740 阅读12分钟

如何使用Golang和MongoDB实现GraphQL API服务器

GraphQL是一种用于API的查询语言,允许客户端和服务器交换数据。

它允许开发人员比REST和SOAP方法更有效地构建数据驱动的应用程序。

GraphQL通过API给出了详细和可理解的数据表示。它使客户只请求他们需要的数据,而不是整个API数据结构。

这使得随着时间的推移,修改API变得更加容易和快速。它还使开发人员能够获得强大的开发工具,以及对他们如何在其应用程序中使用数据进行更多的控制。

GraphQL可以用不同的语言和数据库实现,如Golang和MongoDB。Go是一种通用语言,意味着它可以用于不同的事情。

例如,你可以用Go来构建网络应用、微服务、云服务、API、DevOps工具和其他应用。这意味着您可能会使用Go来构建基于服务器端的应用程序。

就像Node.js一样,你需要合适的工具来处理服务器端的代码。用Go开发完应用程序后,你可能需要一个数据库来存储数据,比如NoSQL的MongoDB数据库。

MongoDB是一个面向文档的数据库管理系统,归类为NoSQL数据库。它将数据存储在类似JSON的文件中。

在与基于网络的应用程序进行交互时,你可能不知道用户发送的数据格式。因此,像MongoDB这样的NoSQL数据库将是数据处理和存储的良好解决方案。

本指南使用Golang和MongoDB数据库运行一个GraphQL服务器。这个Go GraphQL服务器将使用gqlgen框架来引导GraphQL的模板代码。

先决条件

要完全理解本指南,必须具备以下条件。

  • 编写和运行 Golang 代码的一些基本知识。
  • 关于GraphQL APIs如何工作的基本知识。

什么是gqlgen?

当用Go创建GraphQL API时,你需要首先决定使用哪些库来设置它。

Go有不同的库,可以帮助人们建立一个最小类型安全的服务器。这包括gophers,gqlgen,thundergraphql-go

Gqlgen是一个Go包,允许开发者创建和生成类型安全的GraphQL服务器。Gqlgen使GraphQL服务器的构建变得直观和简单。它增加了一些功能,例如。

模式优先

GraphQL API使用Schema Definition Language(SDL)来定义类型。这允许你描述你希望你的GraphQL端点得到的结果的形状。

因此,你总是能得到一致的API数据,这些数据短小、简明、易于阅读。这就创造了客户端和服务器都能理解的模式重用性。

由于模式定义是第一位的,后端团队可以使用SDL模拟来启动一个查询特定数据的服务器。

这意味着前端团队可以在同时开发服务器的时候开始编写客户端代码。

代码生成

从头开始构建一个GraphQL可能是令人厌烦和耗时的。Gqlgen简化了开发GraphQL APIs的艰巨任务,让你专注于Go应用程序的逻辑。

它生成了一个模板代码,其中有一个随时可以运行的GraphQL例子。这与一些流行的后端框架类似,如React.js、Angular和Svelte。

然而,使用gqlgen使你在构建应用程序时领先一步。这是因为与其他框架相比,它很容易设置。

使用gqlgen设置Go GraphQL服务器

要设置一个Go应用程序,首先需要使用go mod init 来初始化Go模块文件,如下图所示。

go mod init go-graphql-mongodb-api

这将设置一个go.mod 文件,将go-graphql-mongodb-api 作为本地模块。一旦完成,继续使用gqlgen init 命令设置一个GraphQL gqlgen项目。

首先,使用go get github.com/99designs/gqlgen 安装gqlgen。然后通过运行这个命令初始化gqlgen。

go run github.com/99designs/gqlgen init

这将在你的项目根部生成文件和文件夹,其中有一个todosboilerplate GraphQL API。这些生成的文件中有些包括;

  • gqlgen.yml - 这是默认的gqlgen配置文件,它允许gqlgen的依赖项控制gqlgen为你的应用样本创建的代码。

  • graph/generated/generated.go - 它包含了gqlgen生成的大量代码。这使你不用再从头开始写Go GraphQL API了。它还控制GraphQL的执行运行时间。

  • graph/model/models_gen.go - 它包含了引导模板图的todos模型,或者你打算建立的图。

  • graph/schema.graphqls - 你将在这里写出你的图的模式并对其进行设置。

  • graph/schema.resolvers.go - 它执行你的应用逻辑代码,并获得请求想要从你建立的应用程序中获得的数据。

  • server.go - 它定义了你的应用程序的函数。这是一个基本的入口点,提供启动Go服务器所需的HTTP处理程序和路由。

你可以通过在你项目的根目录下运行go run server.go 来测试你生成的文件是否正常。

如果你在浏览器上导航到http://localhost:8080/ ,你将会得到一个GraphQL操场,你可以开始与生成的GraphQL服务器进行交互。

注意:在运行go run server.go 时,你可能会遇到处理程序错误。要解决这个问题,请运行下面的命令并安装处理程序的依赖。

go get github.com/99designs/gqlgen/graphql/handler/transport
go get github.com/99designs/gqlgen/graphql/handler/lru
go get github.com/99designs/gqlgen/graphql/handler/extension

用MongoDB数据库设置GraphQL API

让我们深入进去,创建一个使用MongoDB数据库的GraphQL电影API。从定义电影模式开始。

模式定义了电影API将持有的数据。导航到graph/schema.graphqls 文件,并进行以下修改。

type Movie{
  id: ID!
  name: String!
}

input NewMovie{
  name: String!
}

type Mutation{
  createMovie (input: NewMovie!): Movie!
}

type Query {
  movie(_id: String!): Movie!
  movies: [Movie!]!
}

这将创建一个Movie 类型,其中有一个Query ,用于返回电影。每部电影由电影ID电影名称组成。

该模式还定义了API在创建电影时需要的输入值,以及创建该电影的变异。

现在,你需要根据你刚刚定义的模式,重新生成其余的模板代码。要做到这一点,请运行。

go run github.com/99designs/gqlgen generate

这可能需要你删除CreateTodoTodos 函数。导航到你的graph/schema.resolvers.go 文件,删除上面列出的函数。

这可能还需要你安装以下gqlgen依赖项。

go get github.com/99designs/gqlgen/internal/imports
go get github.com/99designs/gqlgen/internal/code
go get github.com/99designs/gqlgen/cmd

一旦完成,再次运行go run github.com/99designs/gqlgen generate 命令,你的代码模板将被更新以反映新的模式。

你也可以运行go run server.go 来测试重新生成的模板是否有效。

设置MongoDB数据库

你现在有了所有与GraphQL有关的代码。现在我们需要实现处理数据库操作的逻辑。

首先,在你项目的根部创建一个database 目录,并添加一个database.go 文件。

接下来,通过运行下面的命令安装MongoDB Go驱动。

go get go.mongodb.org/mongo-driver/mongo

接下来,按照这些步骤来实现GraphQL MongoDB逻辑。

创建数据库包并导入包。

package database

import (
    "time"
    "fmt"
    "context"
    "log"


    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo/readpref"
    "go.mongodb.org/mongo-driver/mongo"
    "go-graphql-mongodb-api/graph/model"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/bson/primitive"

)

创建一个数据库结构。

type DB struct {
    client *mongo.Client
}

这需要一个client ,类型为mongo.Client

设置MongoDB连接客户端。

func Connect(dbUrl string) *DB {
    client, err := mongo.NewClient(options.Client().ApplyURI(dbUrl))
    if err != nil {
        log.Fatal(err)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    err = client.Ping(ctx, readpref.Primary())
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    err = client.Ping(ctx, readpref.Primary())
    if err != nil {
        log.Fatal(err)
    }

    return &DB{
    client: client,
    }
}

Connect() 函数通过启动后台监控goroutines来初始化 ,以监控部署状态。mongo.Client

NewClient 命令生成一个新的客户端来连接到uri 所指示的部署,这就是mongo.Client

当这种监控在后台发生时,使用client.Ping 方法来验证与mongo.Client 的连接是否被成功创建。

如果启动了一个成功的连接,它将返回与MongoDB客户端连接的DB struct 值。

开始实现必要的方法来执行任何数据库操作,即CRUD操作。你需要这些功能来对数据库执行任何操作,例如创建新的电影或获取现有电影的列表。

向Mongo数据库插入一部电影。

func (db *DB) InsertMovieById(movie model.NewMovie) *model.Movie {
    movieColl := db.client.Database("graphql-mongodb-api-db").Collection("movie")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    inserg, err := movieColl.InsertOne(ctx, bson.D{{Key: "name", Value: movie.Name}})

    if err != nil { 
        log.Fatal(err)
    }

    insertedID := inserg.InsertedID.(primitive.ObjectID).Hex()
    returnMovie := model.Movie{ID: insertedID, Name: movie.Name}

    return &returnMovie
}

这基本上会在数据库中添加一个新的电影对象。MongoDB将首先自动创建一个graphql-mongodb-api-db 数据库和一个movie 集合,用于保存新的电影输入。

每个电影查询都会向创建的集合添加一个带有名称id的电影文件。在这种情况下,id是由MongoDB驱动程序生成的一个值,其类型为primitive.ObjectID ,由一个十六进制字符串创建。

方法InsertMovieById() ,一次只能InsertOne() document。InsertOne() 函数运行一个插入命令,向集合中添加一个单一的文档。它根据插入命令的当前Value ,接受要插入的文档参数name

根据MovieId从MongoDB数据库中获取一个单一的电影。

func (db *DB) FindMovieById(id string) *model.Movie {
    ObjectID, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        log.Fatal(err)
    }

    movieColl := db.client.Database("graphql-mongodb-api-db").Collection("movie")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    res := movieColl.FindOne(ctx, bson.M{"_id": ObjectID})

    movie := model.Movie{ID: id}

    res.Decode(&movie)

    return &movie
}

FindMovieById() 是基于从数据库文档中获取一个单一的结果。

为了使这个操作成功地返回一个单一的文档,你需要将数值primitive.ObjectIDFromHex(id) 与文档_id

这个过程是在后台进行的,如果成功,FindOne() 将执行select 命令,并返回集合中一个文档的单一电影。

在这种情况下,ObjectID 作为包含查询响应的文档的过滤参数。然后用它来选择要返回的文档。

从MongoDB数据库中获取所有添加的电影。

func (db *DB) All() []*model.Movie {
    movieColl := db.client.Database("graphql-mongodb-api-db").Collection("movie")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    cur, err := movieColl.Find(ctx, bson.D{})
    if err != nil {
        log.Fatal(err)
    }

    var movies []*model.Movie
    for cur.Next(ctx) {
        sus, err := cur.Current.Elements()
        fmt.Println(sus)
        if err != nil {
            log.Fatal(err)
        }

        movie := model.Movie{ID: (sus[0].String()), Name: (sus[1].String())}

        movies = append(movies, &movie)
        }

    return movies
}

All() 将获得所有保存在 集合中的电影列表。这基本上会执行一个 ,并返回 集合中的所有匹配文档。movie Find() movie

这些文件将根据它们在集合中的保存方式被获取。Next() ,为这个操作获取下一个文件。

Next() 只有在有文档可用或者 块返回Next() 错误时,才会返回,使得后续的调用为错误

设置查询和突变

GraphQL使用查询来从服务器获取数据。一个查询基本上会指定你希望GraphQL返回的数据。

另一方面,突变与查询类似,可用于从GraphQL API中返回数据。当你想运行一个将数据写入GraphQL服务器的查询时,通常会使用突变。

在这一步,我们需要根据schema.graphqls 文件中定义的查询来设置GraphQL服务器查询。

要做到这一点,请编辑schema.resolvers.go 文件、queryResolver ,以及mutationResolver

导航到schema.resolvers.go 文件,并按如下方式编辑。

由于你正在访问MongoDB数据库,确保你已经导入了database 模块。

import (
"go-graphql-mongodb-api/database"
)

然后,创建以下db 变量。它有连接到MongoDB数据库的连接URL。在你的schema.resolvers.go 文件的末尾添加这个变量。

var db = database.Connect("mongodb://localhost:27017/")

编辑CreateMovie mutationResolver。

CreateMovie() 将一个电影文件添加到MongoDB数据库中。因此,它应该返回 的输入值。InsertMovieById()

这个方法执行一个写入数据的查询。因此,你必须引用一个Mutation() ,以返回generated.MutationResolver 的实现。

下面是你的CreateMovie() 更新后的样子。

func (r *mutationResolver) CreateMovie(ctx context.Context, input model.NewMovie) (*model.Movie, error) {
    return db.InsertMovieById(input), nil
}

编辑Movie queryResolver。

Movie() 从MongoDB数据库中获取一个单一的电影文档。因此,它应该返回 。FindMovieById(id)

Movie() 执行一个读取数据的查询,因此你必须用 来引用它,以返回 实现。下面是你的 更新后的样子。Query() generated.QueryResolver Movie()

func (r *queryResolver) Movie(ctx context.Context, id string) (*model.Movie, error) {
    return db.FindMovieById(id), nil
}

编辑Movies queryResolver。

Movies() 从MongoDB数据库中获取电影文件。因此,它应该返回 的参数。All()

由于Movies() 执行读取数据的查询,你可以将其引用到Query() ,以返回generated.QueryResolver 的实现。下面是你的Movies() 更新后的样子。

func (r *queryResolver) Movies(ctx context.Context) ([]*model.Movie, error) {
    return db.All(), nil
}

导入软件包。

import (
    "net/http"
    "log"
    "os"

    "go-graphql-mongodb-api/database"
    "go-graphql-mongodb-api/graph"
    "go-graphql-mongodb-api/graph/generated"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/rs/cors"
    "github.com/99designs/gqlgen/graphql/playground"  
)

设置Go服务器

为了运行这个GraphQL API,你需要将你创建的方法暴露给localhost,以便使用端点访问它们。要实现这一点,请使用以下步骤。

安装AllowedOrigins CORS包。

go get github.com/rs/cors

导入软件包。

import (
    "net/http"
    "log"
    "os"

    "go-graphql-mongodb-api/database"
    "go-graphql-mongodb-api/graph"
    "go-graphql-mongodb-api/graph/generated"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/rs/cors"
    "github.com/99designs/gqlgen/graphql/playground"  
)

创建一个默认的服务器端口。

const defaultPort = "8080"

创建Go的主函数。

func main() {
    database.Connect("mongodb://localhost:27017/")
    c := cors.New(cors.Options{
        AllowedOrigins: []string{"http://localhost:3000"},
        AllowCredentials: true,
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", c.Handler(srv))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

这将执行MongoDB连接URL,并将其暴露给使用Go核心"net/http" 模块的localhost。

然后你使用 HTTPHandlerResolvers 暴露给服务器。NewExecutableSchema 将从ResolverRoot接口创建一个ExecutableSchema

服务器已经准备好了,你现在可以对它进行测试。导航到你的项目根目录并运行go run server.go

这将在http://localhost:8080/ 上公开 GraphQL API。如果你在浏览器上打开http://localhost:8080/ ,你将会得到一个GraphQL游乐场的服务。

现在你可以开始与GraphQL API互动,测试它是否连接到MongoDB数据库。从创建新的电影项目开始。

在你的GraphQL操场查询面板上添加以下突变,并按下播放按钮

mutation createMovie {
  createMovie(
    input: {
      name: "My test movie title"
    }
  ) {
    name
  }
}

create-movie

这将执行上述查询并返回添加的电影。如果你前往你的MongoDB指南针,将创建一个graphql-mongodb-api-db 数据库和一个movie 收集。

新的电影将作为一个文档被添加到movie 集合中。

mongodb-database

继续并尝试使用上述查询添加更多的电影项目。同时,尝试执行以下的查询。

获得一个单一的电影。

query Movie{
  movie (_id: ":id") {
    id
    name
  }
}

其中:id 是你希望你的GraphQL API返回的电影的_id 值。

获得所有的电影。

query Movies{
  movies {
    id
    name
  }
}

结论

本指南使用gqlgen设置了一个GraphQL API。Gqlgen允许你用一个GraphQL API样本来引导GraphQL代码。

我希望你已经了解到建立一个基本的GraphQL API是多么简单,而不需要从头开始编写整个代码。你可以使用gqlgen将你的GraphQL API连接到其他数据库。