尝试使用[Rust] juniper + diesel + actix-web进行GraphQL

用Rust来构建GraphQL API进行了一项简要调查的笔记。

环境

    • Rust: 1.39

 

    • Juniper: 0.14.1

graphql-rust/juniper: GraphQL server library for Rust

PostgreSQL: 11
https://github.com/yagince/graphql-sample-rs

[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"
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在davidpdrsn/juniper-from-schema的主分支上
husseinraoouf/graphql-actix-example: 使用actix + juniper + diesel构建的GraphQL API的完整示例

我所想做的事情基本上就是这样了。因为我对Juniper的宏不太理解,所以我定义了一个模式来加载宏,然后就不再用到宏了,这样就变得容易理解了(至少我感觉是这样的)。

首先,我們試著僅返回一個表格的簡單資料。

创建表格

    • User

id
name

我尝试创建一个简单的表格来存储用户数据,我将使用diesel-rs/diesel的migration工具进行操作。

$ 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
CREATE TABLE users (
  id   SERIAL  PRIMARY KEY,
  name VARCHAR NOT NULL
);
$ diesel migration run
Running migration 2019-11-27-001836_create_users

我试着写API。

table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}
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 {
  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!
}
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));
}
#[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")
}

我会先试试通过变异(mutations)的方式插入数据,就这样。

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"
      }
    ]
  }
}

我以适当的方式输入数据,就取到了这样的数据。

试着添加一个使得关系表变为N的选项。

    • 関連先のデータを取得してみたいので, 1:N な関係になるテーブルを追加してみます。

ここで気になるのは、シンプルなgraphqlの実装だとN+1問題にぶつかる事です
dataloader的なものの使い方を調査します

桌子

我会像给用户打标签一样,尝试添加一个 tags 表格。

$ diesel migration generate create_tags
CREATE TABLE tags (
  id   SERIAL  PRIMARY KEY,
  user_id INT  NOT NULL references users(id),
  name VARCHAR NOT NULL
);
DROP TABLE tags;
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,
+}

在用户创建时允许添加标签。

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'

根据用户单位,似乎正在访问标签。

解决N+1的问题

    • Rustにもdataloader実装はある

cksac/dataloader-rs: Rust implementation of Facebook’s DataLoader using futures.

juniper専用のdataloader的なライブラリもある

davidpdrsn/juniper-eager-loading: Library for avoiding N+1 query bugs with Juniper
juniper縛りでいいならこっちの方が楽そうではあるので、 今回はこっちを使います

让我们尝试使用juniper-eagar-loading进行改写。

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

我将参考这个例子。

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。

    • 中間にrelationテーブルを持つようなN:M関係になる状況を作ってみる

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

ここにexamplesはあるけど、コードを見る限り、3回クエリが走りそうにみえる

桌子

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!
+}
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'

我们可以看出,用户分别查询了就业和公司相关的信息。

使用HasManyThrough关系连接并通过HasMany进行获取

由于HasManyThrough无法进行join取值,所以需要多进行一次无效的查询。
在本例中,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'

减少了一个查询。

总结

    • GraphQL便利

単一エンドポイントになるので、パフォーマンスモニタリングなど、やりにくい部分はありそう

eager_loadはできる

juniper_eager_loadはいろいろ制約がある
モデルはmodelsというmoduleにしなきゃいけない
モデル名とGraphQLのtype名は同じである必要がある

ベンチマークとってみたい
認証周りはactix-webなど、webフレームワーク層でやれそう

广告
将在 10 秒后关闭
bannerAds