如何使用MongoDB和Juniper创建一个GraphQL服务器

393 阅读10分钟

如何使用MongoDB和Juniper创建一个GraphQL服务器

GraphQL是一种开源的查询语言。它是一种直观的、为构建API而精心设计的语言。它是围绕着HTTP建立的,以接收来自服务器的资源。

GraphQL给出了一个单一的端点,以确定根据发送到该端点的查询返回什么数据。

因此,它是一种比REST API更灵活的与服务器互动的方式。

REST API是基于执行不同的端点来获得特定的数据,返回不需要的额外信息。

然而,使用GraphQL API,只获得GraphQL查询中指定的数据。这样一来,我们只需向服务器发出一个请求,查询不同的资源并返回所需的数据。

本文旨在通过使用GraphQL、Rust编程语言和MongoDB数据库构建一个全功能的应用程序,向Rust开发者介绍GraphQL的概念。

前提条件

要跟上这篇文章,最好能有以下条件。

  • 在你的电脑上安装MongoDB
  • 关于如何使用MongoDB数据库的一些知识。
  • 在你的电脑上安装Rust编译器
  • 一些使用GraphQL、Rust、Juniper和Actix框架的基本知识。

设置应用程序

运行以下命令以确认你已经安装了Rust。

cargo -v

如果你没有得到当前版本的Rust,请考虑在进行本教程之前安装Rust。

接下来,创建一个项目目录,然后启动一个终端,指向创建的项目目录。

最后,运行以下命令来初始化Rust模板项目。

cargo init

通过运行下面的命令测试创建的默认模板项目。

cargo run

该项目将构建,然后记录Hello, world! 文本,暗示你的Rust应用程序已经设置好了。

我们将需要几个依赖项来设置Rust服务器并与MongoDB数据库通信。这些依赖性包括。

  • [Actix-Web框架],用于设置和管理Rust HTTP服务器。
  • [Dotenv]用于连接MongoDB数据库,你需要设置环境变量来承载MongoDB连接参数,如MongoDB连接URL。Dotenv用于将环境变量加载到项目文件。
  • 用于处理对MongoDB的异步调用的[功能]。
  • [MongoDB]。我们需要一个MongoDB驱动程序来在GraphQL服务器和MongoDB数据库之间进行通信。
  • [serde_json]。当向MongoDB发送数据时,我们需要将其序列化为JSON格式。我们将使用serde_json来获取GraphQL API请求,并将被发送的数据转换为JSON格式。
  • [Juniper框架]。这是一个用于Rust编程语言的GraphQL服务器框架。Juniper将帮助我们在Rust中编写一个GraphQL服务器。它为Rust提供类型安全的GraphQL APIs和方便的模式定义。
  • [Tokio]用于处理异步调用。

要使用上述依赖,请确保它们在我们的项目中是可用的。导航到项目根目录下的Cargo.toml 文件,并按如下方式更新。

[dependencies]
juniper = "0.13.1"
dotenv = "0.9.0"
serde_json = "1.0"
actix-web = "1.0.0"
serde = { version = "1.0", features = ["derive"] }
[dependencies.mongodb]
tokio = { version = "0.2", features = ["full"] }
futures = "0.1"
version = "2.1.0"
features = ["sync"]
default-features = false

然后,我们需要通过运行以下命令来安装这些依赖项。

cargo run

设置模式

架构是一个数据对象特定的字段集合。它定义了GraphQL API蓝图。架构还定义了诸如查询和突变等类型。

查询和突变是客户端为访问API数据而提出的请求。一个查询类型设定了API的读取操作。

它通常被称为GET请求,特别是使用REST方法。一个突变类型设置了API的写操作,如POST和PUT请求。

要设置这些类型和字段,请到项目的src 目录中,创建一个schema.rs 文件。这个文件将定义Todo字段、查询、突变,并处理与数据库的连接。

首先,我们需要从juniper ,导入RootNode 。这个模块将帮助我们使用Juniper来编写GraphQL模式。

use juniper::{RootNode};

定义Todo字段的模式。

struct Todo {
    id: i32,
    title: String,
    description: String,
    completed: bool
}

每个Todo项目将有上面的ID、标题、描述完成字段。

为上述todo字段定义一个juniper 对象。

#[juniper::object(description = "A todo")]
impl Todo{
    pub fn id(&self)->i32{
        self.id
    }

    pub fn title(&self)->&str{
        self.title.as_str()
    }

    pub fn description(&self)->&str{
        self.description.as_str()
    }

    pub fn completed(&self)->bool{
        self.completed
    }
}

Todo() 函数定义的对象设置了客户端可以从GraphQL API请求的字段。

定义根查询和一个用于根查询的juniper 对象。现在,我们使用一个带有虚拟数据的查询,并在本指南的后面设置MongoDB的动态数据。

pub struct QueryRoot;

