LoginSignup
43
28

More than 3 years have passed since last update.

[Rust] juniper + diesel + actix-webでGraphQLしてみる

Last updated at Posted at 2019-11-28

RustでGraphQLのAPIを作るのに軽く調査したメモ

環境

Cargo.toml
[package]
name = "graphql_example"
version = "0.0.1"
authors = ["yagince <straitwalk@gmail.com>"]
edition = "2018"

[dependencies]
actix-web = "1.0.9"
actix-cors = "0.1.0"
juniper = "0.14.1"
juniper-from-schema = "0.5.1"
juniper-eager-loading = "0.5.0"
diesel = { version = "1.4.3", features = ["postgres", "r2d2"] }
r2d2 = "0.8.7"
docker-compose.yml
version: '3.1'

services:
  postgres:
    container_name: postgres
    image: postgres:11
    restart: always
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: dbuser
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - ./tmp/pgdata:/var/lib/postgresql/data
  app:
    container_name: app
    build:
      context: .
    command: cargo run
    volumes:
      - .:/app
    environment:
      - DATABASE_URL=postgres://dbuser:password@postgres:5432/app
    depends_on:
      - postgres
FROM rust:1.39.0-stretch

MAINTAINER yagince <straitwalk@gmail.com>

RUN apt-get -y -q update \
  && apt-get install -y -q \
     libpq-dev \
  && cargo install diesel_cli --no-default-features --features postgres

ENV CARGO_BUILD_TARGET_DIR=/tmp/target

RUN USER=root cargo new --bin app
WORKDIR /app
COPY ./Cargo.* ./

