Problem/Motivation
Background
Scenario: Source site with N content types. All CT have a node reference field configured to allow referencing nodes of each other CT.
For example:
- Content types: `news`, `events`, `gallery`.
- All of them with a node reference field `field_related_content` enabled to reference bundles: news, events, gallery.
So we create a migration setup with three migrations: myd6_news, myd6_events, myd6_gallery. In order to migrate the node reference field each migration includes this process:
field_related_content:
plugin: iterator
source: field_related_content
process:
target_id:
plugin: migration
migration:
- myd6_news
- myd6_events
- myd6_gallery
source: nid
Since any content type can reference each other, we can't enforce a precedence between migrations. As known, a stub row will be created for referenced nodes not migrated yet. Afterwards, as each migration runs, the stub rows will be properly migrated with the actual contents from the source.
Problems
- The current migration is always used if it is found in the process configuration (ref: https://github.com/drupal/drupal/blob/8.3.x/core/modules/migrate/src/Plu...). In the exposed scenario: when running the `myd6_event` migration, `myd6_event` migration will be chosen to create the stub row.
- Because of 1), the
stub_idconfiguration is ignored. - Because of 1), the stub row is added to the map of the current migration. Later, when the proper migration for the stubbed source row is run, it doesn't find a stub row in its map, because it is in the former migration map, and a second migration of the same source row is performed.
- OTOH the
MigrateExecutableused to perform the import of the stub row is the one of the current migration in spite of the stub migration selected. So the stub row is created using a different process pipeline, leading to errors because it may not be prepared to create stub rows with default values and so. (Related: #2800279: Document that migrations used for stubbing need to deal with empty source values)
Let's ilustrate this with an example:
- Source: node 2 (news) references node 1 (event).
- Run migration
myd6_news. It finds a reference to node 1 that can't solve. So it creates a stub row for source node 1 with destid 101. - The stub row is created using the
myd6_newsexecutable (because 1, sincemyd6_newsis present in the process configuration it is always selected and 2, in spite of the selected migration, theMigrateExecutableof the current migration (myd6_news) is always used) and 1:101 is added to the map ofmyd6_newsmigration. - Source node 2 is migrated with destid 102. 2:102 is added to the map of
myd6_newsmigration. - Run migration
myd6_event. This migration doesn't know about the stub row created bymyd6_newsbecause it is not in its map. Source node 1 is migrated again with destid 103. 1:103 is added to the map ofmyd6_eventmigration.
Proposed resolution
- (?) Respect stub_id configuration or fix documentation.
- Break up
Drupal\migrate\Plugin\migrate\process\Migration::transform()in discrete methods, so it is easy to extend and override. - (?)Add alter hooks to some of the new methods.
- Set the
MigrateExecutableto use the the stubbing migration.
Note: The proposed solution doesn't fix the problem, because we can't know out-of-the-box which migration corresponds to any source row. It only enables developers to build their own solutions.
Remaining tasks
Agree on a resolution.
Write a patch.
User interface changes
None.
API changes
No API changes.
API additions: new public methods in Drupal\migrate\Plugin\migrate\process\Migration::transform().
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #30 | 2842811-30.patch | 9.77 KB | jofitz |
| #25 | 2842811-25.patch | 9.78 KB | jofitz |
| #17 | interdiff.txt | 1.27 KB | pritishkumar |
| #17 | 2842811-17.patch | 10.02 KB | pritishkumar |
| #16 | 2842811-16.patch | 10.04 KB | jofitz |
Comments
Comment #2
jonhattanComment #3
jonhattanComment #4
jonhattanComment #5
jonhattanOk here's a refactoring of the transform() method. It addresses points 2 and 4 of the proposed resolution.
Comment #6
jonhattanComment #7
jonhattanCatch exception in processRow().
Comment #8
jonhattanComment #9
heddnCan I get an update of the proposed solution? And some tests?
Comment #11
mikeryanI haven't looked closely at changes to the migration process plugin itself, but this is a BC break to the migrate module (which is currently in experimental beta status).
Comment #12
heddn#2835586: Allow customization of stub rows from Migration process plugin is making more progress as it just went RTBC. If/when it gets committed, this will need a re-roll.
Comment #13
jonhattanReturn NULL instead of [] when no dest ids found. This is a change to maintain compatiblity. Patch for 8.2.x. Needs reroll on 8.4.x and tests.
Comment #14
jofitzComment #16
jofitzRe-roll of #13 for 8.4.x including improved PHPdocs.
Comment #17
pritishkumar commentedComment #18
jofitz@pritish.kumar Thanks for your corrections and especially for including an interdiff. Next time it would also be useful if you could add a short comment describing the change(s) to save us having to investigate the patch and/or interdiff.
Comment #19
joelpittet@Jo Fitzgerald this patch may need a re-roll once #2864563: Migration lookup process plugin doesn't call setMessage on the migration idMap They are conflicting with these lines currently:
Comment #20
joelpittetAdding a public method isn't a "BC Break" @mikeryan, it's an API addition, or am I missing something?
Comment #22
heddnI'll take a look at reviewing this week.
Comment #23
heddnComment #24
heddnComment #25
jofitzRe-rolled.
Comment #26
heddnGoing to review again.
Comment #27
heddnCan we get a test only patch demonstrating the problem? The problem with the migrate_lookup plugin, is it is so complicated, touching it with a 10 foot pole is dangerous.
Comment #28
heddnAdding something to the executable interface is a break. But I think we can do this in a non-breaking method by adding a new interface that is implemented by Core's executable. Then do instance_of check. Not clean, but it keeps BC.
Comment #29
heddnAlternatively, we could just new up a new executable, no?
Comment #30
jofitzI had hoped to address @heddn's comments in #28 & #29, but with the code in front of me, I realise that I don't really understand what is required.
...so here is a re-roll because the patch in #25 no longer applies.
@heddn Would you mind expanding on your suggestions, please.
Comment #31
heddn#28 suggests that we add a new interface and check if the executable implements this interface before calling setMigration. Let's call this option #1. Alternatively, in #29 I suggest we could just new up a new executable. That would sorta work because that object isn't a service. Its the runner. To determine that, we can run
get_class()against the executable, then new up a new one passing into it the migration. Do this where ever we want to do use setMigration. So, 2 options.Which is better? Why don't we do both? And use the second (more hacky) solution as a fallback. Then we can sunset that approach in 9.x when we can do things more properly.
Comment #33
joachim commentedThis looks like the problem I am currently seeing, though it's not entirely clear from the summary.
Here's what I have:
1. The main migration is running. One of its process mappings uses the migration_lookup plugin, configured to use stubs.
2. MigrationLookup::transform() wants to create a stub row, and then run it through the normal migration process:
(That's the lookup $migration, not the main one! See #2963811: Improve variable names in MigrationLookup for DX!)
3. MigrateExecutable::processRow() is ok to receive a $process pipeline as its parameter:
So far so good!
4. Here is where problems start. MigrateExecutable thinks it knows the migration it is working with -- the main one. It's not told any different.
Uh-oh. It's calling getProcessPlugins() on the *main* migration, with the $process pipeline configuration from the lookup migration!
5. The Migration plugin class is ok to receive a $process for getProcessPlugins(). That's to allow for the SubProcess process plugin, where an inner process pipeline is used. In this case though, it's a WTF! Migration A should not be asked to do anything with Migration B's process pipeline!
6. And now the real problem. In Migration::getProcessPlugins(), every process plugin gets the migration it is working with passed in as a constructor parameter. That's the $this in these two lines:
Of course, this is the WRONG migration now!
Comment #34
joachim commentedI think this is too convoluted. It's going to mean that whenever we jump out of an inner migration, such as a lookup, we have to ensure we restore the previous migration. I see bugs arising from times when we forget to do that.
I was thinking we could add an optional $migration parameter to MigrateExecutable::processRow(), and then this call uses that if it's provided:
That would fix things pretty simply, with the addition of an optional parameter to the interface, which is allowed.
> Alternatively, we could just new up a new executable, no?
Do you mean have MigrationLookup do:
That looks like a good approach too, and cleaner overall. I'm a bit concerned that there may be side-effects we don't know about to having two MigrateExecutable objects in play, since it's been designed to run as a singleton.
BTW:
> Adding something to the executable interface is a break.
As it's not tagged as @api, then this part of the policy at https://www.drupal.org/core/d8-bc-policy applies:
Comment #40
berdirRelated #3156730: Stubs should only be created if the referenced source row actually exists
Comment #43
codebymikey commentedComment #44
huzookaWe have written a replacement lookup plugin and released it in Migrate Magician. I think it does what you expect.
See https://www.drupal.org/project/drupal/issues/2891073#comment-14452582