Skip to content

Fix #944 - allows to filter by relation by keeping EagerLoading enabled #950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 20, 2017

Conversation

soyuka
Copy link
Member

@soyuka soyuka commented Feb 16, 2017

Q A
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #944
License MIT
Doc PR na

/!\ This is probably the worse code I've ever produced here and I hope we can find how to improve it together.

To fix #944 I needed to alter the query generated by the filters and the eager loading extension. To do so, I've added a FilterEagerLoadingExtension that patches the Query generated by the FilterExtension.

It runs directly after the FilterExtension and transforms the query so that it ends up doing something like:

SELECT o, partial bars_a1_p.{id,prop} 
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo o 
LEFT JOIN o.bars bars_a1_p 
WHERE o IN(
  SELECT p 
  FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo p 
  LEFT JOIN p.bars bars_a1 
  WHERE LOWER(bars_a1.prop) LIKE LOWER(CONCAT('%', :prop_p1, '%'))
)

TODO:

  • Rename test class Foo/Bar too generic
  • Unit tests
  • Allow the user to skip this extension so that we can get the relation items filtered (previous behavior that is considered as a bug)
  • Prevent the extension from running when forceEager is not set
  • Performances might be improved slightly by selecting the wanted id with the were clause (removes the JOIN inside the IN and select from that entity directly, but it's complex)
  • Test OneToMany
  • Test ManyToMany

Apologizes to Scrutinizer, that complexity index will be very high -_-.

@soyuka soyuka force-pushed the reproduce-attempt-944 branch 2 times, most recently from 65686a3 to f220063 Compare February 21, 2017 16:30
@soyuka
Copy link
Member Author

soyuka commented Feb 21, 2017

Query is now better:

SELECT o, partial bars_a1.{id,prop} FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo o
LEFT JOIN o.bars bars_a1 
WHERE o IN(
    SELECT IDENTITY(bars_a1_a2.foo) 
    FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Bar bars_a1_a2 
    WHERE LOWER(bars_a1_a2.prop) LIKE LOWER(CONCAT('%', :prop_p1, '%'))
)

This will work with the PaginationExtension as it doesn't alter the results, and it is optimized.

@soyuka soyuka force-pushed the reproduce-attempt-944 branch from f220063 to 105b6b2 Compare February 23, 2017 11:26
@soyuka
Copy link
Member Author

soyuka commented Feb 23, 2017

WDYT? I'll add unit tests but I want to know first if this solution is fine to you or if you want to try another approach :).

@api-platform/core-team

@soyuka soyuka requested a review from teohhanhui February 24, 2017 14:22
@dunglas
Copy link
Member

dunglas commented Feb 24, 2017

Looks better, indeed. @ymarillet can you try if this patch fix the problem?

@@ -94,6 +94,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
$container->setParameter('api_platform.eager_loading.force_eager', $config['eager_loading']['force_eager']);
$container->setParameter('api_platform.eager_loading.filter_relations', $config['eager_loading']['filter_relations']);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teohhanhui suggestions about that parameter name ? \o/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch_partial?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch_partial to true would mean we want to fetch a partial relation?

Copy link
Contributor

@teohhanhui teohhanhui Feb 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. So the default should be false.

Or perhaps partial_relations, partial_collections, partial_collection_fetch?

@@ -49,6 +49,7 @@ public function getConfigTreeBuilder()
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end()
->booleanNode('filter_relations')->defaultFalse()->info('Allow to filter the relation collection when the filter is on a nested property. By default, it will retrieve the full relation of the filtered resource. If enabled, only relations matching the filter will be returned.')->end()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me to rewrite this for you. 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!RemindMe

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When filtering on a nested property through a to-many relation, do not fetch the other items in the collection. Warning: this breaks round trip, as you cannot PUT the returned collection back to the parent property.

p/s: @soyuka Does this only happen when filtering on a nested property? Or whenever the other side of the relation is fetch-joined?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we need to warn that this would apply to every level in a multi-level nested property.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only when filtering on a nested property because it only joins the result matching that filter.

Copy link
Contributor

@teohhanhui teohhanhui Feb 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When filtering by e.g. products?brands=/brands/1, doesn't it result in SQL something like:

SELECT ...
FROM products
INNER JOIN products_brands
    ON products.id = products_brands.product_id
INNER JOIN brands
    ON products_brands.brand_id = brands.id
WHERE brands.id = 1

and thus:

{
  "@id": "/products/1",
  "brands": [
    "/brands/1"
  ]
}

even if product 1 has other brands?

This is not a nested property. So I think the problem manifests itself when filtering on any to-many relation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think we should not have this option, because it results in returning invalid data. ("Product 1 has 1 brand which is Brand 1.")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understood, what is the relation type on products_brands and brands?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many-to-many. products_brands is the join table.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah your right I think that this option is too much confusing, ManyToMany relations are working fine, I've added a test case anyway.

I'm removing the availability to filter on the relation. I think that the best to filter a relation would be to use subresource + filter.

@soyuka
Copy link
Member Author

soyuka commented Feb 24, 2017 via email

@soyuka soyuka force-pushed the reproduce-attempt-944 branch from 105b6b2 to f06a3a7 Compare March 2, 2017 09:18
@soyuka
Copy link
Member Author

soyuka commented Mar 2, 2017

@teohhanhui I refactored the fix, it's much cleaner and simple now. It clones the query and uses a subquery with the where clause to find the root entity identifiers.

Origin query:

SELECT o, partial relatedToDummyFriend_a1.{name,dummyFriend,relatedDummy}, partial dummyFriend_a2.{id,name} 
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o 
LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a1 
LEFT JOIN relatedToDummyFriend_a1.dummyFriend dummyFriend_a2 
WHERE dummyFriend_a2.id = :dummyFriend_p1

Fixed query:

SELECT o, partial relatedToDummyFriend_a1.{name,dummyFriend,relatedDummy}, partial dummyFriend_a2.{id,name} 
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o 
LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a1 
LEFT JOIN relatedToDummyFriend_a1.dummyFriend dummyFriend_a2 
WHERE o IN(
  SELECT o_2 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o_2 
  LEFT JOIN o_2.relatedToDummyFriend relatedToDummyFriend_a1_a3 
  LEFT JOIN relatedToDummyFriend_a1_a3.dummyFriend dummyFriend_a2_a4 
  WHERE dummyFriend_a2_a4.id = :dummyFriend_p1
)

This will work with any relation (manytomany, manytoone etc.). I've removed the parameter to skip this extension.

@soyuka soyuka force-pushed the reproduce-attempt-944 branch 3 times, most recently from 8cdbec9 to 36c1ad5 Compare March 2, 2017 15:45
@soyuka soyuka force-pushed the reproduce-attempt-944 branch 2 times, most recently from fb1d84d to d03d4fe Compare March 11, 2017 10:32
@soyuka
Copy link
Member Author

soyuka commented Mar 11, 2017

ping @api-platform/core-team

I think that this (as it's a bug fix) should target 2.0. Lmk.

@teohhanhui
Copy link
Contributor

Yes, this is a bug fix. 2.0 it should be then.

@soyuka soyuka force-pushed the reproduce-attempt-944 branch from d03d4fe to 167e3be Compare March 13, 2017 10:06
@soyuka soyuka changed the base branch from master to 2.0 March 13, 2017 10:07
@soyuka soyuka force-pushed the reproduce-attempt-944 branch from 167e3be to 495ea9e Compare March 13, 2017 10:08
@dunglas dunglas merged commit 603ad01 into api-platform:2.0 Mar 20, 2017
@dunglas
Copy link
Member

dunglas commented Mar 20, 2017

Thank you very much @soyuka

@soyuka soyuka deleted the reproduce-attempt-944 branch March 30, 2017 06:32
hoangnd25 pushed a commit to hoangnd25/core that referenced this pull request Feb 23, 2018
Fix api-platform#944 - allows to filter by relation by keeping EagerLoading enabled
@vasilvestre
Copy link

filter_relations parameter seem not documented, it's still exist ?

@soyuka
Copy link
Member Author

soyuka commented May 15, 2019

filter_relations parameter seem not documented, it's still exist ?

Nope.

@qDL7
Copy link

qDL7 commented Feb 5, 2020

is it possible then to filter relations without a custom filters? @soyuka

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants