Problem/Motivation
In Drupal 8, node translations are all stored in the same node as their default language node while in Drupal 6 & 7 they were stored in separate nodes. After a migration, entity reference field may now reference non-existent node translations, as show here:
Proposed resolution
Write a new deriver that will generate a migration for every entity bundle containing an entity reference field. These migrations will then update the entity reference field values with the new IDs found in the mapping tables using the migration_lookup process plugin.
Since those migrations need to be derived based on the migrated fields, we can't derived them at the same time as the other migrations since the fileds won't exists on the D8 site at that time. We need the migrations on which they depend to generate them after they have been succesfully executed.
Remaining tasks
Write the test and patch
Test it some more
Review it
Commit it!
User interface changes
N/A
API changes
New interface MigrationWithFollowUpInterface for migrations with follow-up migrations.
Data model changes
N/A
| Comment | File | Size | Author |
|---|---|---|---|
| #87 | 2912348-87.patch | 58.82 KB | maxocub |
| #87 | 2912348-87-without-fixtures.patch | 35.95 KB | maxocub |
| #87 | interdiff-2912348-86-87.txt | 818 bytes | maxocub |
Comments
Comment #2
maxocub commentedTagging.
Comment #3
heddnReviewed in weekly maintainers meeting and Adam is going to look at doing this with deriver.
Comment #4
phenaproximaHere's how I think we could approach this.
Obviously there will be a lot more to this, but that's the basic idea. I'll try to write this and see how far I get.
Comment #5
masipila commentedSubscribing so that I can help testing this when the patch arrives.
Comment #6
maxocub commentedI'll have some time in the next few days to take another shot at this.
Comment #7
maxocub commented@phenaproxima: Things are going pretty well here, I have a working deriver and I think I'm on the right track, except for on key point from #4:
My million dollar question: How can we know which migration to lookup?
Comment #8
phenaproximaI ran into this as well. My solution (for now): just hard-code it.
Comment #9
maxocub commentedThis patch is still a work in progress, I just wanted to upload it so we can discuss the approach.
What I understood was that this migration was kind of a D8 to D8 migration, in the sense that we would loop through all migrated entity reference fields and update the ones that are pointing to a translation.
The problem is that when the deriver is executed, the migrations have not run yet so there's no entity reference field on the D8 site yet, and then no derived migration is created.
I guess that the approach is wrong and we should try to derive the migrations based on the fields of the source site, if that's possible?
Or is there a way to make the deriver run after the migrations? I don't think so.
Comment #10
phenaproximaThis is a misunderstanding. The entity_references migrations *are* only supposed to be discovered, and executed, after the D6/7 -> 8 migration process has completed. So the derivers will discover the fields; we just have to execute these migrations as a bunch, separately, after the main D6/7 ones are completed.
Comment #11
maxocub commentedAnd how can we do that? With migration dependencies?
Will it be doable both from the UI and with
drush migrate-upgrade --configure-only?EDIT:
Nevermind, I just read the Migration.php file and I see that it can be done using the requirements (or dependencies?) I think. I'll try that.SECOND EDIT: Hmm, doesn't seem to work.
Comment #12
phenaproximaHow about an event subscriber in Migrate Drupal? I'm not 100% sure what event we'd be reacting to, although I can see some ways to do it.
For example:
It's a bit clumsy, but it could work. I'm open to other ideas...?
Comment #13
maxocub commentedOK, I have to admit that I'm stuck. I hit several walls trying to solve this thing. Here's two things I tried:
Version 1:
This is what have bee suggested in the previous comments of this issue.
The new migration is being derived based on the migrated fields on the D8 site. So it have to run and be derived after some required migration have been run. To do that, we put that in state in a hook_migration_plugins_alter() and we check if the dependencies have been run in an eventsubscriber.
This seems like a weird thing to do and I haven't been able to make it work.
Version 2:
I also tried to derive the new migration based on the values in the source database, this way we don't have to try to make it derived after all the others. I didn't had anymore success with this approach.
I would appreciate if someone could look over those two version to see if I'm doing something wrong, and if one approach is more promising thatn the other.
Damn this is a hard problem.
Comment #14
masipila commentedI decided to document the problem statement mainly for myself to understand the challenge @maxocub, @phenaproxima and @heddn were discussing in the maintainers' meeting this week.
Drupal 7 source data model
The data relations look like this in Drupal 7:

