Skip to content

Commit ec0d7d8

Browse files
tyranronLegNeato
andauthored
Rework Book (#1230)
- rework and update existing chapters - mention correct case transformation for GraphQL enums (#1029) - document N+1 mitigation techniques and look-ahead features (#234, #444) - mention all integration crates (#867) - fix Book links (#679, #974, #1056) - remove old version of Book (#1168) Additionally: - disable `bson`, `url`, `uuid` and `schema-language` Cargo features by default in `juniper` crate Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
1 parent 86b5319 commit ec0d7d8

File tree

79 files changed

+3876
-3249
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3876
-3249
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ jobs:
404404
deploy-book:
405405
name: deploy (Book)
406406
if: ${{ github.ref == 'refs/heads/master'
407-
|| startsWith(github.ref, 'refs/tags/juniper@') }}
407+
|| startsWith(github.ref, 'refs/tags/juniper') }}
408408
needs: ["codespell", "test", "test-book"]
409409
runs-on: ubuntu-latest
410410
steps:

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
108108
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
109109
[hyper]: https://hyper.rs
110110
[rocket]: https://rocket.rs
111-
[book]: https://graphql-rust.github.io
111+
[book]: https://graphql-rust.github.io/juniper
112112
[book_master]: https://graphql-rust.github.io/juniper/master
113-
[book_index]: https://graphql-rust.github.io
114-
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
113+
[book_index]: https://graphql-rust.github.io/juniper
114+
[book_quickstart]: https://graphql-rust.github.io/juniper/quickstart.html
115115
[docsrs]: https://docs.rs/juniper
116116
[warp]: https://github.com/seanmonstar/warp
117117
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples

benches/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ authors = ["Christoph Herzog <chris@theduke.at>"]
66
publish = false
77

88
[dependencies]
9+
dataloader = "0.17" # for Book only
910
futures = "0.3"
1011
juniper = { path = "../juniper" }
1112

book/book.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
[book]
2-
title = "Juniper Book (GraphQL server for Rust)"
2+
title = "Juniper Book"
33
description = "User guide for Juniper (GraphQL server library for Rust)."
44
language = "en"
55
multilingual = false
6+
authors = [
7+
"Kai Ren (@tyranron)",
8+
]
69
src = "src"
710

811
[build]
912
build-dir = "_rendered"
1013
create-missing = false
1114

1215
[output.html]
13-
git_repository_url = "https://github.com/graphql-rs/juniper"
16+
git_repository_url = "https://github.com/graphql-rust/juniper"
1417

1518
[rust]
1619
edition = "2021"

book/src/README.md

-73
This file was deleted.

book/src/SUMMARY.md

+23-34
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
1-
- [Introduction](README.md)
2-
- [Quickstart](quickstart.md)
3-
4-
- [Type System](types/index.md)
1+
# Summary
52

6-
- [Defining objects](types/objects/defining_objects.md)
7-
- [Complex fields](types/objects/complex_fields.md)
8-
- [Using contexts](types/objects/using_contexts.md)
9-
- [Error handling](types/objects/error_handling.md)
10-
- [Other types](types/other-index.md)
11-
- [Enums](types/enums.md)
3+
- [Introduction](introduction.md)
4+
- [Quickstart](quickstart.md)
5+
- [Type system](types/index.md)
6+
- [Objects](types/objects/index.md)
7+
- [Complex fields](types/objects/complex_fields.md)
8+
- [Context](types/objects/context.md)
9+
- [Error handling](types/objects/error/index.md)
10+
- [Field errors](types/objects/error/field.md)
11+
- [Schema errors](types/objects/error/schema.md)
12+
- [Generics](types/objects/generics.md)
1213
- [Interfaces](types/interfaces.md)
14+
- [Unions](types/unions.md)
15+
- [Enums](types/enums.md)
1316
- [Input objects](types/input_objects.md)
1417
- [Scalars](types/scalars.md)
15-
- [Unions](types/unions.md)
16-
17-
- [Schemas and mutations](schema/schemas_and_mutations.md)
18-
19-
- [Adding A Server](servers/index.md)
20-
21-
- [Official Server Integrations](servers/official.md) - [Hyper](servers/hyper.md)
22-
- [Warp](servers/warp.md)
23-
- [Rocket](servers/rocket.md)
24-
- [Hyper](servers/hyper.md)
25-
- [Third Party Integrations](servers/third-party.md)
26-
18+
- [Schema](schema/index.md)
19+
- [Subscriptions](schema/subscriptions.md)
20+
- [Introspection](schema/introspection.md)
21+
- [Serving](serve/index.md)
22+
- [Batching](serve/batching.md)
2723
- [Advanced Topics](advanced/index.md)
28-
29-
- [Introspection](advanced/introspection.md)
30-
- [Non-struct objects](advanced/non_struct_objects.md)
31-
- [Implicit and explicit null](advanced/implicit_and_explicit_null.md)
32-
- [Objects and generics](advanced/objects_and_generics.md)
33-
- [Multiple operations per request](advanced/multiple_ops_per_request.md)
34-
- [Dataloaders](advanced/dataloaders.md)
35-
- [Subscriptions](advanced/subscriptions.md)
36-
37-
# - [Context switching]
38-
39-
# - [Dynamic type system]
24+
- [Implicit and explicit `null`](advanced/implicit_and_explicit_null.md)
25+
- [N+1 problem](advanced/n_plus_1.md)
26+
- [DataLoader](advanced/dataloader.md)
27+
- [Look-ahead](advanced/lookahead.md)
28+
- [Eager loading](advanced/eager_loading.md)

