13个版本 (5个重大更改)
0.5.1 | 2020年3月4日 |
---|---|
0.5.0 | 2019年11月27日 |
0.4.2 | 2019年11月14日 |
0.2.0 | 2019年6月30日 |
#11 in #juniper
70KB
475 行
juniper-eager-loading
这是一个用于避免N+1查询错误的库,旨在与Juniper 和 juniper-from-schema 一起使用。
它旨在使最常见的关联设置易于处理,同时保持灵活,允许您根据需要自定义。它也是100%数据存储无关的。因此,无论您的API是否由SQL数据库或其他API支持,您仍然可以使用此库。
有关用法示例和更多信息,请参阅crate文档。
lib.rs
:
juniper-eager-loading是一个用于避免N+1查询错误的库,旨在与Juniper 和 juniper-from-schema 一起使用。
它旨在使最常见的关联设置易于处理,同时保持灵活,允许您根据需要自定义。它也是100%数据存储无关的。因此,无论您的API是否由SQL数据库或其他API支持,您仍然可以使用此库。
如果您熟悉GraphQL中的N+1查询和预加载,可以直接跳转到"一个真实示例"。
注意:由于此库需要juniper-from-schema,因此最好首先熟悉该库。
目录
- 什么是N+1查询错误?
- 此库在高级上如何工作
- 一个真实示例
#[derive(EagerLoading)]
- 关联
- 预加载接口或联合
- 接受参数的字段预加载
- Diesel辅助函数
- 当您的GraphQL模式与数据库模式不匹配时
什么是N+1查询错误?
假设您有以下GraphQL模式
schema {
query: Query
}
type Query {
allUsers: [User!]!
}
type User {
id: Int!
country: Country!
}
type Country {
id: Int!
}
然后有人执行以下查询
query SomeQuery {
allUsers {
country {
id
}
}
}
如果您使用SQL数据库作为数据存储并天真地解析该查询,您将在日志中看到如下内容
select * from users
select * from countries where id = ?
select * from countries where id = ?
select * from countries where id = ?
select * from countries where id = ?
...
这种情况发生是因为你首先加载所有用户,然后对每个用户进行循环,加载该用户的国家信息。这意味着1次查询用于加载用户,N次额外的查询用于加载国家。因此得名“N+1查询”。这种类型的错误会严重影响应用程序的性能,因为你进行的数据库调用比必要的多得多。
解决这个问题的一个可能方法是称为“预加载”。其思路是在循环遍历用户之前先加载所有国家。因此,你只需进行2次查询,而不是N+1次。
select * from users
select * from countries where id in (?, ?, ?, ?)
由于你是在循环之前预先加载国家,这种策略称为“预加载”。
GraphQL中的N+1s
如果你在实现GraphQL API时不够小心,你会遇到很多这些N+1查询错误。每当一个字段返回类型列表,并且这些类型在其解析器中执行查询时,你都会遇到N+1查询错误。
这也是REST API中存在的问题,但由于响应是固定的,我们可以更容易地设置必要的预加载,因为我们知道需要计算响应的类型。
然而,在GraphQL中,响应不是固定的。它们取决于传入的查询,这些查询在执行之前是未知的。因此,设置正确的预加载量需要在执行查询之前检查它们,并预加载请求的类型,以便实际的解析器不需要运行查询。这正是这个库所做的事情。
此库在高级上如何工作
如果你有一个像这样的GraphQL类型
type User {
id: Int!
country: Country!
}
你可以创建相应的Rust模型类型,如下所示
struct User {
id: i32,
country_id: i32
}
然而,这种方法有一个大问题。你将如何解析字段 User.country
而不进行数据库查询?解析器可以访问的只是一个带有 country_id
字段的 User
。它无法从数据库中获取国家...
从根本上说,这些类型的模型结构不适合与GraphQL一起进行预加载。因此,这个库采取了不同的方法。
如果我们为数据库模型和GraphQL模型创建了单独的结构体呢?如下所示
#
mod models {
pub struct User {
id: i32,
country_id: i32
}
pub struct Country {
id: i32,
}
}
struct User {
user: models::User,
country: HasOne<Country>,
}
struct Country {
country: models::Country
}
enum HasOne<T> {
Loaded(T),
NotLoaded,
}
现在我们可以用如下代码解析查询
- 加载所有用户(第一次查询)。
- 将用户映射到一个国家ID列表。
- 使用这些ID加载所有国家(第二次查询)。
- 将用户与具有正确ID的国家配对,因此将
User.country
从HasOne::NotLoaded
改为HasOne::Loaded(matching_country)
。 - 在解析GraphQL字段
User.country
时,只需返回加载的国家。
一个真实示例
use juniper::{Executor, FieldResult};
use juniper_eager_loading::{prelude::*, EagerLoading, HasOne};
use juniper_from_schema::graphql_schema;
use std::error::Error;
// Define our GraphQL schema.
graphql_schema! {
schema {
query: Query
}
type Query {
allUsers: [User!]! @juniper(ownership: "owned")
}
type User {
id: Int!
country: Country!
}
type Country {
id: Int!
}
}
// Our model types.
mod models {
use std::error::Error;
use juniper_eager_loading::LoadFrom;
#[derive(Clone)]
pub struct User {
pub id: i32,
pub country_id: i32
}
#[derive(Clone)]
pub struct Country {
pub id: i32,
}
// This trait is required for eager loading countries.
// It defines how to load a list of countries from a list of ids.
// Notice that `Context` is generic and can be whatever you want.
// It will normally be your Juniper context which would contain
// a database connection.
impl LoadFrom<i32> for Country {
type Error = Box<dyn Error>;
type Context = super::Context;
fn load(
employments: &[i32],
field_args: &(),
ctx: &Self::Context,
) -> Result<Vec<Self>, Self::Error> {
// ...
# unimplemented!()
}
}
}
// Our sample database connection type.
pub struct DbConnection;
impl DbConnection {
// Function that will load all the users.
fn load_all_users(&self) -> Vec<models::User> {
// ...
# unimplemented!()
}
}
// Our Juniper context type which contains a database connection.
pub struct Context {
db: DbConnection,
}
impl juniper::Context for Context {}
// Our GraphQL user type.
// `#[derive(EagerLoading)]` takes care of generating all the boilerplate code.
#[derive(Clone, EagerLoading)]
// You need to set the context and error type.
#[eager_loading(
context = Context,
error = Box<dyn Error>,
// These match the default so you wouldn't have to specify them
model = models::User,
id = i32,
root_model_field = user,
)]
pub struct User {
// This user model is used to resolve `User.id`
user: models::User,
// Setup a "has one" association between a user and a country.
//
// We could also have used `#[has_one(default)]` here.
#[has_one(
foreign_key_field = country_id,
root_model_field = country,
graphql_field = country,
)]
country: HasOne<Country>,
}
// And the GraphQL country type.
#[derive(Clone, EagerLoading)]
#[eager_loading(context = Context, error = Box<dyn Error>)]
pub struct Country {
country: models::Country,
}
// The root query GraphQL type.
pub struct Query;
impl QueryFields for Query {
// The resolver for `Query.allUsers`.
fn field_all_users(
&self,
executor: &Executor<'_, Context>,
trail: &QueryTrail<'_, User, Walked>,
) -> FieldResult<Vec<User>> {
let ctx = executor.context();
// Load the model users.
let user_models = ctx.db.load_all_users();
// Turn the model users into GraphQL users.
let mut users = User::from_db_models(&user_models);
// Perform the eager loading.
// `trail` is used to only eager load the fields that are requested. Because
// we're using `QueryTrail`s from "juniper_from_schema" it would be a compile
// error if we eager loaded associations that aren't requested in the query.
User::eager_load_all_children_for_each(&mut users, &user_models, ctx, trail)?;
Ok(users)
}
}
impl UserFields for User {
fn field_id(
&self,
executor: &Executor<'_, Context>,
) -> FieldResult<&i32> {
Ok(&self.user.id)
}
fn field_country(
&self,
executor: &Executor<'_, Context>,
trail: &QueryTrail<'_, Country, Walked>,
) -> FieldResult<&Country> {
// This will unwrap the country from the `HasOne` or return an error if the
// country wasn't loaded, or wasn't found in the database.
Ok(self.country.try_unwrap()?)
}
}
impl CountryFields for Country {
fn field_id(
&self,
executor: &Executor<'_, Context>,
) -> FieldResult<&i32> {
Ok(&self.country.id)
}
}
#
#[derive(EagerLoading)]
要支持预加载,类型需要实现以下特质
实现这些特质涉及大量的样板代码,因此你应该尽可能使用 #[derive(EagerLoading)]
来生成实现。
有时你可能需要对特定的关联进行自定义的 eager loading,在这种情况下,你仍然需要在你的结构体上保留 #[derive(EagerLoading)]
,但需要自己实现 EagerLoadChildrenOfType
以满足需要自定义设置的字段。如何实现这个的示例可以在 这里 找到。
如果您想查看没有宏的全示例,请查看 这里。
属性
#[derive(EagerLoading)]
需要提供一些属性
名称 | 描述 | 默认值 | 示例 |
---|---|---|---|
上下文 |
您的 Juniper 上下文类型。这通常会包含您的数据库连接或其他可以用来加载数据的东西。 | 不适用 | 上下文=上下文 |
错误 |
可能由 eager loading 导致的错误类型。 | 不适用 | 错误= diesel::结果::错误 |
模型 |
您 GraphQL 结构体背后的模型类型 | 模型::{名称结构体} |
模型= crates::数据库::模型::用户 |
id |
您的应用程序使用哪种 id 类型? | i32 |
id= UUID |
root_model_field |
持有支撑模型的字段的名称 | {name of struct} 使用蛇形命名。 |
root_model_field=user |
primary_key_field |
持有模型主键的字段。此字段仅用于为 #[has_many] 和 #[has_many_through] 关联生成的代码。 |
id |
primary_key_field=标识符 |
打印 |
如果设置,将打印 GraphqlNodeForModel 和 EagerLoadAllChildren 生成的实现。 |
未设置 | 打印 |
关联
关联类似于“用户有一个国家”。这些是需要 eager loading 来避免 N+1 的问题的字段。每个关联适用于不同的外键设置,并且需要以不同的方式 eager load。它们应该适合您应用程序中的大多数关联。点击查看更多详细信息。
每个关联的文档假设您正在使用 SQL 数据库,但应该可以轻松适应其他类型的数据存储。
对于您的 GraphQL 结构体中属于这些四种类型之一的每个字段,都会由 #[derive(EagerLoading)]
实现特质 EagerLoadChildrenOfType
。
所有关联支持的属性
所有关联都支持以下属性。
跳过
跳过为该字段实现EagerLoadChildrenOfType
。如果您需要自定义实现,这很有用。
打印
这将导致在编译期间打印出为该字段实现的EagerLoadChildrenOfType
。当与skip
结合使用时,它将打印出一个良好的起点,以便您进行自定义。
生成的代码不会进行格式化。我们建议您使用rustfmt进行格式化。
字段参数
用于指定用于EagerLoadChildrenOfType::FieldArguments
的类型。更多信息在这里。
例如 #[has_one(fields_arguments = CountryUsersArgs)]
。您可以在这里找到完整的示例。
代码生成默认将EagerLoadChildrenOfType::FieldArguments
设置为()
。这对于不接受参数的字段有效。
预加载接口或联合
对接口或联合使用预加载是可能的,但它需要在QueryTrail
上调用.downcast()
。有关更多信息,请参阅juniper-from-schema 文档。
接受参数的字段预加载
如果您有一个接受参数的 GraphQL 字段,您可能需要考虑将其用于预加载。
如果您为这样的字段使用代码生成,您必须在关联字段上指定类型。更多信息在这里。
如果您手动实现EagerLoadChildrenOfType
,您必须将EagerLoadChildrenOfType::FieldArguments
设置为由 juniper-from-schema 生成的参数结构体的类型。您可以在这里找到更多信息。
您还必须为您的模型实现LoadFrom<T, ArgumentType>
。您可以在这里找到完整的示例。
如果您看到类型错误如下
error[E0308]: mismatched types
--> src/main.rs:254:56
|
254 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)]
| ^^^^^^^^^^^^ expected (), found struct `query_trails::CountryUsersArgs`
|
= note: expected type `&()`
found type `&query_trails::CountryUsersArgs<'_>`
这是因为您的 GraphQL 字段 Country.users
接受参数。代码生成默认使用 ()
作为参数类型,因此您得到了这个类型错误。有趣的是,编译器不会让您忘记处理参数。
Diesel辅助函数
为许多模型类型实现 LoadFrom
可能会涉及大量的模板代码。如果你使用 Diesel,建议你使用 以下宏之一来生成 实现。
当您的GraphQL模式与数据库模式不匹配时
此库支持大多数类型的懒加载关联设置,但它可能不支持你的应用程序中所有可能存在的设置。它还最好是在你的数据库模式与你的 GraphQL 模式紧密匹配时使用。
如果你发现自己需要实现不支持的功能,请记住,你仍然可以按照自己的方式实现解析函数。所以,如果在解析器中进行查询是获得所需行为唯一方式的话,那就这么做吧。避免一些 N+1 查询比避免所有查询要好。
然而,如果你有一个你认为这个库应该支持的设置,请不要犹豫,提交一个 issue。
依赖项
~7–16MB
~238K SLoC