The Destination field is not guaranteed to store the entities found in the same order as the destination lists them.

For example, Suppose the source has a Category term reference field with term names of Product, Foo, Foo-blue stored in that order. Passing the term names to the entity_lookup plugin will cause the destination entity created to have the destination field stored in this order: Foo, Foo blue, Product

This is because for multiple values, a single query with "IN" is used for multiple values in EntityLookup->query method. The results of an 'in' set, will have whatever the ordering of the index used to look them up. Generally alphabetical / numeric ascending.

Order of entities in the fields is important for a lot of reasons. E.g., matching the taxonomy hierarchy, ordering related posts by most relative, ordering paragraphs correctly, and the like.

The fix for this is to perform individual queries to find the values and then aggregate them in the proper order.

Patch attached in the next comment.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

cgmonroe created an issue. See original summary.

cgmonroe’s picture

Status: Active » Needs review
FileSize
1.29 KB

Here is the patch.

ndeet’s picture

Hi,

I faced the same issue on one of our migrations. I came up with the almost same solution like you. (Unfortunately I did not find this issue when taking a look if anybody run into it already.)

As you have done the work already I will try to do my first review, hopefully it is constructive:

  1. +    if ( !$multiple ) {
    +      $value = array($value);
    +    }
    

    You can avoid that check completely if you do a typecast in the foreach below (see below).
    Otherwise if you want to keep that code, codestyle nitpick: between brackets there should be no spaces. if (!$multiple)


  2. +    foreach( $value as $aValue ) {
    

    1. As mentioned above you can do the typecast here:

    foreach ((array) $value as $aValue)

    2. Codestyle nitpick: No spaces between brackets but after "foreach"


  3. +      foreach ($result as $key => $info ) {
    

    Codestyle nitpick: no space before closing bracket
    Maybe also newline above?

Thoughts about your solution (which is the same than I came up with):
The only thing which bothers me a bit is that the performance of the 'IN' solution might be better on huge arrays. The above solution causes some overhead as we do a DB query for each item (dunno if this really is a problem, but it might be on huge datasets). I checked EntityQuery if there is another way to fetch multiple entities but I found none - maybe someone with more D8 knowledge has some hints.

One solution might be to add support for 'ORDER BY FIELD' to EntityQuery:
http://stackoverflow.com/questions/2609986/mysql-where-in-ordering

But maybe there is already a solution baked into D8, would be great if anybody with more insight could chip in.

ndeet’s picture

Status: Needs review » Needs work
Les Lim’s picture

Status: Needs work » Needs review
FileSize
1.11 KB

One solution might be to add support for 'ORDER BY FIELD' to EntityQuery

This seems unlikely to be supported, since FIELD() is MySQL-specific.

One possibility to save queries is to implement static caching for each queried result, which at least prevents the same query from being executed over and over. Ultimately though, I'm less concerned about the performance aspects, since migrations generally don't happen in production environments.

+++ b/src/Plugin/migrate/process/EntityLookup.php
@@ -213,14 +213,29 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn
+      $result = $query->execute();
+
+      if (empty($result)) {
+       continue;
+      }
+      foreach ($result as $key => $info ) {
+        $results[$key] = $info;
+      }

$query->execute() always returns an array here, and since we're just concatenating key/value pairs together, we can use array arithmetic:
$results += $query->execute();

New patch attached for that and the style fixes in #3.

heddn’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Great fix. Thanks for the contributions.

Can we add a quick test on this so we don't ever get a regression? This seems like something we'd accidentally loose at some point if we don't test it.

Chris Dart’s picture

Version: 8.x-3.x-dev » 8.x-4.1
Status: Needs work » Patch (to be ported)
FileSize
1.29 KB

Updated patch migrate_plus-2833260-5.patch to work with 8.x-4.1

Les Lim’s picture

Status: Patch (to be ported) » Needs work

Reverting status.

Chris Dart’s picture

Version: 8.x-4.1 » 8.x-4.2
Status: Needs work » Patch (to be ported)
FileSize
1.38 KB

Updating the patch to work after changes to version 4.2.0

Les Lim’s picture

Status: Patch (to be ported) » Needs work

Resetting the status again; the status "Patch (to be ported)" is meant for work that has already been merged into one branch, and now needs the patch backported to other versions/branches. This issue is still technically in "Needs work", since it still requires written tests.

nicholasThompson’s picture

Version: 8.x-4.2 » 8.x-5.x-dev
FileSize
1.43 KB

I've ported the patch to work with Migrate Plus 5.1 (well, 5.x as of writing).

Worked for me.

What kind of test coverage do we need / expect to get this moved forward?

heddn’s picture

When this plugin was made, it was supposed to make it easy to import simple values into simple destinations. If we have complicated sort orders that make the use of this module hard, perhaps re-thinking how to import the data is needed. Split migration into 2 parts. One for the complicated terms structure. Then import that into nodes via migration_lookup.

NicklasMF’s picture

Patch #11 works for me as well. I'll use the patch until this issue has been solved.

Matroskeen’s picture

Issue tags: +LutskGCW22

Maybe we could do a similar trick as in EntityStorageBase::loadMultiple():

// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid IDs.
if ($flipped_ids) {
  // Remove any invalid IDs from the array and preserve the order passed in.
  $flipped_ids = array_intersect_key($flipped_ids, $entities);
  $entities = array_replace($flipped_ids, $entities);
}

Anyway, let's take a look into it and obviously add some test coverage.

longwave’s picture

Version: 8.x-5.x-dev » 6.0.x-dev
FileSize
1.43 KB

Rerolled #11 for 6.0.x.