-
Notifications
You must be signed in to change notification settings - Fork 824
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
FIX Use ORM functions to perform eager loading instead of raw query #11509
base: 5.3
Are you sure you want to change the base?
Conversation
. ' ORDER BY FIELD(' . $childIDField . ', ' . $fetchedIDsAsString . ')' | ||
); | ||
$joinRows = SQLSelect::create() | ||
->setSelect('"' . $joinTable . '".' . "*") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
->setSelect('"' . $joinTable . '".' . "*") |
It will select everything by default, I believe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the table-specific select may lead to ambiguity with the columns from the joined table. This ensures that we're always working with the correct values.
src/ORM/DataList.php
Outdated
->addWhere('"' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')') | ||
->addWhere('"' . $childIDField . '" IN (' . $fetchedIDsAsString . ')') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to include the table name in addWhere
so there's no ambiguity if we change the query in the future.
->setFrom([$joinTable => $joinTable]) | ||
->addWhere('"' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')') | ||
->addWhere('"' . $childIDField . '" IN (' . $fetchedIDsAsString . ')') | ||
->addLeftJoin($childTable, "$childTable.ID = $joinTable.$childIDField") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this join?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say we have a joinTable AB linking A's many_many to B. The original query preserved the order of the items that were fetched from table B by putting their IDs in the FIELD list. This changes that by joining table B so that the original order by used to fetch B items can also be used in this query.
The perfect solution would be to right join the join table to the relation query (line 1350), which removes the need for this query altogether, though I'm not sure what the implications will be. I'll have to look into that.
->addWhere('"' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')') | ||
->addWhere('"' . $childIDField . '" IN (' . $fetchedIDsAsString . ')') | ||
->addLeftJoin($childTable, "$childTable.ID = $joinTable.$childIDField") | ||
->setOrderBy($fetchedOrderBy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this orderby? How do we know it's what we actually want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This order by is the replacement for the FIELD order by in the original query as explained in the other comment.
$query = $fetchList->dataQuery()->query(); | ||
$fetchedOrderBy = $query->getOrderBy(); | ||
$childTables = $query->queriedTables(); | ||
$childTable = reset($childTables); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we arbitrarily take the first table?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I could tell this is the only way to derive the base table (the FROM) from a DataList query. An alternative is to grab the table name from the baseClass data object.
I'll add comments to my next commit to make clear what's happening exactly.
Alternative proposal: Optimize sorting logic with
|
Aspect | Your solution | Proposed alternative |
---|---|---|
Performance | Relies on an additional join (addLeftJoin ) and follows $fetchedOrderBy , which may introduce overhead for large datasets. |
Uses a CASE SQL clause for sorting, avoiding unnecessary joins. |
Compatibility | Compatible with various SQL databases, avoids raw SQL. | Compatible with various SQL databases, avoids raw SQL but uses a manual CASE . |
Conformity | Fully conforms to SilverStripe ORM standards via SQLSelect . |
Bypasses ORM methods, introducing direct SQL for simplicity and efficiency. |
Maintainability | More maintainable for developers familiar with SilverStripe ORM. | Slightly less maintainable due to manual SQL logic. |
Use Case | Preferred when maintaining conformity and ORM standards. | Ideal for high-performance requirements without additional joins. |
Proposed implementation
Below is the alternative logic for the join records, utilizing a CASE SQL clause for ordering while ensuring compatibility across supported databases:
$joinRows = [];
if (!empty($parentIDs) && !empty($fetchedIDs)) {
// Construct CASE-based ORDER BY clause
$orderClauses = [];
foreach ($fetchedIDs as $index => $id) {
$orderClauses[] = "WHEN {$childIDField} = {$id} THEN {$index}";
}
$orderByCase = implode(' ', $orderClauses);
// Construct optimized query
$joinQuery = "
SELECT *
FROM {$joinTable}
WHERE {$parentIDField} IN (" . implode(',', $parentIDs) . ")
AND {$childIDField} IN (" . implode(',', $fetchedIDs) . ")
ORDER BY CASE {$orderByCase} ELSE 999 END
";
$joinRows = DB::query($joinQuery);
}
Unit test
The following unit test validates the functionality and correctness of the proposed implementation:
<?php
namespace SilverStripe\ORM\Tests\DataListTest\EagerLoading;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DB;
class CustomOrderEagerLoadingTest extends SapphireTest
{
/**
* Test that the ORDER BY CASE logic correctly sorts the results
*/
public function testOrderByCaseSorting()
{
// Define test data
$parentIDs = [1, 2];
$fetchedIDs = [3, 2, 1]; // Expected order
$expectedOrder = [3, 2, 1];
$joinTable = 'TestJoinTable';
$parentIDField = 'ParentID';
$childIDField = 'ChildID';
// Create a temporary test table
DB::query("
CREATE TEMPORARY TABLE {$joinTable} (
{$childIDField} INT NOT NULL,
{$parentIDField} INT NOT NULL
)
");
// Insert mock data
DB::query("
INSERT INTO {$joinTable} ({$childIDField}, {$parentIDField})
VALUES (1, 1), (2, 2), (3, 1)
");
// Construct the ORDER BY CASE logic
$orderClauses = [];
foreach ($fetchedIDs as $index => $id) {
$orderClauses[] = "WHEN {$childIDField} = {$id} THEN {$index}";
}
$orderByCase = implode(' ', $orderClauses);
// Build the query with ORDER BY CASE
$query = "
SELECT *
FROM {$joinTable}
WHERE {$parentIDField} IN (" . implode(',', $parentIDs) . ")
AND {$childIDField} IN (" . implode(',', $fetchedIDs) . ")
ORDER BY CASE {$orderByCase} ELSE 999 END
";
// Execute the query and convert results to an array
$result = DB::query($query);
$rows = [];
foreach ($result as $row) {
$rows[] = $row;
}
// Extract the actual order of fetched ChildIDs
$actualOrder = array_column($rows, $childIDField);
// Assert that the results are ordered as expected
$this->assertEquals(
$expectedOrder,
$actualOrder,
'The results should maintain the custom order defined by fetchedIDs'
);
}
}
in addition to that, maybe it would make sense to add in the abstract database class a |
Thank you for your insightful suggestion. I agree that introducing a Proposed implementation:
|
Description
The original query was a hardcoded and a rather hacky way of retaining the ordering of the child objects while fetching from a many_many table. The method used to retain this order was 'FIELD', which is a MySQL exclusive feature and breaks in other sql engines. This change converts the query to properly make use of the ORM functions and use the ordering used in the original query directly.
Manual testing steps
Issues
Pull request checklist