book/src/advanced/dataloader.md

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
DataLoader
2+
==========
3+
4+
DataLoader pattern, named after the correspondent [`dataloader` NPM package][0], represents a mechanism of batching and caching data requests in a delayed manner for solving the [N+1 problem](n_plus_1.md).
5+
6+
> A port of the "Loader" API originally developed by [@schrockn] at Facebook in 2010 as a simplifying force to coalesce the sundry key-value store back-end APIs which existed at the time. At Facebook, "Loader" became one of the implementation details of the "Ent" framework, a privacy-aware data entity loading and caching layer within web server product code. This ultimately became the underpinning for Facebook's GraphQL server implementation and type definitions.
7+
8+
In [Rust] ecosystem, DataLoader pattern is introduced with the [`dataloader` crate][1], naturally usable with [Juniper].
9+
10+
Let's remake our [example of N+1 problem](n_plus_1.md), so it's solved by applying the DataLoader pattern:
11+
```rust
12+
# extern crate anyhow;
13+
# extern crate dataloader;
14+
# extern crate juniper;
15+
# use std::{collections::HashMap, sync::Arc};
16+
# use anyhow::anyhow;
17+
# use dataloader::non_cached::Loader;
18+
# use juniper::{graphql_object, GraphQLObject};
19+
#
20+
# type CultId = i32;
21+
# type UserId = i32;
22+
#
23+
# struct Repository;
24+
#
25+
# impl Repository {
26+
# async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
27+
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
28+
# }
29+
#
30+
struct Context {
31+
repo: Repository,
32+
cult_loader: CultLoader,
33+
}
34+
35+
impl juniper::Context for Context {}
36+
37+
#[derive(Clone, GraphQLObject)]
38+
struct Cult {
39+
id: CultId,
40+
name: String,
41+
}
42+
43+
struct CultBatcher {
44+
repo: Repository,
45+
}
46+
47+
// Since `BatchFn` doesn't provide any notion of fallible loading, like
48+
// `try_load()` returning `Result<HashMap<K, V>, E>`, we handle possible
49+
// errors as loaded values and unpack them later in the resolver.
50+
impl dataloader::BatchFn<CultId, Result<Cult, Arc<anyhow::Error>>> for CultBatcher {
51+
async fn load(
52+
&mut self,
53+
cult_ids: &[CultId],
54+
) -> HashMap<CultId, Result<Cult, Arc<anyhow::Error>>> {
55+
// Effectively performs the following SQL query:
56+
// SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
57+
match self.repo.load_cults_by_ids(cult_ids).await {
58+
Ok(found_cults) => {
59+
found_cults.into_iter().map(|(id, cult)| (id, Ok(cult))).collect()
60+
}
61+
// One could choose a different strategy to deal with fallible loads,
62+
// like consider values that failed to load as absent, or just panic.
63+
// See cksac/dataloader-rs#35 for details:
64+
// https://github.com/cksac/dataloader-rs/issues/35
65+
Err(e) => {
66+
// Since `anyhow::Error` doesn't implement `Clone`, we have to
67+
// work around here.
68+
let e = Arc::new(e);
69+
cult_ids.iter().map(|k| (k.clone(), Err(e.clone()))).collect()
70+
}
71+
}
72+
}
73+
}
74+
75+
type CultLoader = Loader<CultId, Result<Cult, Arc<anyhow::Error>>, CultBatcher>;
76+
77+
fn new_cult_loader(repo: Repository) -> CultLoader {
78+
CultLoader::new(CultBatcher { repo })
79+
// Usually a `Loader` will coalesce all individual loads which occur
80+
// within a single frame of execution before calling a `BatchFn::load()`
81+
// with all the collected keys. However, sometimes this behavior is not
82+
// desirable or optimal (perhaps, a request is expected to be spread out
83+
// over a few subsequent ticks).
84+
// A larger yield count will allow more keys to be appended to the batch,
85+
// but will wait longer before the actual load. For more details see:
86+
// https://github.com/cksac/dataloader-rs/issues/12
87+
// https://github.com/graphql/dataloader#batch-scheduling
88+
.with_yield_count(100)
89+
}
90+
91+
struct Person {
92+
id: UserId,
93+
name: String,
94+
cult_id: CultId,
95+
}
96+
97+
#[graphql_object]
98+
#[graphql(context = Context)]
99+
impl Person {
100+
fn id(&self) -> CultId {
101+
self.id
102+
}
103+
104+
fn name(&self) -> &str {
105+
self.name.as_str()
106+
}
107+
108+
async fn cult(&self, ctx: &Context) -> anyhow::Result<Cult> {
109+
ctx.cult_loader
110+
// Here, we don't run the `CultBatcher::load()` eagerly, but rather
111+
// only register the `self.cult_id` value in the `cult_loader` and
112+
// wait for other concurrent resolvers to do the same.
113+
// The actual batch loading happens once all the resolvers register
114+
// their IDs and there is nothing more to execute.
115+
.try_load(self.cult_id)
116+
.await
117+
// The outer error is the `io::Error` returned by `try_load()` if
118+
// no value is present in the `HashMap` for the specified
119+
// `self.cult_id`, meaning that there is no `Cult` with such ID
120+
// in the `Repository`.
121+
.map_err(|_| anyhow!("No cult exists for ID `{}`", self.cult_id))?
122+
// The inner error is the one returned by the `CultBatcher::load()`
123+
// if the `Repository::load_cults_by_ids()` fails, meaning that
124+
// running the SQL query failed.
125+
.map_err(|arc_err| anyhow!("{arc_err}"))
126+
}
127+
}
128+
129+
struct Query;
130+
131+
#[graphql_object]
132+
#[graphql(context = Context)]
133+
impl Query {
134+
async fn persons(ctx: &Context) -> anyhow::Result<Vec<Person>> {
135+
// Effectively performs the following SQL query:
136+
// SELECT id, name, cult_id FROM persons
137+
ctx.repo.load_all_persons().await
138+
}
139+
}
140+
141+
fn main() {
142+
143+
}
144+
```
145+
146+
And now, performing a [GraphQL query which lead to N+1 problem](n_plus_1.md)
147+
```graphql
148+
query {
149+
persons {
150+
id
151+
name
152+
cult {
153+
id
154+
name
155+
}
156+
}
157+
}
158+
```
159+
will lead to efficient [SQL] queries, just as expected:
160+
```sql
161+
SELECT id, name, cult_id FROM persons;
162+
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
163+
```
164+
165+
166+
167+
168+
## Caching
169+
170+
[`dataloader::cached`] provides a [memoization][2] cache: after `BatchFn::load()` is called once with given keys, the resulting values are cached to eliminate redundant loads.
171+
172+
DataLoader caching does not replace [Redis], [Memcached], or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data [in the context of a single request][3].
173+
174+
> **WARNING**: A DataLoader should be created per-request to avoid risk of bugs where one client is able to load cached/batched data from another client outside its authenticated scope. Creating a DataLoader within an individual resolver will prevent batching from occurring and will nullify any benefits of it.
175+
176+
177+
178+
179+
## Full example
180+
181+
For a full example using DataLoaders in [Juniper] check out the [`jayy-lmao/rust-graphql-docker` repository][4].
182+
183+
184+
185+
186+
[`dataloader::cached`]: https://docs.rs/dataloader/latest/dataloader/cached/index.html
187+
[@schrockn]: https://github.com/schrockn
188+
[Juniper]: https://docs.rs/juniper
189+
[Memcached]: https://memcached.org
190+
[Redis]: https://redis.io
191+
[Rust]: https://www.rust-lang.org
192+
[SQL]: https://en.wikipedia.org/wiki/SQL
193+
194+
[0]: https://github.com/graphql/dataloader
195+
[1]: https://docs.rs/crate/dataloader
196+
[2]: https://en.wikipedia.org/wiki/Memoization
197+
[3]: https://github.com/graphql/dataloader#caching
198+
[4]: https://github.com/jayy-lmao/rust-graphql-docker

0 commit comments

Comments
 (0)