用Diesel和PostgreSQL设置Rust API
对象关系映射(ORM)是一种用于存储、检索、更新和删除数据库数据的技术。常见的框架如Node.js有不同的ORM库,可以帮助开发者将他们的应用程序连接到数据库。
简介
ORM帮助我们在应用程序中创建数据模式和关系,这样,每当我们需要改变一个特定的数据库字段时,我们只需在应用程序中用几行代码就可以完成。
ORM帮助我们避免了每次为匹配新的数据库结构而重新创建数据库的繁琐工作。
ORM的概念被许多语言所广泛支持,如Rust、JavaScript和Python。例如,Rust使用Diesel框架来帮助你在Rust应用程序中编写模式查询。
在这篇文章中,我们将了解ORM以及如何在Rust中与Diesel一起使用它。然后,我们将创建一个Rust API服务器,使用Diesel连接到一个PostgreSQL数据库。最后,我们将生成和检索存储在数据库中的应用程序的数据。
这篇文章将帮助读者了解如何在应用程序中使用Rust Diseal框架的ORM。
先决条件
要跟上这篇文章,建议拥有以下工具。
- 有编写Rust API和设置主要Rust[GraphQL服务器]的知识。
- 在你的电脑上安装了[PostgreSQL]。
- 在你的电脑上安装并设置了[Rust编译器]。
设置一个Rust应用程序
Rust使用Cargo来设置和运行其应用程序。
Cargo是一个Rust包管理器,允许我们在应用程序中访问、安装和使用远程库。它和Rust编译器一起被安装。
要设置Rust应用程序,请导航到你想要的位置,并运行以下命令。
cargo new todos-graphql-api
这个命令将创建一个新的目录todos-graphql-api ,里面有一个基本的Rust应用程序。接下来,使用下面的命令导航到这个新创建的目录。
cd todos-graphql-api
我们有一个cargo.toml 文件,包含了当前目录下的项目依赖。在src 文件夹内的main.rs 有一个main 函数,在控制台打印一个Hello, world! 。
你可以通过在todos-graphql-api 目录内运行cargo run 来测试这个功能。
我们的应用程序将使用以下依赖/库。
- [Actix-web]:用于设置基于Rust的HTTP服务器。
- [Diesel]:用于与PostgreSQL交互,作为ORM和查询生成器。
- [Dotenv]:用于加载数据库连接环境变量。
- [Env_logger]:用于记录环境变量。
- [功能]:用于处理Rust HTTP异步调用。
- [Serde]和[Serde_derive]:用于序列化和反序列化Rust数据结构。
- [Serde_json]:用于序列化JSON文件格式。
要使用这些库,请到cargo.toml ,并按以下方式更新依赖关系。
[dependencies]
serde_json = "1.0"
dotenv = "0.9.0"
serde_derive = "1.0"
juniper = "0.13.1"
serde = "1.0"
actix-web = "1.0.0"
diesel = { version = "1.0.0", features = ["postgres"] }
futures = "0.1"
env_logger = "0.6"
设置GraphQL模式
GraphQL模式是由一个根查询和突变组成的。一个查询指定了GraphQL API要返回的数据。
突变与查询类似,可以从GraphQL API中返回数据。例如,突变被用来运行一个将数据写入GraphQL服务器的查询。
我们将设置一个根查询和一个空的突变,它骑在一些假数据上。然后,我们将在教程的后面整合PostgreSQL数据库的动态数据。
在src 文件夹中,创建一个graphql_schema.rs 文件,并从juniper 中导入EmptyMutation 和RootNode 。然后按照以下步骤实现GraphQL模式。
use juniper::{EmptyMutation,RootNode};
通过设置Todo 的字段来定义一个Todo的结构。
struct Todo{
id:i32,
title:String,
completed:bool
}
通过定义每个字段应该返回什么来描述Todo对象。
#[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 completed(&self)->bool{
self.completed
}
}
接下来,定义根查询。
pub struct QueryRoot;
实现根查询以返回一些假的todos。
#[juniper::object]
impl QueryRoot {
fn todos() -> Vec<Todo> {
vec![
Todo{
id:1,
title:"Code in Rust".to_string(),
completed:false
},
Todo{
id:2,
title:"Cook supper meal".to_string(),
completed:false
}
]
}
}
这个片段将创建两个假Todos,如QueryRoot 。接下来,用根查询和空突变初始化模式。
pub type Schema = RootNode<'static, QueryRoot, EmptyMutation<()>>;
定义一个函数来创建模式并执行QueryRoot 和EmptyMutation 。
pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, EmptyMutation::new())
}
设置GraphQL服务器
现在继续main.rs ,并设置HTTP服务器,以确保模式被传递和调用。然而,首先要更新导入的内容,如下所示。
#[macro_use]
extern crate juniper;
use futures::future::Future;
use std::io;
use juniper::http::GraphQLRequest;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use std::sync::Arc;
use juniper::http::graphiql::graphiql_source;
mod graphql_schema;
use graphql_schema::{create_schema, Schema};
更新主函数,如下所示。
fn main() -> io::Result<()> {
let schema = std::sync::Arc::new(create_schema());
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/graphql").route(web::post().to_async(graphql)))
.service(web::resource("/graphiql").route(web::get().to(graphiql)))
})
.bind("localhost:8080")?
.run()
}
这里我们定义了main() ,它返回一个io::Result<()> 类型。接下来,我们调用create_schema() 来初始化GraphQL模式。
HttpServer::new 在 ,这样闭包就可以获得内部变量的所有权,在我们的例子中,这将是模式的一个副本。然后,在 函数中,我们将模式传递给暗示使用它来设置 服务。move data web
/graphql 服务将针对我们的模式运行我们的请求,而/graphiql 服务将作为一个接口,用于发出 GraphQL 请求。
接下来,实现/graphql 服务的处理程序,如下图所示。
fn graphql(
st: web::Data<Arc<Schema>>,
data: web::Json<GraphQLRequest>,
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || {
let res = data.execute(&st, &());
Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
})
.map_err(Error::from)
.and_then(|user| {
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(user))
})
}
上述处理程序在JSON中获取GraphQL请求,从web::block 中创建futures ,并链接两个处理程序。map_err 用于错误状态,and_then 用于成功状态。
实现/graphiql 服务的处理程序,以访问GraphQL API数据。
fn graphiql() -> HttpResponse {
let html = graphiql_source("http://localhost:8080/graphql");
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
这个处理程序本质上是为GraphQL操场生成HTML。到此为止,我们的服务器应该已经准备好进行测试了。要做到这一点,从终端运行以下命令。
cargo run
当构建完成后,在浏览器上导航到http://localhost:8080/graphiql 。你应该收到一个GraphQL操场界面。然后,在左边的窗格中,写下以下查询,以获得todos。
query GetTodos{
todos{
id
title
completed
}
}
点击上面的播放按钮,观察右边窗格中的结果。结果应该与下面的类似。

设置Diesel
Diesel是一个用于Rust应用程序的安全和可扩展的查询生成器。Diesel生成客户端代码,并提供一种交互式的方式来连接到数据库服务器。
Diesel有以下功能。- 表宏,生成数据库表,与SQL数据库中的不同列绑定,你可以查询。- 数据库迁移以保存以前的查询/表。你可以回滚并使用任何先前的迁移。
运行下面的命令,在Postgres功能旁边安装Diesel。
cargo install diesel_cli --no-default-features --features postgres
在项目的根目录下创建一个.env 文件,并设置数据库的URL。
echo DATABASE_URL=postgres://your_username:your_password@localhost/graphql_todos_example > .env
使用下面的命令在项目中设置Diesel。
diesel setup
上面的命令将在项目根目录下创建一个migrations 文件夹。因此,继续生成一个迁移,以创建todos。
diesel migration generate create_todos
这个命令将在migrations 文件夹下生成一个子文件夹,其中包含两个文件。
up.sql:托管用于设置数据库的SQL命令。down.sql:用于关闭数据库的Hosts SQL命令。
在up.sql 内创建一个todos 表,并向该表插入记录,如下图所示。
CREATE TABLE todos(
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
completed BOOLEAN NOT NULL
)
INSERT INTO todos(title,completed) VALUES('Coding in Rust',FALSE);
INSERT INTO todos(title,completed) VALUES('Cooking Supper',FALSE);
在down.sql ,使用下面的SQL语句删除表todos 。
DROP TABLE todos;
创建数据库并运行迁移。
diesel setup --database-url "postgres://your_username:your_password@localhost/graphql_todos_example"
上述命令将在src 目录内创建一个schema.rs 文件,其模式如下。
table! {
todos (id) {
id -> Int4,
title -> Varchar,
completed -> Bool,
}
}
处理查询
我们已经设置了Diseal和todos应用程序工作所需的查询。现在,我们需要处理应用程序将如何访问这些查询。
在graphql_schema.rs ,添加库的导入,如下所示。
extern crate dotenv;
use std::env;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use dotenv::dotenv;
use crate::schema::todos;
创建一个函数来建立数据库连接。
fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
}
在QueryRoot 内使用下面的片段,删除从todos 函数返回的假数据。
impl QueryRoot {
fn todos() -> Vec<Todo> {
}
}
在todos 函数中,启动数据库连接,并使用从模式中生成的todos dsl 从数据库中加载todos。
impl QueryRoot {
fn todos() -> Vec<Todo> {
use crate::schema::todos::dsl::*;
let connection = establish_connection();
let results = todos.load::<Todo>(&connection).expect("Error loading todos");
results
}
}
通过运行下面的命令启动开发服务器。
cargo run
打开浏览器的前一个标签,http://localhost:8080/graphiql ,并运行同样的查询,即。
query GetTodos{
todos{
id
title
completed
}
}
现在你应该收到直接从数据库中获取的todos了。
处理突变
从Diesel导入Insertable 。
use diesel::Insertable;
在QueryRoot 下面声明一个MutationRoot 。
pub struct MutationRoot;
定义一个新todo的结构。
#[derive(juniper::GraphQLInputObject, Insertable)]
#[table_name = "todos"]
pub struct NewTodo {
pub title: String,
pub completed: bool
}
上面的代码块是派生的。
juniper::GraphQLInputObject:用于为GraphQL模式创建一个输入对象。Insertable:用于通知Diesel,它是一个有效的输入,可用于SQL语句。table_name:通知Diesel在哪个表中插入数据。
实现MutationRoot 。
#[juniper::object]
impl MutationRoot {
fn create_todo(new_todo: NewTodo) -> Todo {
use crate::schema::todos::dsl::*;
let connection = establish_connection();
let todo = diesel::insert_into(todos)
.values(&new_todo)
.get_result::<Todo>(&connection)
.expect("Error saving new todo");
todo
}
}
create_todo() 接收新的todo作为参数,建立一个数据库连接,插入todo,并返回新插入的todo。
用MutationRoot 替换模式中的EmptyMutation 。这将帮助我们执行动态突变,而不是我们之前设置的空突变。
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
在create_schema() 函数中替换EmptyMutation ,如下所示。
pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, MutationRoot {});
}
重新运行开发服务器,并在同一个浏览器标签上,然后执行一个类似的GraphQL请求,如下图所示。
mutation CreateTodoMutation($data: NewTodo!) {
createTodo(data: {
"title":"Ride a bike",
"completed":false
}) {
id
title
completed
}
}
点击播放按钮,新添加的todo应该被打印在右侧窗格中。
总结
在本指南中,我们已经学会了如何设置Diesel ORM并将其用于一个理想的Rust应用程序。我们重点介绍了PostgreSQL作为理想的数据库。