如何使用MongoDB和Juniper创建一个GraphQL服务器
GraphQL是一种开源的查询语言。它是一种直观的、为构建API而精心设计的语言。它是围绕着HTTP建立的,以接收来自服务器的资源。
GraphQL给出了一个单一的端点,以确定根据发送到该端点的查询返回什么数据。
因此,它是一种比REST API更灵活的与服务器互动的方式。
REST API是基于执行不同的端点来获得特定的数据,返回不需要的额外信息。
然而,使用GraphQL API,只获得GraphQL查询中指定的数据。这样一来,我们只需向服务器发出一个请求,查询不同的资源并返回所需的数据。
本文旨在通过使用GraphQL、Rust编程语言和MongoDB数据库构建一个全功能的应用程序,向Rust开发者介绍GraphQL的概念。
前提条件
要跟上这篇文章,最好能有以下条件。
设置应用程序
运行以下命令以确认你已经安装了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将有一个title ,description ,和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的实例,并复制模式。 - 克隆模式数据。
- 设置
graphql和graphiql服务。 - 在 localhost 端口
8080上运行服务器。
这些路由将执行graphql 和graphiql 服务。继续前进并定义它们,如下所示。
- 定义
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
}
}
点击中间的播放按钮,使结果可视化,如下图。

在GraphQL操场上写一个突变,添加一个tododo。
mutation CreateTodo{
createTodo(
newTodo:{
title:"Coding in Rust",
description:"Implementing GraphQL and MongoDB",
completed:false
}){
id
title
description
completed
}
}
请随意改变标题和描述。然后,点击中间的播放按钮,观察结果。

现在,下一步将涉及设置数据库。
设置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突变。

- 用MongoDB获取todos查询。

- 获得MongoDB响应的todos。

结论
API为世界上许多应用程序提供动力。因此,API必须有能力提供不同数量的数据。
用GraphQL暴露API,使客户能够以不同的格式访问这些数据。
他们也可以只请求他们需要的信息,这使他们能够更快更灵活地访问API数据。