#[juniper::object]
impl QueryRoot{
    fn todos() -> Vec<Todo> {
        vec![
            Todo{
                id:1,
                title:"Watching Basketball".to_string(),
                description:"Watchig the NBA finals".to_string(),
                completed: false
            },
            Todo{
                id:2,
                title:"Watching Football".to_string(),
                description:"Watching the NFL".to_string(),
                completed: false
            },
        ]
    }
}

我们需要定义根突变和使用GraphQLInputObject 添加新todo 的结构。

pub struct MutationRoot;

#[derive(juniper::GraphQLInputObject)]
pub struct NewTodo{
    pub title: String,
    pub description: String,
    pub completed: bool
}

GraphQLInputObject 设置客户端在创建新todo时需要使用的对象。例如,每个新的todo将有一个titledescription ,和completed 。此外,这个对象设置了突变需要的结构,以将数据写入GraphQL服务器。

定义突变的juniper对象。

#[juniper::object]
impl MutationRoot {
    fn create_todo(new_todo: NewTodo) -> Todo {        
        Todo{
            id:1,
            title:new_todo.title,
            description:new_todo.description,
            completed: new_todo.completed
        }        
    }
}

这个对象返回模拟查询所设定的记录。

现在方案已经设置好了,可以执行了。继续前进,定义一个函数来创建方案。

pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;

pub fn create_schema() -> Schema {
    return Schema::new(QueryRoot, MutationRoot);
}

设置路由

要访问任何基于网络的API,我们需要设置路由,这将有助于我们分别发送和接收请求和响应。

这个GraphQL API将有以下两个路由。

  • /graphql:用于执行查询和变异。
  • /graphiql:用于加载GraphQL操场以执行查询和突变。

要设置这些路由,请导航到main.rs ,并开始更新你的模块和依赖关系的导入,如下所示。

#[macro_use]
extern crate juniper;

use std::io;
use std::sync::Arc;

use actix_web::{web, App, Error, HttpResponse, HttpServer};
use futures::future::Future;
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;

mod schema;
use schema::{create_schema, Schema};

main() 函数里面,配置两个路由,如下所示。

fn main() -> io::Result<()> {

// Initialize the graphql schema
let schema = std::sync::Arc::new(create_schema());

// move: to create a copy of the schema
HttpServer::new(move || {
    App::new()

        // clone the schema
        .data(schema.clone())
        .service(web::resource("/graphql").route(web::post().to_async
        
        // service for executing query and mutation requests
        (graphql)))
        .service(web::resource("/graphiql").route(web::get().to
        
        // service for providing an interface to send the requests
        (graphiql))) 
})

// start on port 8080
.bind("localhost:8080")?
.run()
}

从这个main() 函数。

  • 初始化前面定义的GraphQL模式。
  • 创建一个新的HTTPServer 的实例,并复制模式。
  • 克隆模式数据。
  • 设置graphqlgraphiql 服务。
  • 在 localhost 端口8080 上运行服务器。

这些路由将执行graphqlgraphiql 服务。继续前进并定义它们,如下所示。

  • 定义graphql 服务。
fn graphql(
        st: web::Data<Arc<Schema>>,
        data: web::Json<GraphQLRequest>,
    ) -> impl Future<Item = HttpResponse, Error = Error> {

        // Get the GraphQL request in JSON
        web::block(move || {
            let res = data.execute(&st, &());
            Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
        })

        // Error occurred.
        .map_err(Error::from)

        // Successful.
        .and_then(|user| {
            Ok(HttpResponse::Ok()
                .content_type("application/json")
                .body(user))
        })
    }

这个graphql() 函数返回一个异步调用,有成功或错误状态。

首先,它在JSON中获取GraphQL请求并执行它们。然后,如果发生错误,它将它们链接到.map_err ,如果HTTP响应成功,则链接到.and_then

  • 定义graphiql 服务。
