【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中,

    1. 用于接收SQL查询结果

 

    1. 用于插入的

 

    用于更新的

我定义了三个结构体。我认为只要创建这三个结构体,基本上对任何表格都足够了。在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关联起来…

再见。

广告
将在 10 秒后关闭
bannerAds