Skip to content

Commit 4cc1501

Browse files
authored
Merge pull request #27 from lorado/bug/26
Fixing n+1 relations problem
2 parents 99618dc + 63f6abd commit 4cc1501

Some content is hidden

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

42 files changed

+1447
-181
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/vendor
33
/.php_cs.cache
44
*.orig
5+
/.idea

.travis.yml

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
language : php
2-
php : [7.1, 7.2]
3-
cache : { directories : [$COMPOSER_CACHE_DIR, $HOME/.composer/cache, vendor] }
4-
install : composer update --no-interaction --prefer-dist
1+
language : php
2+
php : [7.1, 7.2]
3+
cache : { directories : [$COMPOSER_CACHE_DIR, $HOME/.composer/cache, vendor] }
4+
install : composer update --no-interaction --prefer-dist
55
notifications :
6-
email : false
6+
email : false
77

88
stages :
9-
- test
10-
- lint
9+
- test
10+
- lint
1111

1212
script :
13-
- vendor/bin/phpunit
13+
- vendor/bin/phpunit
1414

1515
before_install :
16-
- composer global require hirak/prestissimo --update-no-dev
16+
- composer global require hirak/prestissimo --update-no-dev
1717

1818
jobs :
19-
include :
20-
- stage : lint
21-
php : 7.2
22-
before_install :
23-
- composer global require hirak/prestissimo --update-no-dev
24-
- composer require phpmd/phpmd --no-update --prefer-dist
25-
- composer require phpstan/phpstan --no-update --prefer-dist
26-
script :
27-
- vendor/bin/phpmd src text phpmd.xml
28-
- vendor/bin/phpmd tests text phpmd.xml
29-
- vendor/bin/phpstan analyse --autoload-file=_ide_helper.php --level 1 src
19+
include :
20+
- stage : lint
21+
php : 7.2
22+
env : TESTBENCH_VERSION=3.6.* LARAVEL_VERSION=5.6.*
23+
before_install :
24+
- composer global require hirak/prestissimo --update-no-dev
25+
- composer require phpmd/phpmd --no-update --prefer-dist
26+
- composer require phpstan/phpstan --no-update --prefer-dist
27+
script :
28+
- vendor/bin/phpmd src text phpmd.xml
29+
- vendor/bin/phpmd tests text phpmd.xml
30+
- vendor/bin/phpstan analyse --autoload-file=_ide_helper.php --level 1 src

