4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rust】GraphQL API Serverを作ってみる話。(2/?) ~単一ノードとDB編~

Posted at

はじめに

この記事はこちらの記事の続きです。
ソースコードは前回のものを引き続き使っていくので、まだ見てないよーという方はよろしければ前回記事を先にご覧ください。

こんにちは、「GraphQL API Serverを作ってみる話。」シリーズの第二弾です。
前回は簡易的なQueryMutationを実装し、RustのGraphQLがどんなものなのかをちょっとだけ体験するところまでやりました。
今回はUserオブジェクトを実装し、それをDBに保存したり更新したり削除したり...なんてことをしていきます。

環境

  • OS: Windows 10
  • Rust: 1.60.0-nightly

準備

今回の開発を進めるにあたって、いくつかの準備が必要です。
順番に見ていきましょう。

diesel cliインストール

このシリーズではPostgreSQLでDBを扱っていくので、これは各自でインストールしておいてください。
それが終わったら、便利なCLIをインストールしていきます。
以下のコマンドを実行してください。

cargo install diesel_cli --no-default-features --features postgres

コマンドでは、今回使うORMであるdieselのCLIをインストールしています。
ちなみにdefault-featuresでインストールするとPostgreSQLに加えてMySQL、SQLiteがない方はこわおじに怒られます。
なんでも各SQLが持つライブラリファイルに依存しているらしいです。
なければ自動でダウンロードしてくれてもいいのに...(n敗

特にエラーが出なければ、これでインストールは完了です。

DB作成

DBを扱うにはまずDBを作成する必要があります。
そのための一歩は、.envファイルを修正することです。

.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に対して少し修正を加えます。

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"

これでschema.rsがdieselによって自動生成されるフォルダを変更できました。
この時点でdiesel setup時に生成されたsrc/schema.rsは削除しても大丈夫です。

では次に、マイグレーションファイルを作成します。
以下のコマンドを実行してください。

diesel migration generate users

そうしたら、生成されたmigrations/{timestamp}_users/up.sqlとdown.sqlにマイグレーションを記述していきます。

up.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
);
down.sql
-- This file should undo anything in `up.sql`
drop table users;

セミコロンの付け忘れに十分注意してください(一敗

では、マイグレーションを走らせましょう。

diesel migration run

これでgraphql/src/db/schema.rsが以下のような内容で生成されていれば、ここまでの手順を正しく踏めている証拠です。

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.-単一ノードとDB編」というブランチになります。

Userのスキーマを作る

では早速始めましょう。
まずはUserというGraphQLオブジェクト型を作成します。

schemas/user.rsを作成し、mod.rsを以下のように修正してください。

graphql/src/schemas/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モジュールができたので、ユーザーに関する型を定義していきます。
先ほど作成したuser.rsに、以下のような記述を加えましょう。

graphql/src/schemas/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>,
}

解説

UserのフィールドにいくつかあるNaiveDateTimeですが、これは一見するとスカラー型ではないので後の実装で面倒なことになりそうな気がします。
しかし、JuniperはNaiveDateTimeに関しては組み込みスカラーとしてこれをサポートしているので、他のスカラー型と同様に問題なく使用できます。
詳しくは前回記事をご覧ください。
またNewUserUpdateUserは、今後createUserupdateUserのようなmutationを実装する際の入力型になります。
いわば、引数専用の型ですね。

Userのリゾルバを作る

スキーマが出来上がったので、前回同様次はそれに関するリゾルバを作成します。

resolvers/user.rsを新たに作成し、mod.rsを以下のように修正してください。

graphql/src/resolvers/mod.rs
mod root;
+ mod user;

そうしたら、user.rsにUserのリゾルバを記述していきます。

graphql/src/resolvers/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オブジェクトが使えるのかを確かめるために、Queryのリゾルバに以下のような変更を加えましょう。

graphql/src/resolvers/root.rs
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}
    }
  }
}

解説