Expected result in Drupal 8
The data relations look like this in Drupal 8:

Challenge
The migrations we're trying to write here MUST be derived *after* all other relevant migrations are run, because they derived based on that data and at the time they are derived, there's no migrated fields on the D8 site.
Two phase migration as a possible solution
The concept that was discussed in the maintainer meeting was something as follows:
Next steps
Did this accurately cover the concept that was discussed this Thrusday?
Comment #15
maxocub commentedRe #14: Thanks @masipila for this great detailed summary of the problem! I might update the IS with this since it's so damn clear.
Meanwhile, here's yet another WIP/POC. This just works from the UI, but the migration are now derived at the right moment.
The problem now is that my nodes are not updated, I get this error when the entity reference migrations are run:
A translation already exists for the specified language (fr). (/home/maxocub/drupal8/core/lib/Drupal/Core/Entity/ContentEntityBase.php:726)I'll take that as progress and as a small hope that we might find a solution.
Comment #16
catchWhat about something like the following?
1. As the main migration is running, add every node with a reference to a queue if content translation is enabled in the source.
2. In the queue runner, check the reference fields to see if items are in the reference map and re-add items to the queue if they aren't
3. We'd then need to run through the queues. Drush migrations could add a drush queue-run at the end. UI is trickier, but there are cron runs if nothing else.
Since this is something that can happen without any access to the source database, it might be OK if the post-process step isn't technically a migration.
Comment #17
heddnI think another part of the issue here is that we don't have a good reliable way to know when all migrations are completed. When can we trigger this second set of migrations? If you run drush, you can trigger migrations individually. If you run the UI, then we know its a one shot migration. But what about rollbacks? Do we then need to clean out the queue? What about partial rollbacks. Do we only clean out part of the queue?
Also, we don't have a way to rollback the d8->d8 migration if we don't use the migrate ecosystem.
So, to address these concerns... what about if we have a service that can be called from migrate_drupal_ui or drush? This service generates any new migrations needed. I'd envision this being part of the upgrade form/wizard in Core. And in Drush, I'd see it as a drupal_set_message that gets triggered whenever someone runs a migration that has i18n data in it. Then the runner of the drush migration knows they have to run a new command. Which this new command will generate a bunch of new migrations. And when the user desires, they can run these new migrations.
The d8=>d8 migration path is already solved in contrib with https://www.drupal.org/project/migrate_drupal_d8. We'd need to maybe clean things up a little, but that project has pretty decent test coverage and could be brought into core.
Comment #18
catchYou can't really clear out a queue as such since items can't be introspected unless they're claimed. When processing the queue, we'd need to check the id_map and return early for anything that's not there, then the queue item goes away via that early return. Possibly an additional check that the uuid in the queue item and the uuid of the queue entity matches, in case a migration has been run, rolled back and run again in-between.
Generally any queue that's doing something after node save needs to do something like this (at least checking that entities still load), since you never know if the node is going to be deleted before the queue item is processed.
If that's simpler than doing the queue it sounds like it could work, but also feels like adding an explicit manual step in the middle of quite a long process. For a very long migration that takes hours, it's going to take some extra bash scripting to generate and call those additional migrations at the end (as opposed to running a queue via drush which is just an extra line or two).
Comment #19
heddnThe last patch here looks pretty close to what I would have expected. However, I think we should build a service to trigger the derriver and such as I mentioned in my last comment.
Comment #20
heddnI think 'd7_node_translation' is returning the nid of the most recent revision. Hmm, no. It is returning nid + langcode. Or it should be. I wonder if the source plugin isn't pulling the correct language code and nid combination?
This seems like almost an exact copy of the functionality in http://cgit.drupalcode.org/migrate_drupal_d8/tree/src/Plugin/migrate/sou.... Perhaps we should adopt it into core instead? I had some test coverage. Were this new source plugin has none.
Comment #21
maxocub commentedRe #20:
Comment #22
heddnI also wonder if we've also stumbled on a failing test case for #2921661: Add support to migrate multilingual revisions. Because we'd want to key the data in the mapping table by nid and langcode. Even on the revisions.
Comment #23
maxocub commentedI have not given up on this one, I'll be working on it this week, especially during the Migrate Sprint on January 11 & 12.
@heddn: How can we get migrate_drupal_d8 into core? It would be super helpful here. Open a new issue and postpone this issue? Or add it right here?
Comment #24
heddnre #23: If we are pretty sure it will help, then open a new issue and postpone this is probably a good idea.
Comment #25
heddnComment #26
quietone commentedThis issue to get migrate_drupal_d8 into core has been created, so this is postponed on that . #2935951: Copy migrate source plugin from migrate_drupal_d8 into migrate_drupal
Comment #27
maxocub commentedWhile #2935951: Copy migrate source plugin from migrate_drupal_d8 into migrate_drupal is not yet finished, I copied the ContentEntity source plugin and it's deriver from there to show how it will be useful here.
The new service is just a start, I'm not quite sure what it should do exactly and how it should do it. For now it just returns the post process migrations.
The new form step is also not ideal. It would be nice to have a separate form for those post process migrations, for if someone doesn't want to run them right away after the upgrade and would like to review the migrated content and come back to run them later.
I added a short test for D7 to show that it actually works, I don't have a D6 site right now but I'll add a test for it another time.
There's a major lack of comments in the code to explain what's going on, I'll also fix that later.
In summary, this is still just a work in progress, but it show that it IS progressing!
(No interdiff because to much changed)
Comment #28
heddnI'm a fan of breaking out the form step into a new form with a new route that can be run separately. See #2918761: Break up MigrateUpgradeForm into smaller forms for where we might start doing something like that for the rest of the form.
Comment #29
maxocub commentedHere's a new problem that I stumbled upon. Imagine a simple scenario with Drupal 7:
But on D8 this is different, the default autocomplete widget of the entity reference field (or the select field and the checkboxes for that matter) will only show entities in the same language as of the current entity. (See #2144377: Entity reference autocomplete lists entity labels only in current content language)
So what will we do when we want to migrate a D7 site with entities that refer to entities in other languages?
One solution that comes to mind would be to try to detect those cases, warn the users about it, and point them to a documentation page where they can learn how to create an entity reference autocomplete view that shows entities in all languages.
Comment #30
catch@maxocub I think that's worth a separate issue, but for me it shouldn't block migrate being stable.
Comment #32
maxocub commentedDiscussed in the weekly migrate meeting. Since D8 entity reference field only refer to IDs and no langcode, theres no way to solve #29 with migrate. We should at least document it.
Comment #33
masipila commentedAdded the documentation tag. The correct place to document this is the 'known issues' page in the upgrade handbook. https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-d...
Markus
Comment #34
heddnThis is no longer blocked.
Comment #35
maxocub commentedJust a re-roll, for starters.
Comment #37
jofitzFix one of the test failures.
Correct the coding standards infractions.
Comment #39
maxocub commentedThis should fix the tests. But why is there one additional field_config and field_storage_config migrated, that I don't understand yet.
Comment #40
maxocub commentedTests are green, back to work.
Comment #41
maxocub commentedIt would be nice if #2918761: Break up MigrateUpgradeForm into smaller forms could land before this one, but I don't think we should postponed it.
For now, I just moved the post process form in its own class with a new route so that it can be accessed easily after a migration.
It still needs a lot of work, like checking if post process migrations are ready to be run and displaying the post process migrations that will be run. I'm on it.
Comment #42
maxocub commentedStill a work in progress but I would appreciate reviews to make sure it's in the right direction and to know your opinion on the approach.
Comment #43
heddnJust quick notes during the migrate meeting.
This should be 'I18n Content' or something. And adjust the recent category/tagging tests.
Bikeshed name here. Could pick a better name.
Comment #44
phenaproximaWe discussed this issue at length during the Migrate maintainer call this morning. Here's what we're thinking:
Migrations already fire a post-import event when they are complete. Migrate Drupal UI and Migrate Tools/Migrate Upgrade should subscribe to this event and handle it in different ways. For migrations which will require additional "clean-up" work (we decided to get rid of the "post-process" verbiage, since that sounds like it has something to do with the process pipeline)...
This dual approach is the best of all possible worlds. For users who want a one-click, "just works" solution, the Migrate Drupal UI will do all the work. For power users who want to use Drush and heavily customize things, Migrate Tools will tell them what they need to do, and how they need to do it, but leave the actual work to them.
Comment #45
heddnFor the second point about drush in #44, I think the subscribed event would let us know that we just ran a migration that could necessitate running a
migrate:generate-post-migrations(or some such) command. That way if someone isn't done with all the migration rebuilding yet in their custom world, they have time to futz with things before they then run the generate command. After running the generate command, then folks can simply rundrush mim --tags="I18n Content"ordrush mim --all* "I18n Content" is perhaps a little premature naming , but replace this with whatever we land on for tagging.
* "migrate:generate-post-migrations" is perhaps a little premature naming , but replace this with whatever we land on for the drush command.
Comment #46
maxocub commentedHere's another patch trying the post migration event idea, and I think it's in fact way better.
The migrate upgrade batch is now listening for POST_IMPORT events and if the migration that just finished implements MigrationFollowUpInterface, then it generates it's follow-up migrations and adds them to the current batch process.
I will now try to add tests for D6 and functional tests that shows that this event approach really works.
No interdiff because so much has changed.
Comment #47
maxocub commentedHere's tests for Drupal 6.
I had to modify the NodeReference field plugin because it was creating stubs of translated nodes that were never converted to real nodes on the D8 site.
Still needs some functional tests.
Comment #49
maxocub commentedFixing the test.
Comment #50
phenaproximaThis patch looks great and makes sense. Amazing work, @maxocub!
I have more points of review, but these are just the initial set of minor things I found.
I suggest we set the default destination to the 'null' destination here, and have the deriver adjust it later.
For type safety, let's pass TRUE as the third argument to in_array(). Also, we should move this if check outside of the enclosing try block.
We should probably expand this doc comment, but that doesn't need to happen just yet. Let's sic @masipila on it :)
Same here. I also think we should rename this method to defineFollowUpMigrations() or generateFollowUpMigrations(), because I think 'get' is too vague.
I think the migrate_drupal_post_process tag is not applicable anymore :)
d7_node_translation is the name of the migration, not the mapping table.
Is "for the plugin ID" necessary?
I think (but could be wrong) that we can use getSetting('target_type'), rather than dereferencing from getSettings().
Let's pass TRUE as the third argument to in_array().
I'd rather we just mapped the ID, revision ID, and langcode keys than every base field.
Comment #51
maxocub commentedAll points in #50 are addressed, except for the interface docs.
Comment #52
maxocub commentedHere's an attempt to test that the follow-up migrations are successfully run in the migrate_drupal_ui functional tests.
Comment #54
maxocub commentedFix for the failing tests and the coding standards message.
Comment #55
maxocub commented@phenaproxima: About #50.10, I just realized that the idea behind getting all the base fields dinamically like I did before was because this migration can have any entity types as its source & destination, so the NID, VID & langcode won't always be the required base fields.
Comment #56
maxocub commentedHere's a new patch with two improvements:
Comment #57
catchDidn't do a detailed review yet but the approach is really encouraging here and I couldn't see any obvious problems at all.
Comment #58
phenaproximaI love it. I think we are on the right track and getting close to home stretch. This work is heroic.
This is really clever!
I'm not a huge fan of hard-coding magic tags; maybe we can make this a config setting somewhere, or find another way to make it a little less concrete?
Doc comment needs substantial expansion, and I think we should rename this to FollowUpMigrationInterface.
Can "with" be changed to "...to point to one of the supported target entity types"?
I don't think we need this isset() check.
For readability, can we set up the $derivative in its own variable before adding it to $this->derivatives? It'll just make the lines below easier to read.
The entity type cannot be revisionable wihtout a revision key, so I don't think we need the implicit check of $revision_key.
Same here.
Should probably have TRUE as the third argument here.
I'm not entirely clear on the value that this code adds. Wouldn't it be enough for the test to assert that the follow-up migrations made the appropriate changes to the nodes they affected? What do we gain by checking the imported count?
Comment #59
masipila commented@maxocub, absolutely superb work with this issue!
I can do some wordsmithing for the FollowUpMigrationInterface docblock but I would need some input from you. Could you try to summarize this on a conceptual level with a couple of bullet points and I'll then catch up from there?
Edit: no need to focus on the conceptual background / problem statement. That one is clear (#14 summarizes that if anyone else is interested) but it would be great if you could summarize the solution.
Cheers,
Markus
Comment #60
maxocub commented@phenaproxima: Thanks for the review, I'm on it.
@masipila: Thanks, I'll summarize it for you.
Comment #61
maxocub commentedMigrationWithFollowUpInterface&FollowUpMigrationInterface? The first one should be implemented by migrations needing follow-up migrations, and the second one by the follow-up migrations themselves. This way we could get rid of the tag and do aninstanceof?issetcheck is necessary here because if an entity type has more than one entity reference field, we want to update the derivative and not override any entity reference fields previously added to the process pipeline. Another way to do it would be to key derivatives by entity types, bundle & field name, but I don't think it would be better.Comment #62
heddnFirst, this is looking really nice. Thanks for all the hard work on this.
#58.2/#61.2: we got around that for other magic tags by adding a config option to migrate_drupal. These tags only have special significance in that context any way. Perhaps we can write an update hook and set some config that way? That gets around hard-coded, per se. But interface checks are another fine way to go about this.
Is this true if multiple ER fields exist on a single entity type and bundle?
There is a derivative separator const in PluginBase, but I'm not sure we can use it here. To bad.
A comment explaining why we are doing this could help.
Comment #63
maxocub commentedThanks for the review!
Comment #64
heddnThis has gone through a couple rounds of feedback. All of which is now addressed. Let's try RTBC on for size.
Comment #65
maxocub commentedBack to NW because of missing documentation and change record.
Comment #66
maxocub commentedDiscussed in the weekly Migrate meeting. Things left to do:
$this->derivativesat the end, as suggested in #58.6I'll work on these tomorrow.
Comment #67
maxocub commentedRe #66:
Points 1 & 2 have been addressed, let's see if the tests pass.
Comment #68
maxocub commentedRe #66:
Point 3 is now done, at least a first draft.
Comment #69
maxocub commentedIS update.
Comment #70
maxocub commentedTypo in the image path.
Comment #71
maxocub commentedChange record draft created.
I'm leaving the "Needs Documentation" tag added in #33 because we need to document the problem mentioned in #29 in the Known issues handbook page.
Comment #72
maxocub commentedA re-roll was needed.
Comment #74
phenaproximaSelf-assigning for review, today.
Comment #75
phenaproximaDiscussed this issue in the Migrate call this morning and we decided to open a follow-up issue to talk about #29 and how we might go about fixing it.
Comment #76
maxocub commentedNeeded a re-roll.
And follow-up created: #2959839: Migrating entity reference fields pointing to nodes in different language now point to current language
Comment #77
phenaproximaThis patch is magnificent. It's a really elegant solution to an extraordinarily complex problem and it makes me glow with pride that I had a hand in conceiving and reviewing it. Truly, it's a privilege to be a part of this team.
I have no major complaints to lodge against the patch. I am itching to RTBC it. I do have some questions -- nothing blocking -- and very minor changes/typos. Let's land this.
Can we inject this? If not, or not easily, I have no problem deferring this to a minor follow-up.
s/migration/migrations
s/taged/tagged
I'm not seeing this used anywhere else in the deriver, so we can probably remove it.
array_key_exists() will be easier to read here.
I'm not sure we want to use the += operator here, because it only merges the top level of the array. I could be totally wrong here, but don't we want to merge recursively? NestedArray::mergeDeep() might be better here.
s/follow-ups/follow-up
I think executeMigrations() accepts MigrationInterface objects, so we can just pass $follow_up_migrations directly.
s/follow-ups/follow-up
I could be wrong, but I don't think we need array_keys() here.
The event subscriber is a service, so is there any reason this needs to be static?
This bit could use a few comments.
Do we really want to overwrite $followUpMigrations here? Don't we want to merge the generated follow-up migrations into it instead, so that we can execute multiple follow-up migrations in one request?
Why is there a new field?
Comment #78
maxocub commented+=does not work here, nor does theNestedArray::mergeDeep(). To make sure the deriver supports multiple entity reference fileds on the same bundle, I added a second field in the fixtures and corresponding tests.executeMigrations()expects migration IDs.executeMigrations()expects migration IDs.Error: Using $this when not in object context.$followUpMigrationproperty. The follow-up migrations are added to the batch immediately (and removed from the property) after the migration on which they depend has been executed. Otherwise they were added to the batch multiple times. I added a comment.Comment #79
maxocub commentedFollow-up created for #77.1: #2961226: Use dependency injection in MigrationConfigurationTrait.
Comment #80
catchOnly question for me is why does this need to be configurable. I've used drush migrate-run before and it's nice to tag custom migrations to isolate them from everything else, if that's the use-case then great.
Otherwise I think this is ready.
Comment #81
phenaproximaWe're following an established pattern in Migrate Drupal, where "magic" tags are configurable. So it's mostly just to keep things consistent.
Comment #82
heddnAnd the established pattern was because of drush migrate-run being able to create custom tags.
Comment #83
phenaproximaI have only one complaint:
Can we make these assertions easier to read by converting them to the format:
$this->assertSame('13', $node->get('field_reference')->target_id)?And really, that's a nitpick that need not block RTBC. So...
(To the tune of "YMCA", by the Village People): R! T! BC!
Comment #84
maxocub commentedI made the changes asked in #83.
I put it back in NR because I had to change the visibility of the
$followUpMigrationTagsproperty fromprotectedtoprivateinMigrationConfigurationTrait. Otherwise, and I never got this before today, I get this error when I run themigrate_drupal_uifunctional tests:Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeTestBase and Drupal\migrate_drupal\MigrationConfigurationTrait define the same property ($followUpMigrationTags) in the composition of Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase. This might be incompatible, to improve maintainability consider using accessor methods in traits instead. Class was composedComment #85
alexpottRe the trait error and the private property. It is happening because MigrateUpgradeExecuteTestBase and MigrateUpgradeTestBase both use MigrationConfigurationTrait. There were no properties on the trait till this patch so no error. But now there is a property - there is a problem. We can remove the "use MigrationConfigurationTrait" from MigrateUpgradeExecuteTestBase and make the property protected again and everything will be fine.
Comment #86
maxocub commented@alexpott: Thanks for explaining that, I was baffled.
Comment #87
maxocub commentedI Forgot the Trait was also used in
MigrateUpgradeReviewPageTestBase. This should fix the failures on PHP 5.6.Comment #88
phenaproximaAce. I'm so glad we have the wisdom of @alexpott at our disposal. Back to RTBC!
Comment #90
catchCommitted/pushed to 8.6.x and cherry-picked to 8.5.x. Thanks!
Comment #92
gábor hojtsyYay, thanks all!
Comment #93
sinasalek commentedYeah finally, thanks everyone. can't believe it finally landed :O
Comment #96
wim leersThe change record at https://www.drupal.org/node/2955658 never got published 😨😅
So … just published it! Thanks to @huzooka for discovering this 🤓