Problem/Motivation
EntityConfigBase migrations cannot update existing configs due to the lack of an appropriate getEntity implementation.
It defaults to Entity:getEntity which only takes into account the first destination id, however configs typically have multiple IDs which should be concatenated by a ..
This means that in every destination plugin which overrides EntityConfigBase (and do not override the import method), if the Drupal\migrate\Plugin::import() method is called with a non-empty $old_destination_ids param, the import process will fail, because instead of operating on a preexisting entity, the destination plugin tries to create a new one (with the same ID which was calculated based on the destination IDs of the current migration Row).
Affected destination plugins are e.g.
entity:field_configentity:field_storage_configentity:base_field_overrideentity:block
Proposed resolution
Implement appropriate getEntity which takes into account the full config ID.
Remaining tasks
- Add a test for the abstract
EntityConfigBasemigrate destination plugin class - Implement a BC-safe fix.
User interface changes
N/A
API changes
N/A
Side effects
The current EntityConfigBase makes it impossible to track changes of migrations with multi-ID config entity destinations, but this is not evaluated as a bug.
Fixing the issue will make change tracking possible in these scenarios.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3118262
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
codebymikey commentedComment #3
codebymikey commentedComment #4
mikelutzThe ability to rerun and update migrations using the EntityConfigBase destination is not something currently supported by drupal core, but I think it would be useful to add in this ability if we can do so. We would need to add a good number of tests to make sure that this works, so I'm setting to NW for tests. Additionally, the getEntity method in the patch appears to just be a copy of the parent getEntity() method, which is incorrect for reasons I've noted below. I like the idea, but we need to prove that it works.
Comment #5
codebymikey commentedHi Mike,
It's not an exact copy, the main difference being the code used to fetch the entity ID, the rest of the code are fine as they previously were:
vs.
which is used to load against the first destination id value (which is typically the node ID, or revision ID).
And yes, you're definitely right that it needs more testing, especially with languages. I just wanted to raise the issue so that it was at least public, and could be taken further by other interested parties.
Comment #6
mikelutzWow, total fail to copy my notes on that last comment.. My apologies.
What I meant to say was that it appeared the parent method was copied with just the line changed that you needed changed. My point was that if you are going to have a config specific getEntity method, then the rest of the method should be written specifically for config entities. Specifically these notes (which I really thought I posted above)
Config entities don't have bundles. This is unnecessary
Config entities can't be stubbed. This is unnecessary.
I don't think we need 'enforceIsNew' with config entities. enforceIsNew lets us set a id on a content entity and save it without the system thinking it's an update, config entities are defined by their id, so this shouldn't be necessary.
Comment #8
chandrashekhar_srijan commentedrerolled for 9.x. Also applied suggestions made in #6.
Comment #9
codebymikey commentedHi @chandrashekhar_srijan,
Thanks for the patch, but I think it also includes changes for a different functionality.
The only thing left for this issue after incorporating #6 are test cases.
Comment #10
chandrashekhar_srijan commentedRegarding #9: I have rerolled the patch for 9.2.x and addressed the suggestion in #6. You are right in pointing that Test cases are still to be done. Will try to take it up if time allows me.
Comment #12
huzookaImho we only have to override the inherited getEntityId() method, basically repeating what's already done for the initial migration (when the original destination IDs array is empty on import).
Comment #13
huzookaThis actually breaks change tracking of migrations with config entity destination.
Comment #14
huzookaThat's true. Although, why is core testing track_changes? I guess because it should be supported.
At least this suggests that: https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugi...
Comment #15
huzookaAdded a test, but it isn't explicitly testing
\Drupal\migrate\Plugin\migrate\destination\EntityContentBase,Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfigorDrupal\migrate\Plugin\migrate\destination\EntityFieldInstance: Its a functional testing of track_changes for field storage and field instance migrations.I hope that it works for all of us, or at least it's a good starting point for a further improvement.
Comment #16
huzookaCancelled the Mysql tests (let's first test with SQLite, it's much faster)!
Comment #17
huzookaAdding a fix only patch.
Comment #18
huzookaComment #19
huzookaComment #20
huzookaThe fix in #19 is not BC-safe – but it reminds me of #3200936: DX: Block (config) destination shouldn't recalculate block config ID for block translations .
Comment #21
huzookaComment #22
huzookaComment #23
huzookaI was able to clean up the language override import as well (detecting of a translation was broken – the condition was wrong).
But the major update is that I added a test for EntityContentBase, which shows why this issue should be classified as bug.
Comment #24
huzookaComment #25
huzookaComment #26
huzookaComment #27
huzookaExplaining the fix:
This was the root of the issue: If there are more than one destination IDs, every subclass tried to load the entity with the very first one. Apparently was fine in 80% of the situations, but newer worked in case of
field_configandfield_config_storagedestinations....so I moved the original
$entity_idvalue assignment into this new protected method....and I override it in
EntityConfigBase.I also "fix" the
getEntityId()method – this ensures that we will have the right destination entity ID even when we have a preexisting configuration entity. Why this should be possible? Let me give an example!If you have a source Drupal 7 site with e.g. a
field_imagefield with cardinality-1(unlimited), then you are not able to migrate the data into your destination site: Standard profile provides its ownfield_imagestorage, but with cardinality set to1. The most inconvenient part of the problem is that you only notice that something went wrong when you already migrated your data as well (if-you-ever-notice-it).If we ensure that every config entity destination is able to find a preexisting entity (please keep in mind that this is the case for node types, vocabularies, etc), then we can also prevent these cases!
I wasn't able to find out what should be done here, but the comment above helped A LOT.
Notice the
$langcode = $this->configFactory->get('langcode');line! This was actually loading an immutable config. Do you know which config is expected here? I do not, I think core never had a configurationentitywithlangcodeID.But anyway, comparing this object never matches the value of the
langcodedestination property – which means that without fixing this logic, we are not able to fix this bug.So I went ahead and changed this condition. From now on, it will work what its comment describes.
Comment #28
huzookaThe reason why you cannot see the first test's failure message in #23 is #3197324: Exception trace cannot be serialized because of closure.
But!
The second tests failure message clearly reflects the bug:
I think I can remove the needs tests tag.
Comment #29
huzookaPatches in #23 can be applied on 9.2.x and on 9.1.x as well.
Comment #30
codebymikey commentedWith regards to the failing test on the test-only patch on #23:
It's related to #3197324-7: Exception trace cannot be serialized because of closure and PHPUnit, and it's intercepting what the original error should be.
And can be bypassed with the following (I think that's the most appropriate fix I can think of, but it still feels a little hacky):
Comment #31
huzookaRe #30:
I'm able to get a meaningful message even without changing anything out of the kernel test: https://git.drupalcode.org/project/media_migration/-/blob/8.x-1.x/tests/...
But these are out of scope (and core won't solve that issue class by class imho).
(And I already mentioned it in #28)
Comment #32
wim leers#15: wow, I literally have nothing to remark about your test-only patch!
A few questions and nits on the complete patch:
This is technically a BC break … 🙈Already fixed in #19 👍🥳
This improves the config translation handling I think? 🤔I think to be able to change this we'll need explicit test coverage for this too. And arguably it is out of scope here? 🤔I can easily be convinced that this is necessary though, because core doesn't have alangcodeconfig nor does there seem to be anything else trying to get it…Confirmed by #27.5 👏👏👏
Nit: s/in for form/in the form/
#19: nice — updating my review of #15 now :P
#23: This test also looks superb. Only a nit:
🤔 Nit: not sure what "followings" means?
@huzooka I don't think I've ever seen a core patch of this test complexity be done so well on the first iteration. Impeccable work. Thank you for your excellent work!
I'd RTBC this, but I think this kind of change needs migration system maintainer review.
Comment #33
codebymikey commentedRe #31
I had drafted my findings a couple minutes before you made yours, but didn't submit it in time since I had to tend to something else.
The try catch is a much clever workaround for the test case. I just thought it was worth documenting my particular workaround and solving it in core (to avoid other third party test cases from failing in the future).
Core already has a similar fix in
DependencySerializationTraitwhen it attempts to unserialize other complex objects.There's about 7 calls to
__wakeup()in core so should be a bit more manageable.But yeah, you're right, discussions on this are out of scope for the current issue. The comment was just as an FYI.
Comment #34
huzooka@codebymikey, no problem :)
Asking for subsystem maintainer review according to #32.
Comment #35
wim leers#34: d'oh — I forgot about that tag!
Comment #36
quietone commentedTime to start looking at this.
I started with the title. Can the title be changed to state what this is doing 'Allow EntityConfig migrations to track changes' or something. (It is rather harsh to claim this is broken when, at the time and as far as I recall, the migration of configuration was considered a once off).
Then I read the IS. The steps to reproduce says to install Migrate Tools, which is true. But it is also true that using Migrate Run or Drush 10.4+ will also allow one to reproduce the problem.
I then went to review the code.
This is the same summary as an existing method in Entity. I suggest adding an explanation of when and why to use or the other.
The reformatting here makes it difficult to find the change. Plus the operators are at the end of the line when, typically, they are at the beginning of the line as has been done for the ternary operations elsewhere in the patch. The change is to use
$entity->language()->getId()instead of$this->configFactory->get('langcode'). Why is this change needed?If the name is not changed then there is only one line changed here and makes this is easier to review.
The summary and the return description are identical to the summary for method generateId. I think at least the summary heres needs to be expanded to explain the difference.
Not a fan of the name, JoinIds. Yes, it is descriptive but makes me think of SQL joins. I couldn't think of a better name so browsed the config system and I saw buildCacheId. How about buildId?
s/Ids is/IDs are/
I took a brief look at the tests and I did not see any testing for translated sources, which I looked for because of the changes to the if blocks at the beginning of \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity. Did I miss something?
I hope to continue during this week.
Comment #37
duaelfrPer #32 and #36
Comment #38
duaelfrMy bad, those changes that I reverted were useful. Now migrate tells me that it updated the entities but it didn't...
I brought them back and made the minore changes asked in #36.
I did not expand tests to cover the translated source case (and I don't plan to, given my limited skills).
Comment #39
huzookaSince you bumped this issue back on my dashboard, there is a better chance of me addrssing it :)
Comment #41
quietone commentedI looked at the tests now. They look fine and appear to test what is needed (but it is late in the day for me). Just a few suggestions.
We normally alphabetize the $modules list.
Expand the comment to make it clear that the destination is being changed.
"Verify the when the destination configuration changes but the source does not change, that nothing is migrated."
Add a blank line here to separate the rerun from the initial migration.
s/migrations/migration/
Because this only does one migration.
We normally sort the list of use statements.
This will be easier to read if the array spans lines.
s/entity has/entity has/. The last sentence is difficult. I understand parentheses are used for extra information that may be useful but then the exclamation mark implies that this is import. All together it is confusing. So, remove the parentheses around the last sentence and change the exclamation mark to a period.
Out of scope.
Comment #42
ankithashettyUpdated the patch in #38 to address the changes suggested in #41, thanks!
Comment #43
mikelutzTrack changes is a completely different thing in migrate world. Additionally, this isn't a bug. The config destination is currently specifically documented to not support updates through the migration system. Adding that functionality is a feature request, not a bug.
Comment #44
volker23 commentedI have used the patch from #42 on Drupal 9.2.2 to update my migration 'upgrade_d7_field' and it processed right away instead of throwing lots of ''field_storage_config' entity with ID 'xyz' already exists errors. Although the specific update i was trying, didn't work for other reasons, the migration update worked!
Thanks!
Comment #45
huzooka@mikelutz,
What if I keep only EntityConfigBaseTest (so remove the extra change tracking test)? I strongly believe that this issue is about a bug (it is obvious for me).
But fixing the bug of
EntityConfigBasehas a side effect: change tracking will be possible for migrations using destination plugins with multiple destination IDs (which are extendingEntityConfigBase).Comment #46
huzookaRemoved the track change test, and also an unneeded comment.
Comment #48
wim leersSeems like quite a few people/migrations would benefit from this. Lots of collaboration on this issue. Bumping for another round of review 🤞 (I can't RTBC this I think.)
Comment #49
quietone commentedTook a look again and found a few things.
Why not just $row->getDestinationProperty($this->getKey('id'))? It will return NULL if the property does not exist.
If I am incorrect on that point then if
$row->getDestinationProperty($this->getKey('id'))is changed to use use$destination_id_keyit will be easier to read.When is this block of code starting here code executed? The new test does not reach this.
s/subsequent/update/
Because subsequent does not convey that what is being done is an update, not just running say 'drush mim' again.
I'd probably change the variables to use update instead of subsequent as well.
From #27,
The first thing that I notice here is that this in migrating to a Drupal destination site with the standard profile but migrations are to be run on a minimal install. However, I did try this on HEAD. I made a D7 site, standard install and change the image field of the article to be unlimited. Then I made a node that had three unique images. I then made a D9 sites, standard install, enabled migrate_drupal_ui and ran the migrations. The image field was correctly changed to unlimited and 3 images were correctly migrated to the node.
And finally what about a testing of a rollback?
Comment #50
quietone commentedComment #52
narendrarPatch rerolled for 9.3.x
Comment #53
huzookaI ran into this issue without trying to use any contribs:
#3260352: Executing d7_entity_translation_settings and d7_language_content_comment_settings migrations in the calculated optimal order results in migration error
Comment #58
duaelfrRerolled patches in a new MR
Addressed some comments from #49 (points 1 and 3)
Comment #59
duaelfrHiding files now there is a MR
Comment #60
duaelfrJust rerolled on latest 11.x.
This still needs point 2 from #49 to be fixed.
Comment #61
duaelfrPatches for composer users <3