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

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

# 環境

- Rust: 1.39
- Juniper: 0.14.1
  - [graphql-rust/juniper: GraphQL server library for Rust](https://github.com/graphql-rust/juniper)
- PostgreSQL: 11
- https://github.com/yagince/graphql-sample-rs

```toml: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
```

```Dockerfile
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](https://github.com/davidpdrsn/juniper-from-schema/tree/master/juniper-from-schema)
[husseinraoouf/graphql-actix-example: A compelete example of graphql api built with actix + juniper+ diesel](https://github.com/husseinraoouf/graphql-actix-example)

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

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

## テーブル作成

- User
  - id
  - name

シンプルにUserのデータを入れるテーブルを作成してみます
migrationは[diesel/diesel_cli at master · diesel-rs/diesel](https://github.com/diesel-rs/diesel/tree/master/diesel_cli)を使ってみます。

```shell
$ 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
);
```

```shell
$ diesel migration run
Running migration 2019-11-27-001836_create_users
```

## API書いてみる

```rust:schema.rs
table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}
```

```rust: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,
}
```

```graphql: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!
}
```

```rust: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));
}
```

```rust: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でデータ投入してみます。

```graphql
mutation {
  createUser(name: "hoge") {
    id, name
  }
}
```

```graphql
query {
  users{id, name}
}
```

```json

{
  "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` テーブルを追加してみます。

```shell
$ diesel migration generate create_tags
```

```sql:up.sql
CREATE TABLE tags (
  id   SERIAL  PRIMARY KEY,
  user_id INT  NOT NULL references users(id),
  name VARCHAR NOT NULL
);
```

```sql:down.sql
DROP TABLE tags;
```

```rust: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のスキーマ定義を追加

```diff
--- 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
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
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も登録するように変更

## 確認する

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

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

```graphql
query {
  users{id, name, tags {id, name}}
}
```

```json
{
  "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の設定でログを出す

```shell
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を解決する

- Rustにもdataloader実装はある
  - [cksac/dataloader-rs: Rust implementation of Facebook's DataLoader using futures.](https://github.com/cksac/dataloader-rs)
- juniper専用のdataloader的なライブラリもある
  - [davidpdrsn/juniper-eager-loading: Library for avoiding N+1 query bugs with Juniper](https://github.com/davidpdrsn/juniper-eager-loading)
  - juniper縛りでいいならこっちの方が楽そうではあるので、 **今回はこっちを使います**

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

[juniper-eager-loading/has_many.rs · davidpdrsn/juniper-eager-loading](https://github.com/davidpdrsn/juniper-eager-loading/blob/eeb5309639/examples/has_many.rs)

このexampleを参考にします。

```diff
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
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
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してみる

- 中間にrelationテーブルを持つようなN:M関係になる状況を作ってみる
- [juniper-eager-loading/has_many_through.rs · davidpdrsn/juniper-eager-loading](https://github.com/davidpdrsn/juniper-eager-loading/blob/eeb5309639/examples/has_many_through.rs)
  - ここにexamplesはあるけど、コードを見る限り、３回クエリが走りそうにみえる

## テーブル

```sql: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
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
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
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!
+}
```

```rust: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
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
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")
 }
```

```graphql
mutation {
  createUser(name:"hoge3", tags: ["tag4"], companies: ["Apple", "Amazon"]) {
    id, name, tags { id, name}, companies { id, name}
  }
}
```

いくつか登録

```json
{
  "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して取ってこれないので、１回無駄なクエリを挟んでしまいます。
今回のケースではEmploymentには意味のあるデータを持っていないので、GraphQL上ではEmploymentは登場しません。
ここはjoinして一発で取ってきたいところなので、やってみました。

```diff
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
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
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フレームワーク層でやれそう