Skip to content

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

Merged
dunglas merged 1 commit into
api-platform:2.0from
soyuka:reproduce-attempt-944
Mar 20, 2017
Merged

Fix #944 - allows to filter by relation by keeping EagerLoading enabled#950
dunglas merged 1 commit into
api-platform:2.0from
soyuka:reproduce-attempt-944

Conversation

@soyuka

@soyuka soyuka commented Feb 16, 2017

Copy link
Copy Markdown
Member
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

soyuka commented Feb 21, 2017

Copy link
Copy Markdown
Member Author

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

soyuka commented Feb 23, 2017

Copy link
Copy Markdown
Member Author

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

dunglas commented Feb 24, 2017

Copy link
Copy Markdown
Member

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

$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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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?

@teohhanhui teohhanhui Feb 27, 2017

Copy link
Copy Markdown
Contributor

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?

->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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

@teohhanhui teohhanhui Feb 27, 2017

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

soyuka commented Feb 24, 2017 via email

Copy link
Copy Markdown
Member Author

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

soyuka commented Mar 2, 2017

Copy link
Copy Markdown
Member Author

@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

soyuka commented Mar 11, 2017

Copy link
Copy Markdown
Member Author

ping @api-platform/core-team

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

@teohhanhui

Copy link
Copy Markdown
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

dunglas commented Mar 20, 2017

Copy link
Copy Markdown
Member

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
Copy Markdown

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

@soyuka

soyuka commented May 15, 2019

Copy link
Copy Markdown
Member Author

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

Nope.

@qDL7

qDL7 commented Feb 5, 2020

Copy link
Copy Markdown

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