user.rsのimplブロックですが、self.clone()祭りで本当にこれでいいのかと思うかもしれません。
私は思いました(鋼の意志
ですが思い出してください。
このリゾルバはそのままUserオブジェクトのフィールドになるんです。
ということは、struct Userで定義したフィールドと全く同じ構成になっていてむしろ当たり前なんですね。
自分はここが罠だなぁと思いました。

DBpoolを確立させる

Userオブジェクトの実装を終え、いよいよDBを弄り倒すときが来ました。
それにはまずコネクションを確立させる必要があります。

DBに関する処理を記述するために、モジュールを増やしましょう。
graphql/src/db/mod.rsとgraphql/src/db/users/mod.rsを新たに作成し、lib.rsを以下のように修正してください。

graphql/src/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には以下のような記述を加えます。

graphql/src/db/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()自体は環境変数からDBのURLを拾い、それをもとにPostgreSQL用のコネクションプールを確立させるという単純な関数です。
しかしその返り値の型がかなり複雑な形になってしまうので、これをPgPoolという型エイリアス(型同義語)で置換してあげるようにしています。

テーブルに関するリポジトリを作成する

準備のセクションではusersテーブルを作成しましたね。
このセクションでは、そのusersテーブルに対して行う処理を記述していきます。

graphql/src/db/users/repository.rsを新たに作成し、mod.rsに以下の記述を追加してください。

graphql/src/db/users/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,
}

これでDBを操作するために必要な型が出来上がりました。

お次はこれらを使って実際にDBを操作する処理を記述します。
先ほど作成したrepository.rsに以下のような記述を加えてください。

graohql/src/db/users/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クエリの結果を受け取る用
  2. INSERT用
  3. UPDATE用

の3つの構造体を定義しています。
基本的にどんなテーブルでもこの3つを作っておけば十分なんじゃないかなぁと思います。
そしてrepository.rsでは、それらを使ってSELECT、INSERT、UPDATE、DELETEを適宜WHEREやINを使用して実装しています。
また、SQLクエリを発行する関数ではdebug!()によってどんなクエリが発行されたのかをdebugログとして出力できるようにしています。
後のN+1問題を解決するために使うので、めんどくせって思った方は思いとどまってください。
ちなみに私は思いとどまれませんでした(デジャヴ

Userに関するリゾルバをGraphQLに実装する

いよいよUserオブジェクトを使うときが来ました。
実装するqueryおよびmutationは以下の通りです。

  • getUser
  • listUser
  • createUser
  • updateUser
  • deleteUser

ですがその前に、少しだけ準備をしておく必要があります。
schemas/user.rsを以下のように修正してください。

graphql/src/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(),
+         }
+     }
+ }

これで、Fromトレイトを実装した任意の型へは.into()によって簡単に変換できるようになりました。

お次はコネクションプールをContextから拾えるようにしましょう。
schemas/root.rs、lib.rs、main.rsを以下のように修正してください。

graphql/src/schemas/root.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;
graphql/src/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;
+ 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
}
src/main.rs
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に加えます。

graphql/src/db/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,
}

お疲れ様です。
これで準備は全て完了しました。

残りのタスクはQueryMutationにリゾルバを実装するだけです。
resolvers/root.rsに最後の変更を加えましょう。

graphql/src/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())
+     }
}

おめでとうございます!
これでgetUserlistUsercreateUserupdateUserdeleteUserの全てをサーバーに実装することができました!
cargo runでサーバーを起動し、graphiqlまたはplaygroundで実際にクエリをリクエストしてみましょう。

解説

ここでのポイントは以下の行です。

graphql/src/resolvers/root.rs
#[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に反映される様々な設定を施すことができます。
例えば今回は、startrangeに対してそれぞれデフォルト引数を設定していますね。
こうすることでクライアントは、引数を入力しないかわりにバックエンド開発者が設定した値を使用することができます。

とりあえず完成!

お疲れ様です!
そしてここまで読んでくださりありがとうございました!
ここまでの手順を正しく踏めていれば、各queryやmutationによって自由にDBを操作できると思います。

記事的な進行度ですが、この記事が第二弾ですので大体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という単一のノードを作成し、それに対応するDBのテーブルを操作することをしました。
次回はもうひとつのオブジェクトとなるPostを作成し、それをGraphQLとDBの両方でUserとの関連付けを行う...というところまでやりたいなぁと思っています。

またね。

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?