README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ php artisan vendor:publish --provider="StudioNet\GraphQL\ServiceProvider"
3333
- [Definition](#definition)
3434
- [Query](#query)
3535
- [Mutation](#mutation)
36+
- [Require authorization](#require-authorization)
3637
- [Self documentation](#self-documentation)
3738
- [Examples](#examples)
39+
- [N+1 Problem](#n+1-problem)
3840

3941
### Definition
4042

@@ -50,6 +52,7 @@ use StudioNet\GraphQL\Definition\Type;
5052
use StudioNet\GraphQL\Support\Definition\EloquentDefinition;
5153
use StudioNet\GraphQL\Filter\EqualsOrContainsFilter;
5254
use App\User;
55+
use Auth;
5356

5457
/**
5558
* Specify user GraphQL definition
@@ -184,21 +187,38 @@ namespace App\GraphQL\Query;
184187

185188
use StudioNet\GraphQL\Support\Definition\Query;
186189
use Illuminate\Support\Facades\Auth;
190+
use App\User;
191+
use Auth;
187192

188193
class Viewer extends Query {
194+
/**
195+
* {@inheritDoc}
196+
*/
197+
protected function authorize(array $args) {
198+
// check, that user is not a guest
199+
return !Auth::guest();
200+
}
201+
189202
/**
190203
* {@inheritDoc}
191204
*/
192205
public function getRelatedType() {
193206
return \GraphQL::type('user');
194207
}
208+
209+
/**
210+
* {@inheritdoc}
211+
*/
212+
public function getSource() {
213+
return User::class;
214+
}
195215

196216
/**
197217
* Return logged user
198218
*
199-
* @return \App\User|null
219+
* @return User|null
200220
*/
201-
public function getResolver() {
221+
public function getResolver($opts) {
202222
return Auth::user();
203223
}
204224
}
@@ -222,6 +242,15 @@ return [
222242
];
223243
```
224244

245+
`getResolver()` receives an array-argument with followed item:
246+
247+
- `root` 1st argument given by webonyx library - `GraphQL\Executor\Executor::resolveOrError()`
248+
- `args` 2nd argument given by webonyx library
249+
- `context` 3rd argument given by webonyx library
250+
- `info` 4th argument given by webonyx library
251+
- `fields` array of fields, that were fetched from query. Limited by depth in `StudioNet\GraphQL\GraphQL::FIELD_SELECTION_DEPTH`
252+
- `with` array of relations, that could/should be eager loaded. **NOTICE:** Finding this relations happens ONLY, if `getSource()` is defined - this method should return a class name of a associated root-type in query. If `getSource()` is not defined, then `with` will be always empty.
253+
225254
### Mutation
226255

227256
Mutation are used to update or create data.
@@ -236,6 +265,14 @@ use StudioNet\GraphQL\Definition\Type;
236265
use App\User;
237266

238267
class Profile extends Mutation {
268+
/**
269+
* {@inheritDoc}
270+
*/
271+
protected function authorize(array $args) {
272+
// check, that user is not a guest
273+
return !Auth::guest();
274+
}
275+
239276
/**
240277
* {@inheritDoc}
241278
*
@@ -294,6 +331,14 @@ return [
294331
];
295332
```
296333

334+
### Require authorization
335+
336+
Currently you have a possibility to protect your own queries and mutations. You have to implement `authorize()` method in your query/mutation, that return a boolean, that indicates, if requested query/mutation has to be executed. If method return `false`, an `UNAUTHORIZED` GraphQL-Error will be thrown.
337+
338+
Usage examples are in query and mutation above.
339+
340+
Protection of definition transformers are currently not implemented, but may be will in the future. By now you have to define your query/mutation yourself, and protect it then with logic in `authorize()`.
341+
297342
### Self documentation
298343

299344
A documentation generator is implemented with the package. By default, you can access it by navigate to `/doc/graphql`. You can change this behavior within the configuration file. The built-in documentation is implemented from [this repository](https://github.com/mhallin/graphql-docs).
@@ -560,6 +605,14 @@ post-save (which can be useful for eloquent relational models) :
560605
}
561606
```
562607

608+
### N+1 Problem
609+
610+
The common question is, if graphql library solves n+1 problem. This occures, when graphql resolves relation. Often entities are fetched without relations, and when graphql query needs to fetch relation, for each fetched entity relation would be fetched from SQL separately. So instead of executing 2 SQL queries, you will get N+1 queries, where N is the count of results of root entity. In that example you would query only one relation. If you query more relations, then it becomes N^2+1 problem.
611+
612+
To solve it, Eloquent has already options to eager load relations. Transformers in this library use eager loading, depends on what you query.
613+
614+
Currently this smart detection works perfect only on View and List Transformers. Other transformers will be reworked soon.
615+
563616
## Contribution
564617

565618
If you want participate to the project, thank you ! In order to work properly,

database/factories/Comment.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
use Faker\Generator;
3+
use StudioNet\GraphQL\Tests\Entity;
4+
5+
$factory->define(Entity\Comment::class, function (Generator $faker) {
6+
return [
7+
'body' => $faker->text(100)
8+
];
9+
});

database/factories/Country.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
use Faker\Generator;
3+
use StudioNet\GraphQL\Tests\Entity;
4+
5+
$factory->define(Entity\Country::class, function (Generator $faker) {
6+
return [
7+
'name' => $faker->country
8+
];
9+
});

database/factories/Label.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
use Faker\Generator;
3+
use StudioNet\GraphQL\Tests\Entity;
4+
5+
$factory->define(Entity\Label::class, function (Generator $faker) {
6+
return [
7+
'name' => $faker->word
8+
];
9+
});

database/factories/Phone.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
use Faker\Generator;
3+
use StudioNet\GraphQL\Tests\Entity;
4+
5+
$factory->define(Entity\Phone::class, function (Generator $faker) {
6+
return [
7+
'label' => $faker->word,
8+
'number' => $faker->phoneNumber
9+
];
10+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
use Illuminate\Database\Migrations\Migration;
3+
4+
/**
5+
* CreatePhonesTable
6+
*
7+
* @see Migration
8+
*/
9+
class CreatePhonesTable extends Migration {
10+
/**
11+
* Run the migrations.
12+
*
13+
* @return void
14+
*/
15+
public function up() {
16+
Schema::create('phones', function ($table) {
17+
$table->increments('id');
18+
19+
$table->string('label');
20+
$table->string('number');
21+
$table->unsignedInteger('user_id');
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*
28+
* @return void
29+
*/
30+
public function down() {
31+
Schema::drop('phones');
32+
}
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
6+
/**
7+
* CreateCountriesTable
8+
*
9+
* @see Migration
10+
*/
11+
class CreateCountriesTableAndExtendUsersTable extends Migration {
12+
/**
13+
* Run the migrations.
14+
*
15+
* @return void
16+
*/
17+
public function up() {
18+
Schema::create('countries', function (Blueprint $table) {
19+
$table->increments('id');
20+
$table->string('name');
21+
});
22+
23+
Schema::table('users', function (Blueprint $table) {
24+
$table->unsignedInteger('country_id')->nullable();
25+
});
26+
}
27+
28+
/**
29+
* Reverse the migrations.
30+
*
31+
* @return void
32+
*/
33+
public function down() {
34+
Schema::table('users', function (Blueprint $table) {
35+
$table->dropColumn('country_id');
36+
});
37+
Schema::drop('phones');
38+
}
39+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
6+
/**
7+
* CreateCommentsTable
8+
*
9+
* @see Migration
10+
*/
11+
class CreateCommentsTable extends Migration {
12+
/**
13+
* Run the migrations.
14+
*
15+
* @return void
16+
*/
17+
public function up() {
18+
Schema::create('comments', function (Blueprint $table) {
19+
$table->increments('id');
20+
$table->text('body');
21+
$table->unsignedInteger('commentable_id');
22+
$table->string('commentable_type');
23+
$table->timestamps();
24+
});
25+
}
26+
27+
/**
28+
* Reverse the migrations.
29+
*
30+
* @return void
31+
*/
32+
public function down() {
33+
Schema::drop('comments');
34+
}
35+
}

0 commit comments

Comments
 (0)