RUN cargo build --color never && \
    rm src/*.rs

参考

juniper-from-schema/juniper-from-schema at master · davidpdrsn/juniper-from-schema
husseinraoouf/graphql-actix-example: A compelete example of graphql api built with actix + juniper+ diesel

やりたいことはほぼこれでした。
juniperのマクロがわかりにくかったので、schemaを定義してマクロで読み込ませてあとはマクロが登場しないのでわかりやすくなった(気がする)

まずは単純に1テーブルだけのデータを返してみる

テーブル作成

  • User
    • id
    • name

シンプルにUserのデータを入れるテーブルを作成してみます
migrationはdiesel/diesel_cli at master · diesel-rs/dieselを使ってみます。

$ diesel setup
Creating migrations directory at: /app/migrations
Creating database: app

$ diesel migration generate create_users
Creating migrations/2019-11-27-001836_create_users/up.sql
Creating migrations/2019-11-27-001836_create_users/down.sql
up.sql
CREATE TABLE users (
  id   SERIAL  PRIMARY KEY,
  name VARCHAR NOT NULL
);
$ diesel migration run
Running migration 2019-11-27-001836_create_users

API書いてみる

schema.rs
table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}
models.rs
use super::schema::users;

#[derive(Queryable)]
pub struct User {
    pub id: i32,
    pub name: String,
}

#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser {
    pub name: String,
}
schema.graphql
schema {
  query: Query
  mutation: Mutation
}

type Query {
  users: [User!]! @juniper(ownership: "owned")
}

type Mutation {
  createUser(
    name: String!,
  ): User! @juniper(ownership: "owned")
}

type User {
  id: ID! @juniper(ownership: "owned")
  name: String!
}
graphql.rs
use std::convert::From;
use std::sync::Arc;

use actix_web::{web, Error, HttpResponse};
use futures01::future::Future;

use juniper::http::playground::playground_source;
use juniper::{http::GraphQLRequest, Executor, FieldResult};
use juniper_from_schema::graphql_schema_from_file;

use diesel::prelude::*;

use itertools::Itertools;

use crate::{DbCon, DbPool};

graphql_schema_from_file!("src/schema.graphql");

pub struct Context {
    db_con: DbCon,
}
impl juniper::Context for Context {}

pub struct Query;
pub struct Mutation;

impl QueryFields for Query {
    fn field_users(
        &self,
        executor: &Executor<'_, Context>,
        _trail: &QueryTrail<'_, User, Walked>,
    ) -> FieldResult<Vec<User>> {
        use crate::schema::users;

        users::table
            .load::<crate::models::User>(&executor.context().db_con)
            .and_then(|users| Ok(users.into_iter().map_into().collect()))
            .map_err(Into::into)
    }
}

impl MutationFields for Mutation {
    fn field_create_user(
        &self,
        executor: &Executor<'_, Context>,
        _trail: &QueryTrail<'_, User, Walked>,
        name: String,
    ) -> FieldResult<User> {
        use crate::schema::users;

        let new_user = crate::models::NewUser { name: name };

        diesel::insert_into(users::table)
            .values(&new_user)
            .get_result::<crate::models::User>(&executor.context().db_con)
            .map(Into::into)
            .map_err(Into::into)
    }
}

pub struct User {
    id: i32,
    name: String,
}

impl UserFields for User {
    fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
        Ok(juniper::ID::new(self.id.to_string()))
    }

    fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
        Ok(&self.name)
    }
}

impl From<crate::models::User> for User {
    fn from(user: crate::models::User) -> Self {
        Self {
            id: user.id,
            name: user.name,
        }
    }
}

fn playground() -> HttpResponse {
    let html = playground_source("");
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

fn graphql(
    schema: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
    db_pool: web::Data<DbPool>,
) -> impl Future<Item = HttpResponse, Error = Error> {
    let ctx = Context {
        db_con: db_pool.get().unwrap(),
    };

    web::block(move || {
        let res = data.execute(&schema, &ctx);
        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))
    })
}

pub fn register(config: &mut web::ServiceConfig) {
    let schema = std::sync::Arc::new(Schema::new(Query, Mutation));

    config
        .data(schema)
        .route("/", web::post().to_async(graphql))
        .route("/", web::get().to(playground));
}
main.rs
#[macro_use]
extern crate diesel;

use actix_cors::Cors;
use actix_web::{web, App, HttpServer};

use diesel::{
    prelude::*,
    r2d2::{self, ConnectionManager},
};

pub mod graphql;
pub mod models;
pub mod schema;

pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub type DbCon = r2d2::PooledConnection<ConnectionManager<PgConnection>>;

fn main() {
    let db_pool = create_db_pool();
    let port: u16 = std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(3000);

    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));

    HttpServer::new(move || {
        App::new()
            .data(db_pool.clone())
            .wrap(Cors::new())
            .configure(graphql::register)
            .default_service(web::to(|| "404"))
    })
    .bind(addr)
    .unwrap()
    .run()
    .unwrap();
}

fn create_db_pool() -> DbPool {
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    r2d2::Pool::builder()
        .max_size(3)
        .build(ConnectionManager::<PgConnection>::new(database_url))
        .expect("failed to create db connection pool")
}

こんな感じで、とりあえずmutationでデータ投入してみます。

mutation {
  createUser(name: "hoge") {
    id, name
  }
}
query {
  users{id, name}
}

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "test"
      },
      {
        "id": "2",
        "name": "test"
      },
      {
        "id": "3",
        "name": "test"
      },
      {
        "id": "4",
        "name": "test"
      },
      {
        "id": "5",
        "name": "hoge"
      }
    ]
  }
}

こんな感じで適当に入れたデータが取れました

1:Nになるような関係のテーブルを追加してみる

  • 関連先のデータを取得してみたいので, 1:N な関係になるテーブルを追加してみます。
    • ここで気になるのは、シンプルなgraphqlの実装だとN+1問題にぶつかる事です
    • dataloader的なものの使い方を調査します

テーブル

Userにタグ付けする感じで tags テーブルを追加してみます。

$ diesel migration generate create_tags
up.sql
CREATE TABLE tags (
  id   SERIAL  PRIMARY KEY,
  user_id INT  NOT NULL references users(id),
  name VARCHAR NOT NULL
);
down.sql
DROP TABLE tags;
schema.rs
table! {
    tags (id) {
        id -> Int4,
        user_id -> Int4,
        name -> Varchar,
    }
}

table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}

joinable!(tags -> users (user_id));

allow_tables_to_appear_in_same_query!(
    tags,
    users,
);

GraphQLのスキーマ定義を追加

--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -16,4 +16,11 @@ type Mutation {
 type User {
   id: ID! @juniper(ownership: "owned")
   name: String!
+  tags: [Tag!]! @juniper(ownership: "owned")
+}
+
+type Tag {
+  id: ID! @juniper(ownership: "owned")
+  userId: ID! @juniper(ownership: "owned")
+  name: String!
 }

GraphQLの実装とモデルを追加

diff --git a/src/graphql.rs b/src/graphql.rs
index c080860..9586231 100644
--- a/src/graphql.rs
+++ b/src/graphql.rs
@@ -71,6 +71,19 @@ impl UserFields for User {
     fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
         Ok(&self.name)
     }
+
+    fn field_tags(
+        &self,
+        executor: &Executor<'_, Context>,
+        _trail: &QueryTrail<'_, Tag, Walked>,
+    ) -> FieldResult<Vec<Tag>> {
+        use crate::schema::tags;
+        tags::table
+            .filter(tags::user_id.eq(&self.id))
+            .load::<crate::models::Tag>(&executor.context().db_con)
+            .and_then(|tags| Ok(tags.into_iter().map_into().collect()))
+            .map_err(Into::into)
+    }
 }

 impl From<crate::models::User> for User {
@@ -82,6 +95,36 @@ impl From<crate::models::User> for User {
     }
 }

+pub struct Tag {
+    id: i32,
+    user_id: i32,
+    name: String,
+}
+
+impl TagFields for Tag {
+    fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
+        Ok(juniper::ID::new(self.id.to_string()))
+    }
+
+    fn field_user_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
+        Ok(juniper::ID::new(self.user_id.to_string()))
+    }
+
+    fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
+        Ok(&self.name)
+    }
+}
+
+impl From<crate::models::Tag> for Tag {
+    fn from(tag: crate::models::Tag) -> Self {
+        Self {
+            id: tag.id,
+            user_id: tag.user_id,
+            name: tag.name,
+        }
+    }
+}
+
 fn playground() -> HttpResponse {
     let html = playground_source("");
     HttpResponse::Ok()
diff --git a/src/models.rs b/src/models.rs
index 91b8447..bc7ea32 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -11,3 +11,10 @@ pub struct User {
 pub struct NewUser {
     pub name: String,
 }
+
+#[derive(Queryable)]
+pub struct Tag {
+    pub id: i32,
+    pub user_id: i32,
+    pub name: String,
+}

User作成時にtagも追加できるようにする

diff --git a/src/graphql.rs b/src/graphql.rs
index 9586231..5aca150 100644
--- a/src/graphql.rs
+++ b/src/graphql.rs
@@ -45,14 +45,26 @@ impl MutationFields for Mutation {
         executor: &Executor<'_, Context>,
         _trail: &QueryTrail<'_, User, Walked>,
         name: String,
+        tags: Vec<String>,
     ) -> FieldResult<User> {
-        use crate::schema::users;
+        use crate::schema::{tags, users};

         let new_user = crate::models::NewUser { name: name };

         diesel::insert_into(users::table)
             .values(&new_user)
             .get_result::<crate::models::User>(&executor.context().db_con)
+            .and_then(|user| {
+                let values = tags
+                    .into_iter()
+                    .map(|tag| (tags::user_id.eq(&user.id), tags::name.eq(tag)))
+                    .collect_vec();
+
+                diesel::insert_into(tags::table)
+                    .values(&values)
+                    .execute(&executor.context().db_con)?;
+                Ok(user)
+            })
             .map(Into::into)
             .map_err(Into::into)
     }
diff --git a/src/schema.graphql b/src/schema.graphql
index b809a73..769e00a 100644
--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -9,7 +9,8 @@ type Query {

 type Mutation {
   createUser(
-    name: String!,
+    name: String!
+    tags: [String!]!
   ): User! @juniper(ownership: "owned")
 }
  • schemaのcreateUserにtagsを追加
  • graphql.rsのcreateUser時にtagsも登録するように変更

確認する

mutation {
  createUser(name:"hoge2", tags: ["tag2", "tag3"]) {
    id, name, tags { id, name}
  }
}

こんな感じでいくつか登録

query {
  users{id, name, tags {id, name}}
}
{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "hoge",
        "tags": [
          {
            "id": "1",
            "name": "tag1"
          }
        ]
      },
      {
        "id": "2",
        "name": "hoge2",
        "tags": [
          {
            "id": "2",
            "name": "tag2"
          },
          {
            "id": "3",
            "name": "tag3"
          }
        ]
      }
    ]
  }
}

いちおう期待通りには取れていそうです。

N+1になっているか確認する

PostgreSQLの設定でログを出す

docker-compose exec postgres bash
root@9f006c0ac3da:/# psql app dbuser
psql (11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.
app=#  ALTER DATABASE app SET log_statement = 'all';
ALTER DATABASE
  • コンテナに入ってpsqlでアクセス
  • ALTER DATABASE app SET log_statement = 'all'; でappテーブルのログを出力する設定にする
  • コンテナを再起動する

ログを見てみる

postgres    | 2019-11-27 14:20:40.779 UTC [32] LOG:  execute __diesel_stmt_0: SELECT "users"."id", "users"."name" FROM "users"
postgres    | 2019-11-27 14:20:40.815 UTC [32] LOG:  execute __diesel_stmt_1: SELECT "tags"."id", "tags"."user_id", "tags"."name" FROM "tags" WHERE "tags"."user_id" = $1
postgres    | 2019-11-27 14:20:40.815 UTC [32] DETAIL:  parameters: $1 = '1'
postgres    | 2019-11-27 14:20:40.819 UTC [32] LOG:  execute __diesel_stmt_1: SELECT "tags"."id", "tags"."user_id", "tags"."name" FROM "tags" WHERE "tags"."user_id" = $1
postgres    | 2019-11-27 14:20:40.819 UTC [32] DETAIL:  parameters: $1 = '2'

user単位でtagsにアクセスしていそうです

N+1を解決する

juniper-eagar-loadingを使って書き換えてみる

juniper-eager-loading/has_many.rs · davidpdrsn/juniper-eager-loading

このexampleを参考にします。

diff --git a/src/graphql.rs b/src/graphql.rs
index 9586231..5dd9d34 100644
--- a/src/graphql.rs
+++ b/src/graphql.rs
@@ -6,13 +6,14 @@ use futures01::future::Future;

 use juniper::http::playground::playground_source;
 use juniper::{http::GraphQLRequest, Executor, FieldResult};
+use juniper_eager_loading::{prelude::*, EagerLoading, HasMany};
 use juniper_from_schema::graphql_schema_from_file;

 use diesel::prelude::*;

 use itertools::Itertools;

-use crate::{DbCon, DbPool};
+use crate::{models, DbCon, DbPool};

 graphql_schema_from_file!("src/schema.graphql");

@@ -28,14 +29,23 @@ impl QueryFields for Query {
     fn field_users(
         &self,
         executor: &Executor<'_, Context>,
-        _trail: &QueryTrail<'_, User, Walked>,
+        trail: &QueryTrail<'_, User, Walked>,
     ) -> FieldResult<Vec<User>> {
         use crate::schema::users;

-        users::table
-            .load::<crate::models::User>(&executor.context().db_con)
-            .and_then(|users| Ok(users.into_iter().map_into().collect()))
-            .map_err(Into::into)
+        let model_users = users::table
+            .load::<models::User>(&executor.context().db_con)
+            .and_then(|users| Ok(users.into_iter().map_into().collect_vec()))?;
+
+        let mut users = User::from_db_models(&model_users);
+        User::eager_load_all_children_for_each(
+            &mut users,
+            &model_users,
+            executor.context(),
+            trail,
+        )?;
+
+        Ok(users)
     }
 }

@@ -43,85 +53,95 @@ impl MutationFields for Mutation {
     fn field_create_user(
         &self,
         executor: &Executor<'_, Context>,
-        _trail: &QueryTrail<'_, User, Walked>,
+        trail: &QueryTrail<'_, User, Walked>,
         name: String,
+        tags: Vec<String>,
     ) -> FieldResult<User> {
-        use crate::schema::users;
+        use crate::schema::{tags, users};

-        let new_user = crate::models::NewUser { name: name };
+        let new_user = models::NewUser { name: name };

-        diesel::insert_into(users::table)
+        let model_user = diesel::insert_into(users::table)
             .values(&new_user)
-            .get_result::<crate::models::User>(&executor.context().db_con)
-            .map(Into::into)
+            .get_result::<models::User>(&executor.context().db_con)
+            .and_then(|user| {
+                let values = tags
+                    .into_iter()
+                    .map(|tag| (tags::user_id.eq(&user.id), tags::name.eq(tag)))
+                    .collect_vec();
+
+                diesel::insert_into(tags::table)
+                    .values(&values)
+                    .execute(&executor.context().db_con)?;
+                Ok(user)
+            })?;
+
+        let user = User::new_from_model(&model_user);
+        User::eager_load_all_children(user, &[model_user], &executor.context(), trail)
             .map_err(Into::into)
     }
 }

+#[derive(Debug, Clone, PartialEq, EagerLoading)]
+#[eager_loading(context = Context, error = diesel::result::Error)]
 pub struct User {
-    id: i32,
-    name: String,
+    user: models::User,
+
+    #[has_many(root_model_field = tag)]
+    tags: HasMany<Tag>,
 }

 impl UserFields for User {
     fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
-        Ok(juniper::ID::new(self.id.to_string()))
+        Ok(juniper::ID::new(self.user.id.to_string()))
     }

     fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
-        Ok(&self.name)
+        Ok(&self.user.name)
     }

     fn field_tags(
         &self,
-        executor: &Executor<'_, Context>,
-        _trail: &QueryTrail<'_, Tag, Walked>,
-    ) -> FieldResult<Vec<Tag>> {
-        use crate::schema::tags;
-        tags::table
-            .filter(tags::user_id.eq(&self.id))
-            .load::<crate::models::Tag>(&executor.context().db_con)
-            .and_then(|tags| Ok(tags.into_iter().map_into().collect()))
-            .map_err(Into::into)
+        _: &Executor<'_, Context>,
+        _: &QueryTrail<'_, Tag, Walked>,
+    ) -> FieldResult<&Vec<Tag>> {
+        self.tags.try_unwrap().map_err(Into::into)
     }
 }

-impl From<crate::models::User> for User {
-    fn from(user: crate::models::User) -> Self {
-        Self {
-            id: user.id,
-            name: user.name,
-        }
+impl juniper_eager_loading::LoadFrom<models::User> for models::Tag {
+    type Error = diesel::result::Error;
+    type Context = Context;
+
+    fn load(
+        users: &[models::User],
+        _field_args: &(),
+        context: &Self::Context,
+    ) -> Result<Vec<models::Tag>, Self::Error> {
+        use crate::schema::tags;
+        tags::table
+            .filter(tags::user_id.eq_any(users.iter().map(|x| x.id).collect_vec()))
+            .load::<models::Tag>(&context.db_con)
     }
 }

+#[derive(Debug, Clone, PartialEq, EagerLoading)]
+#[eager_loading(context = Context, error = diesel::result::Error)]
 pub struct Tag {
-    id: i32,
-    user_id: i32,
-    name: String,
+    tag: models::Tag,
 }

 impl TagFields for Tag {
     fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
-        Ok(juniper::ID::new(self.id.to_string()))
+        Ok(juniper::ID::new(self.tag.id.to_string()))
     }

     fn field_user_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
-        Ok(juniper::ID::new(self.user_id.to_string()))
+        Ok(juniper::ID::new(self.tag.user_id.to_string()))
     }

     fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
-        Ok(&self.name)
-    }
-}
-
-impl From<crate::models::Tag> for Tag {
-    fn from(tag: crate::models::Tag) -> Self {
-        Self {
-            id: tag.id,
-            user_id: tag.user_id,
-            name: tag.name,
-        }
+        Ok(&self.tag.name)
     }
 }
diff --git a/src/models.rs b/src/models.rs
index bc7ea32..9fe73f0 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -1,6 +1,6 @@
 use super::schema::users;

-#[derive(Queryable)]
+#[derive(Queryable, Clone, PartialEq, Debug)]
 pub struct User {
     pub id: i32,
     pub name: String,
@@ -12,7 +12,7 @@ pub struct NewUser {
     pub name: String,
 }

-#[derive(Queryable)]
+#[derive(Queryable, Clone, PartialEq, Debug)]
 pub struct Tag {
     pub id: i32,
     pub user_id: i32,
diff --git a/src/schema.graphql b/src/schema.graphql
index b809a73..a27a7fb 100644
--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -9,14 +9,15 @@ type Query {

 type Mutation {
   createUser(
-    name: String!,
+    name: String!
+    tags: [String!]!
   ): User! @juniper(ownership: "owned")
 }

 type User {
   id: ID! @juniper(ownership: "owned")
   name: String!
-  tags: [Tag!]! @juniper(ownership: "owned")
+  tags: [Tag!]!
 }

 type Tag {

PostgreSQLのログを確認する

postgres    | 2019-11-27 15:02:52.472 UTC [81] LOG:  execute __diesel_stmt_0: SELECT "users"."id", "users"."name" FROM "users"
postgres    | 2019-11-27 15:02:52.490 UTC [81] LOG:  execute <unnamed>: SELECT "tags"."id", "tags"."user_id", "tags"."name" FROM "tags" WHERE "tags"."user_id" IN ($1, $2)
postgres    | 2019-11-27 15:02:52.490 UTC [81] DETAIL:  parameters: $1 = '1', $2 = '2'

HasManyThroughしてみる

テーブル

up.sql
CREATE TABLE companies (
  id   SERIAL  PRIMARY KEY,
  name VARCHAR NOT NULL
);

CREATE TABLE employments (
  id         SERIAL PRIMARY KEY,
  user_id    INT    NOT NULL references users(id),
  company_id INT    NOT NULL references companies(id)
);

モデル

diff --git a/src/models.rs b/src/models.rs
index 9fe73f0..bec0a13 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -18,3 +18,16 @@ pub struct Tag {
     pub user_id: i32,
     pub name: String,
 }
+
+#[derive(Queryable, Clone, PartialEq, Debug)]
+pub struct Company {
+    pub id: i32,
+    pub name: String,
+}
+
+#[derive(Queryable, Clone, PartialEq, Debug)]
+pub struct Employment {
+    pub id: i32,
+    pub user_id: i32,
+    pub company_id: i32,
+}
diff --git a/src/schema.rs b/src/schema.rs
index 72fd8f6..2c7cdea 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,3 +1,18 @@
+table! {
+    companies (id) {
+        id -> Int4,
+        name -> Varchar,
+    }
+}
+
+table! {
+    employments (id) {
+        id -> Int4,
+        user_id -> Int4,
+        company_id -> Int4,
+    }
+}
+
 table! {
     tags (id) {
         id -> Int4,
@@ -13,9 +28,13 @@ table! {
     }
 }

+joinable!(employments -> companies (company_id));
+joinable!(employments -> users (user_id));
 joinable!(tags -> users (user_id));

 allow_tables_to_appear_in_same_query!(
+    companies,
+    employments,
     tags,
     users,
 );

GraphQL

diff --git a/src/schema.graphql b/src/schema.graphql
index a27a7fb..fbd22cf 100644
--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -18,6 +18,7 @@ type User {
   id: ID! @juniper(ownership: "owned")
   name: String!
   tags: [Tag!]!
+  companies: [Company!]!
 }

 type Tag {
@@ -25,3 +26,8 @@ type Tag {
   userId: ID! @juniper(ownership: "owned")
   name: String!
 }
+
+type Company {
+  id: ID! @juniper(ownership: "owned")
+  name: String!
+}
graphql.rs
use std::convert::From;
use std::sync::Arc;

use actix_web::{web, Error, HttpResponse};
use futures01::future::Future;

use juniper::http::playground::playground_source;
use juniper::{http::GraphQLRequest, Executor, FieldResult};
use juniper_eager_loading::{prelude::*, EagerLoading, HasMany, HasManyThrough};
use juniper_from_schema::graphql_schema_from_file;

use diesel::prelude::*;

use itertools::Itertools;

use crate::{models, DbCon, DbPool};

graphql_schema_from_file!("src/schema.graphql");

pub struct Context {
    db_con: DbCon,
}
impl juniper::Context for Context {}

pub struct Query;
pub struct Mutation;

impl QueryFields for Query {
    fn field_users(
        &self,
        executor: &Executor<'_, Context>,
        trail: &QueryTrail<'_, User, Walked>,
    ) -> FieldResult<Vec<User>> {
        use crate::schema::users;

        let model_users = users::table
            .load::<models::User>(&executor.context().db_con)
            .and_then(|users| Ok(users.into_iter().map_into().collect_vec()))?;

        let mut users = User::from_db_models(&model_users);
        User::eager_load_all_children_for_each(
            &mut users,
            &model_users,
            executor.context(),
            trail,
        )?;

        Ok(users)
    }
}

impl MutationFields for Mutation {
    fn field_create_user(
        &self,
        executor: &Executor<'_, Context>,
        trail: &QueryTrail<'_, User, Walked>,
        name: String,
        tags: Vec<String>,
    ) -> FieldResult<User> {
        use crate::schema::{tags, users};

        let new_user = models::NewUser { name: name };

        let model_user = executor.context().db_con.transaction(|| {
            diesel::insert_into(users::table)
                .values(&new_user)
                .get_result::<models::User>(&executor.context().db_con)
                .and_then(|user| {
                    let values = tags
                        .into_iter()
                        .map(|tag| (tags::user_id.eq(&user.id), tags::name.eq(tag)))
                        .collect_vec();

                    diesel::insert_into(tags::table)
                        .values(&values)
                        .execute(&executor.context().db_con)?;
                    Ok(user)
                })
        })?;

        let user = User::new_from_model(&model_user);
        User::eager_load_all_children(user, &[model_user], &executor.context(), trail)
            .map_err(Into::into)
    }
}

#[derive(Debug, Clone, PartialEq, EagerLoading)]
#[eager_loading(context = Context, error = diesel::result::Error)]
pub struct User {
    user: models::User,

    #[has_many(root_model_field = tag)]
    tags: HasMany<Tag>,

    #[has_many_through(join_model = models::Employment)]
    companies: HasManyThrough<Company>,
}

impl UserFields for User {
    fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
        Ok(juniper::ID::new(self.user.id.to_string()))
    }

    fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
        Ok(&self.user.name)
    }

    fn field_tags(
        &self,
        _: &Executor<'_, Context>,
        _: &QueryTrail<'_, Tag, Walked>,
    ) -> FieldResult<&Vec<Tag>> {
        self.tags.try_unwrap().map_err(Into::into)
    }

    fn field_companies(
        &self,
        _: &Executor<'_, Context>,
        _: &QueryTrail<'_, Company, Walked>,
    ) -> FieldResult<&Vec<Company>> {
        self.companies.try_unwrap().map_err(Into::into)
    }
}

#[derive(Debug, Clone, PartialEq, EagerLoading)]
#[eager_loading(context = Context, error = diesel::result::Error)]
pub struct Tag {
    tag: models::Tag,
}

impl TagFields for Tag {
    fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
        Ok(juniper::ID::new(self.tag.id.to_string()))
    }

    fn field_user_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
        Ok(juniper::ID::new(self.tag.user_id.to_string()))
    }

    fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
        Ok(&self.tag.name)
    }
}

#[derive(Debug, Clone, PartialEq, EagerLoading)]
#[eager_loading(context = Context, error = diesel::result::Error)]
pub struct Company {
    company: models::Company,
}

impl CompanyFields for Company {
    fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
        Ok(juniper::ID::new(self.company.id.to_string()))
    }

    fn field_name(&self, _: &Executor<'_, Context>) -> FieldResult<&String> {
        Ok(&self.company.name)
    }
}

impl juniper_eager_loading::LoadFrom<models::User> for models::Tag {
    type Error = diesel::result::Error;
    type Context = Context;

    fn load(
        users: &[models::User],
        _field_args: &(),
        context: &Self::Context,
    ) -> Result<Vec<models::Tag>, Self::Error> {
        use crate::schema::tags;
        tags::table
            .filter(tags::user_id.eq_any(users.iter().map(|x| x.id).collect_vec()))
            .load::<models::Tag>(&context.db_con)
    }
}

impl juniper_eager_loading::LoadFrom<models::Employment> for models::Company {
    type Error = diesel::result::Error;
    type Context = Context;

    fn load(
        employments: &[models::Employment],
        _field_args: &(),
        context: &Self::Context,
    ) -> Result<Vec<models::Company>, Self::Error> {
        use crate::schema::companies;
        companies::table
            .filter(companies::id.eq_any(employments.iter().map(|x| x.company_id).collect_vec()))
            .load::<models::Company>(&context.db_con)
    }
}

impl juniper_eager_loading::LoadFrom<models::User> for models::Employment {
    type Error = diesel::result::Error;
    type Context = Context;

    fn load(
        users: &[models::User],
        _field_args: &(),
        context: &Self::Context,
    ) -> Result<Vec<models::Employment>, Self::Error> {
        use crate::schema::employments;
        employments::table
            .filter(employments::user_id.eq_any(users.iter().map(|x| x.id).collect_vec()))
            .load::<models::Employment>(&context.db_con)
    }
}

fn playground() -> HttpResponse {
    let html = playground_source("");
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

fn graphql(
    schema: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
    db_pool: web::Data<DbPool>,
) -> impl Future<Item = HttpResponse, Error = Error> {
    let ctx = Context {
        db_con: db_pool.get().unwrap(),
    };

    web::block(move || {
        let res = data.execute(&schema, &ctx);
        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))
    })
}

pub fn register(config: &mut web::ServiceConfig) {
    let schema = std::sync::Arc::new(Schema::new(Query, Mutation));

    config
        .data(schema)
        .route("/", web::post().to_async(graphql))
        .route("/", web::get().to(playground));
}

登録できるようにする

diff --git a/src/graphql.rs b/src/graphql.rs
index 8283c78..b1aab68 100644
--- a/src/graphql.rs
+++ b/src/graphql.rs
@@ -56,8 +56,9 @@ impl MutationFields for Mutation {
         trail: &QueryTrail<'_, User, Walked>,
         name: String,
         tags: Vec<String>,
+        companies: Vec<String>,
     ) -> FieldResult<User> {
-        use crate::schema::{tags, users};
+        use crate::schema::{companies, employments, tags, users};

         let new_user = models::NewUser { name: name };

@@ -74,6 +75,32 @@ impl MutationFields for Mutation {
                     diesel::insert_into(tags::table)
                         .values(&values)
                         .execute(&executor.context().db_con)?;
+
+                    companies
+                        .into_iter()
+                        .map(|company_name| {
+                            let company = companies::table
+                                .filter(companies::name.eq(&company_name))
+                                .first::<models::Company>(&executor.context().db_con)
+                                .optional()?;
+
+                            let company = match company {
+                                Some(x) => x,
+                                _ => diesel::insert_into(companies::table)
+                                    .values(companies::name.eq(&company_name))
+                                    .get_result::<models::Company>(&executor.context().db_con)?,
+                            };
+
+                            diesel::insert_into(employments::table)
+                                .values((
+                                    employments::user_id.eq(&user.id),
+                                    employments::company_id.eq(&company.id),
+                                ))
+                                .execute(&executor.context().db_con)?;
+
+                            Ok(company)
+                        })
+                        .collect::<Result<Vec<_>, diesel::result::Error>>()?;
                     Ok(user)
                 })
         })?;
diff --git a/src/schema.graphql b/src/schema.graphql
index fbd22cf..7b3df25 100644
--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -11,6 +11,7 @@ type Mutation {
   createUser(
     name: String!
     tags: [String!]!
+    companies: [String!]!
   ): User! @juniper(ownership: "owned")
 }
mutation {
  createUser(name:"hoge3", tags: ["tag4"], companies: ["Apple", "Amazon"]) {
    id, name, tags { id, name}, companies { id, name}
  }
}

いくつか登録

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "hoge",
        "tags": [
          {
            "id": "1",
            "name": "tag1"
          }
        ],
        "companies": []
      },
      {
        "id": "2",
        "name": "hoge2",
        "tags": [
          {
            "id": "2",
            "name": "tag2"
          },
          {
            "id": "3",
            "name": "tag3"
          }
        ],
        "companies": []
      },
      {
        "id": "3",
        "name": "hoge2",
        "tags": [
          {
            "id": "4",
            "name": "tag2"
          },
          {
            "id": "5",
            "name": "tag3"
          }
        ],
        "companies": [
          {
            "id": "1",
            "name": "Google"
          },
          {
            "id": "2",
            "name": "Amazon"
          }
        ]
      },
      {
        "id": "4",
        "name": "hoge3",
        "tags": [
          {
            "id": "6",
            "name": "tag4"
          }
        ],
        "companies": [
          {
            "id": "3",
            "name": "Apple"
          },
          {
            "id": "2",
            "name": "Amazon"
          }
        ]
      }
    ]
  }
}

取れてそう

PostgreSQLのログを確認する

postgres    | 2019-11-27 15:53:32.015 UTC [161] LOG:  execute __diesel_stmt_1: SELECT "users"."id", "users"."name" FROM "users"
postgres    | 2019-11-27 15:53:32.017 UTC [161] LOG:  execute <unnamed>: SELECT "tags"."id", "tags"."user_id", "tags"."name" FROM "tags" WHERE "tags"."user_id" IN ($1, $2, $3, $4)
postgres    | 2019-11-27 15:53:32.017 UTC [161] DETAIL:  parameters: $1 = '1', $2 = '2', $3 = '3', $4 = '4'
postgres    | 2019-11-27 15:53:32.019 UTC [161] LOG:  execute <unnamed>: SELECT "employments"."id", "employments"."user_id", "employments"."company_id" FROM "employments" WHERE "employments"."user_id" IN ($1, $2, $3, $4)
postgres    | 2019-11-27 15:53:32.019 UTC [161] DETAIL:  parameters: $1 = '1', $2 = '2', $3 = '3', $4 = '4'
postgres    | 2019-11-27 15:53:32.021 UTC [161] LOG:  execute <unnamed>: SELECT "companies"."id", "companies"."name" FROM "companies" WHERE "companies"."id" IN ($1, $2, $3, $4)
postgres    | 2019-11-27 15:53:32.021 UTC [161] DETAIL:  parameters: $1 = '1', $2 = '2', $3 = '3', $4 = '2'

users -> employments -> companies と別々にクエリを投げているのがわかります

HasManyThroughな関係をjoinしてHasManyで取得する

HasManyThoughではjoinして取ってこれないので、1回無駄なクエリを挟んでしまいます。
今回のケースではEmploymentには意味のあるデータを持っていないので、GraphQL上ではEmploymentは登場しません。
ここはjoinして一発で取ってきたいところなので、やってみました。

diff --git a/src/graphql.rs b/src/graphql.rs
index b1aab68..c9decf7 100644
--- a/src/graphql.rs
+++ b/src/graphql.rs
@@ -6,7 +6,7 @@ use futures01::future::Future;

 use juniper::http::playground::playground_source;
 use juniper::{http::GraphQLRequest, Executor, FieldResult};
-use juniper_eager_loading::{prelude::*, EagerLoading, HasMany, HasManyThrough};
+use juniper_eager_loading::{prelude::*, EagerLoading, HasMany};
 use juniper_from_schema::graphql_schema_from_file;

 use diesel::prelude::*;
@@ -119,8 +119,8 @@ pub struct User {
     #[has_many(root_model_field = tag)]
     tags: HasMany<Tag>,

-    #[has_many_through(join_model = models::Employment)]
-    companies: HasManyThrough<Company>,
+    #[has_many(root_model_field = company)]
+    companies: HasMany<CompanyWithUser>,
 }

 impl UserFields for User {
@@ -143,8 +143,8 @@ impl UserFields for User {
     fn field_companies(
         &self,
         _: &Executor<'_, Context>,
-        _: &QueryTrail<'_, Company, Walked>,
-    ) -> FieldResult<&Vec<Company>> {
+        _: &QueryTrail<'_, CompanyWithUser, Walked>,
+    ) -> FieldResult<&Vec<CompanyWithUser>> {
         self.companies.try_unwrap().map_err(Into::into)
     }
 }
@@ -171,11 +171,11 @@ impl TagFields for Tag {

 #[derive(Debug, Clone, PartialEq, EagerLoading)]
 #[eager_loading(context = Context, error = diesel::result::Error)]
-pub struct Company {
-    company: models::Company,
+pub struct CompanyWithUser {
+    company: models::CompanyWithUser,
 }

-impl CompanyFields for Company {
+impl CompanyWithUserFields for CompanyWithUser {
     fn field_id(&self, _: &Executor<'_, Context>) -> FieldResult<juniper::ID> {
         Ok(juniper::ID::new(self.company.id.to_string()))
     }
@@ -201,23 +201,7 @@ impl juniper_eager_loading::LoadFrom<models::User> for models::Tag {
     }
 }

-impl juniper_eager_loading::LoadFrom<models::Employment> for models::Company {
-    type Error = diesel::result::Error;
-    type Context = Context;
-
-    fn load(
-        employments: &[models::Employment],
-        _field_args: &(),
-        context: &Self::Context,
-    ) -> Result<Vec<models::Company>, Self::Error> {
-        use crate::schema::companies;
-        companies::table
-            .filter(companies::id.eq_any(employments.iter().map(|x| x.company_id).collect_vec()))
-            .load::<models::Company>(&context.db_con)
-    }
-}
-
-impl juniper_eager_loading::LoadFrom<models::User> for models::Employment {
+impl juniper_eager_loading::LoadFrom<models::User> for models::CompanyWithUser {
     type Error = diesel::result::Error;
     type Context = Context;

@@ -225,11 +209,21 @@ impl juniper_eager_loading::LoadFrom<models::User> for models::Employment {
         users: &[models::User],
         _field_args: &(),
         context: &Self::Context,
-    ) -> Result<Vec<models::Employment>, Self::Error> {
-        use crate::schema::employments;
-        employments::table
-            .filter(employments::user_id.eq_any(users.iter().map(|x| x.id).collect_vec()))
-            .load::<models::Employment>(&context.db_con)
+    ) -> Result<Vec<models::CompanyWithUser>, Self::Error> {
+        use crate::schema::{companies, employments, users};
+        companies::table
+            .inner_join(employments::table.inner_join(users::table))
+            .filter(users::id.eq_any(users.iter().map(|x| x.id).collect_vec()))
+            .load::<(models::Company, (models::Employment, models::User))>(&context.db_con)
+            .map(|data| {
+                data.into_iter()
+                    .map(|(company, (_, user))| models::CompanyWithUser {
+                        id: company.id,
+                        user_id: user.id,
+                        name: company.name,
+                    })
+                    .collect_vec()
+            })
     }
 }
diff --git a/src/models.rs b/src/models.rs
index bec0a13..43e2077 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -25,6 +25,13 @@ pub struct Company {
     pub name: String,
 }

+#[derive(Queryable, Clone, PartialEq, Debug)]
+pub struct CompanyWithUser {
+    pub id: i32,
+    pub user_id: i32,
+    pub name: String,
+}
+
 #[derive(Queryable, Clone, PartialEq, Debug)]
 pub struct Employment {
     pub id: i32,
diff --git a/src/schema.graphql b/src/schema.graphql
index 7b3df25..0700cdd 100644
--- a/src/schema.graphql
+++ b/src/schema.graphql
@@ -19,7 +19,7 @@ type User {
   id: ID! @juniper(ownership: "owned")
   name: String!
   tags: [Tag!]!
-  companies: [Company!]!
+  companies: [CompanyWithUser!]!
 }

 type Tag {
@@ -28,7 +28,7 @@ type Tag {
   name: String!
 }

-type Company {
+type CompanyWithUser {
   id: ID! @juniper(ownership: "owned")
   name: String!
 }

やったことはざっと以下のような感じです。

  • LoadFromでUser->Companyを引けるようにした
  • Companyをeager_laodした後にuserに振り分けるために、Companyにuser_idを持っている必要があるようだったので、CompanyWithUserという新しい入れ物を作った
  • juniper_eager_loadではGraphQLのtype名とモデルの名前が一致している必要があるようなので、GraphQLの定義を変更した

PostgreSQLのログを確認する

postgres    | 2019-11-27 16:22:39.223 UTC [198] LOG:  execute __diesel_stmt_0: SELECT "users"."id", "users"."name" FROM "users"
postgres    | 2019-11-27 16:22:39.228 UTC [198] LOG:  execute <unnamed>: SELECT "tags"."id", "tags"."user_id", "tags"."name" FROM "tags" WHERE "tags"."user_id" IN ($1, $2, $3, $4)
postgres    | 2019-11-27 16:22:39.228 UTC [198] DETAIL:  parameters: $1 = '1', $2 = '2', $3 = '3', $4 = '4'
postgres    | 2019-11-27 16:22:39.237 UTC [198] LOG:  execute <unnamed>: SELECT "companies"."id", "companies"."name", "employments"."id", "employments"."user_id", "employments"."company_id", "users"."id", "users"."name" FROM ("companies" INNER JOIN ("employments" INNER JOIN "users" ON "employments"."user_id" = "users"."id") ON "employments"."company_id" = "companies"."id") WHERE "users"."id" IN ($1, $2, $3, $4)
postgres    | 2019-11-27 16:22:39.237 UTC [198] DETAIL:  parameters: $1 = '1', $2 = '2', $3 = '3', $4 = '4'

1個クエリが減りました。

まとめ

  • GraphQL便利
    • 単一エンドポイントになるので、パフォーマンスモニタリングなど、やりにくい部分はありそう
  • eager_loadはできる
    • juniper_eager_loadはいろいろ制約がある
    • モデルはmodelsというmoduleにしなきゃいけない
    • モデル名とGraphQLのtype名は同じである必要がある
  • ベンチマークとってみたい
  • 認証周りはactix-webなど、webフレームワーク層でやれそう
43
28
0

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
43
28