【Rust】尝试创建GraphQL API服务器的故事。(2/?) 〜单一节点和数据库篇〜
首先
由于源代码将继续使用之前的内容,所以如果还没有看过的话,请先看前一篇文章。
你好,这是”创建GraphQL API服务器的故事”系列的第二部分。
上次我们实现了简单的查询和突变,并体验了一下Rust的GraphQL是什么样子。
这一次我们将实现用户对象,并对其进行保存、更新和删除等操作。
环境
-
- OS: Windows 10
- Rust: 1.60.0-nightly
备战
咱们继续推进这次的开发前,需要做一些准备工作。我们一起按顺序来看一下吧。
安装 Diesel CLI
因为在这个系列中我们将使用PostgreSQL来处理数据库,所以请各位自行安装它。
安装完成后,我们将继续安装方便的CLI工具。
请执行以下命令。
cargo install diesel_cli --no-default-features --features postgres
在命令行中,我正在安装本次使用的ORM工具diesel的CLI。顺便一提,如果使用默认功能(default-features)进行安装,除了PostgreSQL,如果没有安装MySQL和SQLite,就会被怒气冲冲的可怕的人生叔叔(注:这可能是一个玩笑)训斥。听说每个SQL都依赖于其各自的库文件。如果没有库文件的话,自动下载一下也挺好…(虽然失败n次)
如果没有出现特别的错误,那么安装就完成了。
创建数据库
要处理数据库,首先需要创建数据库。
为此,第一步是修改.env文件。
LOCAL_HOST='localhost'
LOCAL_PORT='8000'
+ DATABASE_URL='postgres://postgres:{your_password}@localhost/rust_graphql'
请将{your_password}部分替换为您自己的密码。
然后,执行以下命令。
diesel setup
我认为这将在根目录中生成diesel.toml和migrations文件夹。
接下来,对生成的 disel.toml 文件进行一些修正。
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
- file = "src/schema.rs"
+ file = "graphql/src/db/schema.rs"
现在可以通过diesel自动修改文件夹来改变schema.rs的路径。
此时可以安全地删除由diesel setup生成的src/schema.rs文件。
接下来,我们将创建迁移文件。
请执行以下命令。
diesel migration generate users
然后,我们将在生成的migrations/{timestamp}_users/up.sql和down.sql文件中编写迁移操作。
-- Your SQL goes here
create table users(
id serial not null primary key,
name varchar(255) not null,
profile text,
created_at timestamp with time zone not null default current_timestamp,
updated_at timestamp with time zone not null default current_timestamp
);
-- This file should undo anything in `up.sql`
drop table users;
请务必注意不要遗忘添加分号(即使只有一次疏忽可能导致失败)。
那么,让我们运行迁移。
diesel migration run
如果graphql/src/db/schema.rs生成的内容是如下所示,那么这说明之前的步骤没有错。
table! {
users (id) {
id -> Int4,
name -> Varchar,
profile -> Nullable<Text>,
created_at -> Timestamptz,
updated_at -> Timestamptz,
}
}
现在准备工作结束了。
有很多自动生成的部分,所以让我们在这里整理一下目录结构。
rust_graphql
|
| .env
│ .gitignore
│ Cargo.lock
│ Cargo.toml
| diesel.toml
│
├─graphql
│ │ .gitignore
│ │ Cargo.toml
│ │
│ ├─src
| | | lib.rs
| | ├─db
| | | schema.rs
| | |
| | ├─resolvers
| | | mod.rs
| | | root.rs
| | |
| | └─schemas
| | mod.rs
| | root.rs
│ │
│ └─target
│
├─migrations
| | .gitkeep
| |
| ├─00000000000000_diesel_initial_setup
| | down.sql
| | up.sql
| |
| └─{timestamp}_users
| down.sql
| up.sql
|
├─src
| main.rs
|
└─target
实施
为了继续上一次的进展,这一次我也在GitHub上准备了一个存储库。
为了方便,我尽量将提交历史分成不同的部分,希望能对你有所帮助。
顺便提一下,这次的分支将是“2.-单节点和数据库版”。
创建用户数据库模式
那么,让我们立即开始吧。
首先,我们将创建一个名为User的GraphQL对象类型。
请创建schemas/user.rs文件,然后将mod.rs文件进行以下修改。
use juniper::EmptySubscription;
pub mod root;
use root::{
Context,
Mutation,
Query,
Schema,
};
+ pub mod user;
pub fn create_schema() -> Schema {
// Schemaオブジェクトを新規に作成する関数.
Schema::new(
Query {},
Mutation {},
EmptySubscription::<Context>::new()
)
}
由于用户模块已经创建,我们将定义与用户有关的类型。
请在刚才创建的user.rs文件中添加以下描述。
use chrono::NaiveDateTime;
use juniper::GraphQLInputObject;
pub struct User {
pub id: i32,
pub name: String,
pub profile: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(GraphQLInputObject)]
pub struct NewUser {
pub name: String,
pub profile: Option<String>,
}
#[derive(GraphQLInputObject)]
pub struct UpdateUser {
pub name: Option<String>,
pub profile: Option<String>,
}
解释
用户字段中有几个NaiveDateTime,乍一看似乎不是标量类型,所以在后续实现中可能会变得麻烦。
然而,Juniper支持NaiveDateTime作为内置标量,因此可以像其他标量类型一样轻松使用。
请参阅上一篇文章以获取详细信息。
另外,NewUser和UpdateUser将成为将来实现像createUser或updateUser这样的mutation时的输入类型。
可以说,它们是用于参数的专用类型。
创建用户的解析器
由于已经创建了模式,所以下一步我们将像上次一样创建相关的解析器。
请创建一个新的文件 resolvers/user.rs,并将 mod.rs 修改如下。
mod root;
+ mod user;
那么,接下来我们会在user.rs中编写用户解析器。
use crate::schemas::{
root::Context,
user::User,
};
use chrono::NaiveDateTime;
use juniper::{
graphql_object,
ID,
};
// オブジェクト型のリゾルバは木構造で言う「葉」になるので、変な処理は入れずに大体簡単なものでいい.
#[graphql_object(context=Context)]
impl User {
fn id(&self) -> ID {
ID::new(self.id.to_string())
}
fn name(&self) -> String {
self.name.clone()
}
fn profile(&self) -> String {
self.profile.clone()
}
// NaiveDateTimeは組み込みスカラー型なので、そのまま返り値にしておk.
fn created_at(&self) -> NaiveDateTime {
self.created_at
}
fn updated_at(&self) -> NaiveDateTime {
self.updated_at
}
}
我已经完成了本次任务的一半。为了确认是否可以实际使用User对象,请在查询解析器中进行以下更改。
use crate::{
schemas::{
root::{
Context,
Mutation,
Query,
},
+ user::User,
},
};
use juniper::{
graphql_object,
};
// 「GraphQLのオブジェクト型」という特徴を付与する.
#[graphql_object(context=Context)]
impl Query {
// 今回は導入編なので、リゾルバも簡易的な感じで.
- fn dummy_query() -> String {
- String::from("It is dummy query.")
+ fn dummy_query() -> User {
+ use chrono::offset::Local;
+
+ // ダミーのUserオブジェクトを返す.
+ User {
+ id: 0,
+ name: "yukarisan-lover".to_string(),
+ profile: "I love yukari-san forever...!".to_string(),
+ created_at: Local::now().naive_local(),
+ updated_at: Local::now().naive_local(),
+ }
}
}
#[graphql_object(context=Context)]
impl Mutation {
fn dummy_mutation() -> String {
String::from("It is dummy mutation.")
}
}
辛苦了!现在已经完成了使用User对象的准备。
让我们执行 “cargo run” 并尝试请求以下查询。
query {
dummyQuery {
id
name
profile
createdAt
updatedAt
}
}
如果您收到以下类似的响应,说明您已正确执行了步骤。
{
"data": {
"dummyQuery": {
"id": "0",
"name": "yukarisan-lover",
"profile": "I love yukari-san forever...!",
"createdAt": {timestamp},
"updatedAt": {timestamp}
}
}
}
解释
用户.rs中的impl块可能会让人觉得在self和.clone()上的交互使用是否正确。我个人认为是这样的(有坚定的意志),但请记住,这个解析器就是User对象的字段。换句话说,它的结构与在struct User中定义的字段完全相同,这实际上是理所当然的。我个人认为这是个陷阱。
建立DB池
用户对象的实现完成了,现在是时候开始操纵数据库了。
为此,首先需要建立连接。
为了描述与数据库相关的处理,我们需要添加模块。
请创建新的graphql/src/db/mod.rs和graphql/src/db/users/mod.rs,并将lib.rs修改如下。
use actix_web::{
Error,
HttpResponse,
web::{
Data,
Payload,
},
};
use juniper_actix::{
graphiql_handler,
graphql_handler,
playground_handler,
};
+ #[macro_use]
+ extern crate diesel;
+ pub mod db;
pub mod resolvers;
pub mod schemas;
use crate::schemas::root::{
Context,
Schema,
};
// Actix WebからGraphQLにアクセスするためのハンドラメソッド.
pub async fn graphql(req: actix_web::HttpRequest, payload: Payload, schema: Data<Schema>) -> Result<HttpResponse, Error> {
// tokenがリクエストヘッダに添付されている場合はSomeを、なければNoneを格納する.
let token = req
.headers()
.get("token")
.map(|t| t.to_str().unwrap().to_string());
let context = Context {
token,
};
graphql_handler(&schema, &context, req, payload).await
}
// Actix WebからGraphiQLにアクセスするためのハンドラメソッド.
pub async fn graphiql() -> Result<HttpResponse, Error> {
graphiql_handler("/graphql", None).await
}
// Actix WebからGraphQL Playgroundにアクセスするためのハンドラメソッド.
pub async fn playground() -> Result<HttpResponse, Error> {
playground_handler("/graphql", None).await
}
在 mod.rs 文件中添加如下描述。
use anyhow::{
Context,
Result,
};
use diesel::{
PgConnection,
r2d2::ConnectionManager,
};
use r2d2::Pool;
use std::env;
mod schema;
pub mod users;
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
pub fn new_pool() -> Result<PgPool> {
let database_url = env::var("DATABASE_URL")?;
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder()
.max_size(15)
.build(manager)
.context("failed to build database pool.")
}
解释
重点是PgPool吗?new_pool()函数本身从环境变量中获取数据库的URL,并以此建立一个适用于PostgreSQL的连接池,这是一个简单的函数。然而,由于它的返回值类型变得非常复杂,所以我们将其替换为一个名为PgPool的类型别名(类型同义词)。
创建一个关于桌子的代码仓库。
在准备阶段,我们创建了一个名为users的数据表。
在这个阶段,我们将描述对该users表执行的操作。
请在graphql/src/db/users/repository.rs中创建一个新文件,并在mod.rs中添加以下描述。
use crate::db::schema::users;
use chrono::NaiveDateTime;
mod repository;
// Identifiable: この構造体がDBのテーブルであることを示す.
// Queryable: この構造体がDBに問い合わせることができることを示す.
// Clone: おまけ.
#[derive(Clone, Identifiable, Queryable)]
pub struct User {
pub id: i32,
pub name: String,
pub profile: Option<String>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
// Insertable: この構造体がDBに新しい行を挿入できることを示す.
#[derive(Insertable)]
#[table_name = "users"]
pub struct UserNewForm {
pub name: String,
pub profile: Option<String>,
}
// AsChangeset: この構造体がDBの任意の行に変更を加えられることを示す.
#[derive(AsChangeset)]
#[table_name = "users"]
pub struct UserUpdateForm {
pub name: Option<String>,
pub profile: Option<String>,
pub updated_at: NaiveDateTime,
}
完成了操作数据库所需的类型。
接下来,我们将使用它们来记录实际操作数据库的处理过程。
请在先前创建的repository.rs文件中加入以下描述:
use crate::db::{
PgPool,
users::{
User,
UserNewForm,
UserUpdateForm,
},
schema::users::dsl::*,
};
use actix_web::web::Data;
use anyhow::Result;
use diesel::{
debug_query,
dsl::{
delete,
insert_into,
update,
},
pg::Pg,
prelude::*,
};
use log::debug;
pub struct Repository;
impl Repository {
// 全てのUserを配列として返す.
pub fn all(pool: &Data<PgPool>) -> Result<Vec<User>> {
let connection = pool.get()?;
Ok(users.load(&connection)?)
}
// primary keyの配列から、これに合致するUserを配列として返す.
pub fn any(pool: &Data<PgPool>, keys: &[i32]) -> Result<Vec<User>> {
let connection = pool.get()?;
let query = users.filter(id.eq_any(keys));
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_results(&connection)?)
}
// key_idに合致するUserを返す.
pub fn find_by_id(pool: &Data<PgPool>, key_id: i32) -> Result<User> {
let connection = pool.get()?;
let query = users.find(key_id);
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_result(&connection)?)
}
// key_nameに合致するUserを配列として返す.
pub fn find_by_name(pool: &Data<PgPool>, key_name: String) -> Result<Vec<User>> {
let connection = pool.get()?;
let query = users.filter(name.eq(key_name));
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_results(&connection)?)
}
// new_formを新しい行としてDBに追加し、その行のUserを返す.
pub fn insert(pool: &Data<PgPool>, new_form: UserNewForm) -> Result<User> {
let connection = pool.get()?;
let query = insert_into(users).values(new_form);
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_result(&connection)?)
}
// key_idに合致するUserの行をupdate_formで更新し、その行のUserを返す.
pub fn update(pool: &Data<PgPool>, key_id: i32, update_form: UserUpdateForm) -> Result<User> {
let connection = pool.get()?;
let query = update(users.find(key_id)).set(update_form);
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_result(&connection)?)
}
// idに合致するUserの行をDBから削除し、その行のUserを返す.
pub fn delete(pool: &Data<PgPool>, key_id: i32) -> Result<User> {
let connection = pool.get()?;
let query = delete(users.find(key_id));
let sql = debug_query::<Pg, _>(&query).to_string();
debug!("{}", sql);
Ok(query.get_result(&connection)?)
}
}
解释
mod.rs中,
-
- 用于接收SQL查询结果
-
- 用于插入的
- 用于更新的
我定义了三个结构体。我认为只要创建这三个结构体,基本上对任何表格都足够了。在repository.rs中,我使用它们来实现SELECT、INSERT、UPDATE和DELETE,并适时使用WHERE和IN。此外,我通过debug!()函数将SQL查询作为调试日志输出,以便查看发出了什么查询。如果你觉得这很麻烦,就请放弃,因为我将用它来解决后面的N+1问题。顺便说一下,我自己没有放弃。
在GraphQL中实现与用户有关的解析器
终于到了使用User对象的时候了。
以下是要实现的查询和变更。
-
- getUser
-
- listUser
-
- createUser
-
- updateUser
- deleteUser
在此之前,需要稍微做些准备。
请将schemas/user.rs的内容修改如下。
+ use crate::db::users;
+ use chrono::{
+ NaiveDateTime,
+ offset::Local,
+ };
use juniper::GraphQLInputObject;
pub struct User {
pub id: i32,
pub name: String,
pub profile: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
+ impl From<users::User> for User {
+ fn from(user: users::User) -> Self {
+ Self {
+ id: user.id,
+ name: user.name,
+ profile: user.profile.unwrap_or_else(|| String::from("")),
+ created_at: user.created_at,
+ updated_at: user.updated_at,
+ }
+ }
+ }
#[derive(GraphQLInputObject)]
pub struct NewUser {
pub name: String,
pub profile: Option<String>,
}
+ impl From<NewUser> for users::UserNewForm {
+ fn from(new_user: NewUser) -> Self {
+ Self {
+ name: new_user.name,
+ profile: new_user.profile,
+ }
+ }
+ }
#[derive(GraphQLInputObject)]
pub struct UpdateUser {
pub name: Option<String>,
pub profile: Option<String>,
}
+ impl From<UpdateUser> for users::UserUpdateForm {
+ fn from(update_user: UpdateUser) -> Self {
+ Self {
+ name: update_user.name,
+ profile: update_user.profile,
+ updated_at: Local::now().naive_local(),
+ }
+ }
+ }
现在,通过.into(),任何实现了From Trait的类型都可以轻松转换。
让我们从上下文中获取连接池。请修改schemas/root.rs、lib.rs和main.rs的代码如下。
+ use crate::db::{
+ PgPool,
+ };
+ use actix_web::web::Data;
use juniper::{
// 今回はSubscriptionを使わないので、ダミーの型を使う必要がある.
EmptySubscription,
RootNode,
};
// 後々ジェネリクスの引数とかに使うので、型をまとめておく.
pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
pub struct Context {
// 今回のシリーズではなんの括約もしないtokenニキ.
pub token: Option<String>,
+ pub pool: Data<PgPool>,
}
// 「GraphQLのコンテキスト」という特徴を付与する.
impl juniper::Context for Context {}
pub struct Query;
pub struct Mutation;
use actix_web::{
Error,
HttpResponse,
web::{
Data,
Payload,
},
};
use juniper_actix::{
graphiql_handler,
graphql_handler,
playground_handler,
};
#[macro_use]
extern crate diesel;
pub mod db;
+ use crate::db::{
+ PgPool,
+ };
pub mod resolvers;
pub mod schemas;
use crate::schemas::root::{
Context,
Schema,
};
// Actix WebからGraphQLにアクセスするためのハンドラメソッド.
- pub async fn graphql(req: actix_web::HttpRequest, payload: Payload, schema: Data<Schema>) -> Result<HttpResponse, Error> {
+ pub async fn graphql(req: actix_web::HttpRequest, payload: Payload, schema: Data<Schema>, pool: Data<PgPool>) -> Result<HttpResponse, Error> {
// tokenがリクエストヘッダに添付されている場合はSomeを、なければNoneを格納する.
let token = req
.headers()
.get("token")
.map(|t| t.to_str().unwrap().to_string());
let context = Context {
token,
+ pool,
};
graphql_handler(&schema, &context, req, payload).await
}
// Actix WebからGraphiQLにアクセスするためのハンドラメソッド.
pub async fn graphiql() -> Result<HttpResponse, Error> {
graphiql_handler("/graphql", None).await
}
// Actix WebからGraphQL Playgroundにアクセスするためのハンドラメソッド.
pub async fn playground() -> Result<HttpResponse, Error> {
playground_handler("/graphql", None).await
}
use actix_cors::Cors;
use actix_web::{
App,
http::header,
HttpServer,
middleware::{
Compress,
Logger,
},
web::{
self,
Data,
},
};
use anyhow::Result;
use dotenv::dotenv;
use graphql::{
+ db::new_pool,
graphiql,
graphql,
playground,
schemas::create_schema,
};
use std::{
env,
sync::Arc,
};
// 今回サーバーの実装にActix Webを使用しているので、非同期ランタイムはactix-rtを採用.
#[actix_rt::main]
async fn main() -> Result<()> {
// .envに記述された環境変数の読み込み.
dotenv().ok();
// debugと同等以上の重要度を持つログを表示するように設定し、ログを開始する.
env::set_var("RUST_LOG", "debug");
env_logger::init();
// Schemaオブジェクトをスレッドセーフな型でホランラップする.
let schema = Arc::new(create_schema());
// PgPoolオブジェクトをスレッドセーフな型でホランラップする.
+ let pool = Arc::new(new_pool()?);
// サーバーの色んな設定.
let mut server = HttpServer::new(move || {
App::new()
// SchemaオブジェクトをActix Webのハンドラメソッドの引数として使えるようにする.
.app_data(Data::from(schema.clone()))
// PgPoolオブジェクトをActix Webのハンドラメソッドの引数として使えるようにする.
+ .app_data(Data::from(pool.clone()))
.wrap(
Cors::default()
.allow_any_origin()
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE)
.supports_credentials()
.max_age(3600),
)
.wrap(Compress::default())
.wrap(Logger::default())
// /graphqlエンドポイントにgraphql()をセットする.
.service(
web::resource("/graphql")
.route(web::get().to(graphql))
.route(web::post().to(graphql)),
)
// /graphiqlエンドポイントにgraphiql()をセットする.
.service(web::resource("/graphiql").route(web::get().to(graphiql)))
// /playgroundエンドポイントにplayground()をセットする.
.service(web::resource("/playground").route(web::get().to(playground)))
});
// Herokuとかにデプロイすることを考えて、HOSTやPORTの環境変数を優先する.
let host = match env::var("HOST") {
Ok(ok) => ok,
Err(_) => env::var("LOCAL_HOST")?,
};
let port = match env::var("PORT") {
Ok(ok) => ok,
Err(_) => env::var("LOCAL_PORT")?,
};
let address = format!("{}:{}", host, port);
server = server.bind(address)?;
server.run().await?;
Ok(())
}
现在可以从Context的字段中获取连接池了。
我会在users/mod.rs中做一些相应改动,以涉及到您的反馈。
use crate::db::schema::users;
use chrono::NaiveDateTime;
mod repository;
+ pub use repository::Repository;
// Identifiable: この構造体がDBのテーブルであることを示す.
// Queryable: この構造体がDBに問い合わせることができることを示す.
// Clone: おまけ.
#[derive(Clone, Identifiable, Queryable)]
pub struct User {
pub id: i32,
pub name: String,
pub profile: Option<String>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
// Insertable: この構造体がDBに新しい行を挿入できることを示す.
#[derive(Insertable)]
#[table_name = "users"]
pub struct UserNewForm {
pub name: String,
pub profile: Option<String>,
}
// AsChangeset: この構造体がDBの任意の行に変更を加えられることを示す.
#[derive(AsChangeset)]
#[table_name = "users"]
pub struct UserUpdateForm {
pub name: Option<String>,
pub profile: Option<String>,
pub updated_at: NaiveDateTime,
}
辛苦了。
一切准备工作都已经完成。
我们只需要在查询和变更中实现解析器,来完成剩下的任务。
让我们在resolvers/root.rs中进行最后的更改。
use crate::{
+ db::users,
schemas::{
root::{
Context,
Mutation,
Query,
},
- user::User,
+ user::{
+ User,
+ NewUser,
+ UpdateUser,
+ },
},
};
use juniper::{
+ FieldResult,
graphql_object,
};
// 「GraphQLのオブジェクト型」という特徴を付与する.
#[graphql_object(context=Context)]
impl Query {
- // 今回は導入編なので、リゾルバも簡易的な感じで.
- fn dummy_query() -> User {
- use chrono::offset::Local;
-
- // ダミーのUserオブジェクトを返す.
- User {
- id: 0,
- name: "yukarisan-lover".to_string(),
- profile: "I love yukari-san forever...!".to_string(),
- created_at: Local::now().naive_local(),
- updated_at: Local::now().naive_local(),
- }
- }
+ fn get_user(context: &Context, id: i32) -> FieldResult<User> {
+ let user = users::Repository::find_by_id(&context.pool, id)?;
+
+ Ok(user.into())
+ }
+ #[graphql(
+ arguments(
+ start(default = 0),
+ range(default = 50),
+ )
+ )]
+ async fn list_user(context: &Context, name: String, start: i32, range: i32) -> FieldResult<Vec<User>> {
+ // asよりも安全に型変換を行う.
+ let start: usize = start.try_into()?;
+ let range: usize = range.try_into()?;
+ let end = start + range;
+
+ let users = users::Repository::find_by_name(&context.pool, name)?;
+
+ // 引数に合わせてベクタをスライスする.
+ let users = match users.len() {
+ n if n > end => users[start..end].to_vec(),
+ n if n > start => users[start..].to_vec(),
+ _ => Vec::new(),
+ };
+
+ Ok(users.into_iter().map(|u| u.into()).collect())
+ }
+ }
#[graphql_object(context=Context)]
impl Mutation {
- fn dummy_mutation() -> String {
- String::from("It is dummy mutation.")
- }
+ fn create_user(context: &Context, new_user: NewUser) -> FieldResult<User> {
+ let user = users::Repository::insert(&context.pool, new_user.into())?;
+
+ Ok(user.into())
+ }
+ fn update_user(context: &Context, id: i32, update_user: UpdateUser) -> FieldResult<User> {
+ let user = users::Repository::update(&context.pool, id, update_user.into())?;
+
+ Ok(user.into())
+ }
+ fn delete_user(context: &Context, id: i32) -> FieldResult<User> {
+ let user = users::Repository::delete(&context.pool, id)?;
+
+ Ok(user.into())
+ }
}
恭喜!现在我们可以在服务器上实现getUser、listUser、createUser、updateUser和deleteUser这五个功能了!运行命令cargo run来启动服务器,并在graphiql或playground上尝试发送实际的查询请求吧。
解释
這裡的重點在於以下的行:
#[graphql(
arguments(
start(default = 0),
range(default = 50),
)
)]
async fn list_user(context: &Context, name: String, start: i32, range: i32) -> FieldResult<Vec<User>> {
// --snip--
}
在这个GraphQL手续式宏中,可以应用各种与GraphQL相关的配置到目标对象上。
例如,在这个例子中,我们针对start和range分别设定了默认参数。
这样一来,客户端就可以使用后端开发人员设置的值,而无需再输入参数了。
暂时完成!
辛苦了!非常感谢您读到这里!如果按照这些步骤正确操作,我认为您可以自由地使用各种查询和变更来操作数据库。
关于文章的进度,由于这是第二篇文章,所以我认为大致完成了1/2或1/3左右。
还剩下的主题是无向图,有向图和N+1问题。
每个主题大概需要用一篇文章来展开。
如果可以的话,请继续支持阅读。
最后,我们将提供最终的目录结构。
rust_graphql
|
| .env
│ .gitignore
│ Cargo.lock
│ Cargo.toml
| diesel.toml
│
├─graphql
│ │ .gitignore
│ │ Cargo.toml
│ │
│ ├─src
| | | lib.rs
| | |
| | ├─db
| | | | mod.rs
| | | | schema.rs
| | | |
| | | └─users
| | | mod.rs
| | | repository.rs
| | |
| | ├─resolvers
| | | mod.rs
| | | root.rs
| | | user.rs
| | |
| | └─schemas
| | mod.rs
| | root.rs
| | user.rs
│ │
│ └─target
│
├─migrations
| | .gitkeep
| |
| ├─00000000000000_diesel_initial_setup
| | down.sql
| | up.sql
| |
| └─{timestamp}_users
| down.sql
| up.sql
|
├─src
| main.rs
|
└─target
最后
我已经创建了一个名为User的单一节点,并对与之对应的数据库表进行了操作。
下次我想创建另一个对象Post,并在GraphQL和数据库中都将其与User关联起来…
再见。