尝试使用[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フレームワーク層でやれそう