fn graphiql() -> HttpResponse {

    // Get the HTML content
    let html = graphiql_source("http://localhost:8080/graphql");
    
    // Render the HTML content
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

graphiql() 函数获得执行GraphQL游戏的HTML内容,并将其渲染到浏览器上。

这个过程创建了一个交互式界面,允许我们执行我们的API查询和突变。

我们现在可以使用下面的Cargo命令来测试开发服务器了。

cargo run

该项目应该运行并在你的本地主机上公开端口8080

现在我们可以使用浏览器中的http://localhost:8080/graphiql 路径来访问GraphQL平台。

在GraphQL操场上写一个查询,得到todos。

query GetTodos {
  todos{
    id
    title
    description
    completed
  }
}

点击中间的播放按钮,使结果可视化,如下图。

mock-mutation

在GraphQL操场上写一个突变,添加一个tododo。

mutation CreateTodo{
  createTodo(
    newTodo:{
      title:"Coding in Rust",
      description:"Implementing GraphQL and MongoDB",
      completed:false
    }){
    id
    title
    description
    completed
  }
}

请随意改变标题描述。然后,点击中间的播放按钮,观察结果。

mock-mutation

现在,下一步将涉及设置数据库。

设置MongoDB数据库

上面的例子使用假数据来运行查询和突变。首先,让我们设置一个MongoDB数据库,用动态数据执行API。

在项目的根部创建一个.env 。在这个文件中,指定MongoDB数据库的URL。这个文件指定了允许应用程序连接到MongoDB的URL

MONGODB_URL="mongodb://localhost:27017/test"

打开schema.rs 文件,导入dotenv,用于访问env 变量、Juniper、Serde和MongoDB驱动组件。

// For RootNode and FieldResult type
use juniper::{RootNode,FieldResult};
// For environment variables
use dotenv::dotenv; 
use mongodb::{
    // doc type
    bson::doc, 
     // synchronous calls
    sync::Client,
};
// serializing and derializing data
use serde::{Serialize,Deserialize}; 

重新定义一个Todo的模式。该模式将反映保存在MongoDB中的动态数据。

#[derive(Debug, Serialize, Deserialize)]
struct Todo {
    title: String,
    description: String,
    completed: bool
}

这个代码片段定义了todo上的Debug,Serialize, 和Deserialize 属性。

由于我们将使用MongoDB ,我们也将删除id 属性。每当我们添加一个新的todo时,MongoDB将自动创建它。

定义一个建立数据库连接的函数。

fn connect_to_db()-> FieldResult<Client> {
    dotenv().ok();
    // Load the database URL.
    let db_url = std::env::var("MONGODB_URL").expect("MONGODB_URL must be set"); 
    // Get the client synchronously.
    let client = Client::with_uri_str(&db_url)?; 
    // return the client.
    return Ok(client); 
}

运行查询

我们需要将我们在本地获取的模拟todos替换为从数据库获取的todos。

因此,我们将对schema.rs 文件做如下修改,并替换掉假的todos数据。

导航到QueryRoot ,并按如下方式编辑todos() 函数。

fn todos() -> FieldResult<Vec<Todo>> {
    // Initialize the database connection
    let client = connect_to_db()?; 
    // Connect to the todos collection
    let collection = client.database("test").collection("todos"); 
    / Get the cursor to loop through the todos
    let cursor = collection.find(None, None).unwrap(); /
    // Iniatialize a mutation to store the todos
    let mut todos = Vec::new(); 
    // Map through the todos from the cursor, adding them to the list
    for result in cursor { 
        todos.push(result?);
    }
    // Return the todos
    return Ok({ 
        todos
    })
 }

运行突变

正如我们所说,突变提供了对GraphQL服务器的写操作。以前,API是返回模拟的todos,但现在我们可以发送新的todos值并将其保存在数据库中。

因此,在schema.rs 文件中,导航到MutationRoot ,然后编辑create_todo() ,如下所示。

fn create_todo(new_todo: NewTodo) -> FieldResult<Todo> {
    // Connect to the database
    let client = connect_to_db()?; 
    // Connect to the collection
    let collection = client.database("todos").collection("todos"); 
    // Instanciate the todo to be saved
    let todo = doc!{ 
        "title": new_todo.title,
        "description": new_todo.description,
        "completed": new_todo.completed
    }; 
    // Save the todo and return the ID
    let result = collection.insert_one(todo, None).unwrap(); 
    // Get the ID
    let id = result.inserted_id.as_object_id().unwrap().to_hex(); 
    // Query for the saved ID
    let inserted_todo = collection.find_one(Some(doc!{"_id": id}), None).unwrap().unwrap(); 
    // Return the saved todo
    return Ok(Todo{ 
        title: inserted_todo.get("title").unwrap().as_str().unwrap().to_string(),
        description: inserted_todo.get("description").unwrap().as_str().unwrap().to_string(),
        completed: inserted_todo.get("completed").unwrap().as_bool().unwrap()
    });     
}

在这个阶段,当一个新的todos被发送时,它将被保存到数据库中。为了测试这一点,使用CTRL + C 停止开发服务器,并使用cargo命令重新启动它。

cargo run

一旦服务器启动并运行,打开GraphQL playgroundhttp://localhost:8080/graphiql ,并运行查询和突变,如下面的图片样本所展示的。

  • 用MongoDB创建一个todo突变。

creating_todo_mutation_with_mongodb

  • 用MongoDB获取todos查询。

getting_todos_query_with_mongodb

  • 获得MongoDB响应的todos。

getting_todos_mongodb_response

结论

API为世界上许多应用程序提供动力。因此,API必须有能力提供不同数量的数据。

用GraphQL暴露API,使客户能够以不同的格式访问这些数据。

他们也可以只请求他们需要的信息,这使他们能够更快更灵活地访问API数据。