|
3 | 3 | - [Attributes](#attributes) |
4 | 4 | - [Union types](#union-types) |
5 | 5 | - [Connections (Pagination)](#connections-pagination) |
| 6 | +- [Deferred type loading (Solving N+1 problem)](#deferred-type-loading-solving-n1-problem) |
6 | 7 |
|
7 | 8 | 📌 At a minimum, you need to define a query and a mutation to build a valid schema. |
8 | 9 |
|
@@ -707,3 +708,112 @@ You can also define additional custom input arguments if needed. |
707 | 708 | Each 'node' in a connection must have a `#[Cursor]` attribute. You can add this on a **property** or a **method**. This defines the cursor output for each "edge." |
708 | 709 |
|
709 | 710 | 💡 **Note:** If you **don't** define a `#[Cursor]`, the cursor will always be `null` when querying. |
| 711 | + |
| 712 | +## Deferred type loading (Solving N+1 problem) |
| 713 | + |
| 714 | +When working with a nested GraphQL model, the famous N+1 problem can become an issue. |
| 715 | +As a solution, Webonyx/graphql-php has introduced so-called 'Deferred type loading' |
| 716 | + |
| 717 | +For more details, check out: |
| 718 | +- [Phabricator: Performance: N+1 Query Problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/) |
| 719 | +- [Webonyx: Solving N+1 Problem](https://webonyx.github.io/graphql-php/data-fetching/#solving-n1-problem) |
| 720 | + |
| 721 | +To use 'Deferred type loading' in _GraphQL Attribute Schema_, for each type, a custom `DeferredTypeLoader` needs to be created. |
| 722 | +This `DeferredTypeLoader` can then be configured in `#[Query]`, `#[Mutation]` or `#[Field]`. |
| 723 | + |
| 724 | +### Example |
| 725 | +Create a custom `DeferredTypeLoader` for GraphQL type `SomeType`: |
| 726 | + |
| 727 | +```php |
| 728 | +use Jerowork\GraphqlAttributeSchema\Type\Loader\DeferredTypeLoader; |
| 729 | + |
| 730 | +final readonly class SomeTypeLoader implements DeferredTypeLoader |
| 731 | +{ |
| 732 | + public function __construct( |
| 733 | + private SomeRepository $repository, |
| 734 | + ) {} |
| 735 | + |
| 736 | + public function load(array $references) : array |
| 737 | + { |
| 738 | + return array_map( |
| 739 | + fn($item) => new DeferredType($item->getId(), new SomeType($item)), |
| 740 | + $this->repository->findByIds($references), |
| 741 | + ); |
| 742 | + } |
| 743 | +} |
| 744 | +``` |
| 745 | + |
| 746 | +Configure `#[Query]` or `#[Mutation]`: |
| 747 | + |
| 748 | +```php |
| 749 | +use Jerowork\GraphqlAttributeSchema\Attribute\Mutation; |
| 750 | +use Jerowork\GraphqlAttributeSchema\Attribute\Query; |
| 751 | + |
| 752 | +final readonly class SomeQuery |
| 753 | +{ |
| 754 | + #[Query(type: SomeType::class, deferredTypeLoader: SomeTypeLoader::class)] |
| 755 | + public function someQuery(string $id): string |
| 756 | + { |
| 757 | + return $id; |
| 758 | + } |
| 759 | +} |
| 760 | + |
| 761 | +final readonly class SomeMutation |
| 762 | +{ |
| 763 | + #[Mutation(type: SomeType::class, deferredTypeLoader: SomeTypeLoader::class)] |
| 764 | + public function someMutation(string $id): string |
| 765 | + { |
| 766 | + return $id; |
| 767 | + } |
| 768 | +} |
| 769 | +``` |
| 770 | + |
| 771 | +Or configure `#[Field]` on property or method: |
| 772 | + |
| 773 | +```php |
| 774 | +use Jerowork\GraphqlAttributeSchema\Attribute\Field; |
| 775 | +use Jerowork\GraphqlAttributeSchema\Attribute\Type; |
| 776 | + |
| 777 | +#[Type] |
| 778 | +final readonly class SomeResponseType |
| 779 | +{ |
| 780 | + // Configure on property |
| 781 | + public function __construct( |
| 782 | + #[Field(name: 'some', type: SomeType::class, deferredTypeLoader: SomeTypeLoader::class)] |
| 783 | + public string $id, |
| 784 | + ) {} |
| 785 | + |
| 786 | + // Or configure on method |
| 787 | + #[Field(type: SomeType::class, deferredTypeLoader: SomeTypeLoader::class)] |
| 788 | + public function getSome(string $id): string |
| 789 | + { |
| 790 | + return $id; |
| 791 | + } |
| 792 | +} |
| 793 | +``` |
| 794 | + |
| 795 | +### About `DeferredTypeLoader` |
| 796 | + |
| 797 | +A custom implementation needs to implement `DeferredTypeLoader`. This interface defines one method: `load`. |
| 798 | + |
| 799 | +Input `$references` is a list of type references (identifiers), requested by `#[Query]`, `#[Mutation]` or `#[Field]`. |
| 800 | + |
| 801 | +Output is a list of `DeferredType`, containing the reference of each type and the GraphQL type itself. |
| 802 | + |
| 803 | +Each `DeferredTypeLoader` implementation must be retrievable from the (PSR-11) container via `get()`. |
| 804 | +For Symfony users, make sure they're set to public (e.g., with `#[Autoconfigure(public: true)]`). |
| 805 | +This allows injection of database repositories and other necessary services. |
| 806 | + |
| 807 | +### Configuring in `#[Query]`, `#[Mutation]` or `#[Field]` |
| 808 | + |
| 809 | +By configuring the `deferredTypeLoader` option on `#[Query]`, `#[Mutation]` or `#[Field]`, |
| 810 | +the property or method will be converted to a 'Deferred type loading' field. |
| 811 | + |
| 812 | +Each 'Deferred type loading' field should **only** return the identifier (or 'reference'). |
| 813 | +It can also be a list of references. This reference(s) will automatically be added to the list `$references` in the configured `DeferredTypeLoader`. |
| 814 | + |
| 815 | +The returning reference should be of type: integer, string, `Stringable`, or a list of the mentioned. |
| 816 | + |
| 817 | +As the actual loading now is handled by the `DeferredTypeLoader`, no loading is required in this 'Deferred type loading' field anymore. |
| 818 | + |
| 819 | +**Note:** In order to configure the GraphQL schema with the correct GraphQL type. Setting the `type` option on `#[Query]`, `#[Mutation]` or `#[Field]` is required. |
0 commit comments