If you attach fields to ContentEntityTypes using the hook 'entity_field_storage_info' SqlContentEntityStorage will ignore the changes and throw an error if one tries to save an entity with the new field.
What are the steps required to reproduce the bug?
- Create a custom ContentEntityType.
- Attach at least one storage definition to this ContentEntityType using the hook 'entity_field_storage_info'.
- Create required database fields accordingly to the attached storage definition.
- Create an instance of that entity and try to save it.
What behavior were you expecting?
The entity should be stored to the database.
What happened instead?
An exception is thrown by SqlContentEntityStorage saying "Table mapping contains invalid field @FIELD". It refers to the field created via 'entity_field_storage_info'.
Cause
SqlContentEntityStorage uses the method getFieldStorageDefinitions to fetch the field storage definitions from the entity type. For unknown reasons it fetches the data from EntityManager::getBaseFieldDefinitions wich does not include changes made by the hook 'entity_field_storage_info' instead of EntityManager::getFieldStorageDefinitions.
Workaround
Till this on is fixed in core one may create a custom subclass of SqlContentEntityStorage overwriting the getFieldStorageDefinitions method and attach at as storage handler to their entities.
This is expected behavior. Storage Definitions in hook_field_storage_info do not go in the base tables and should throw the exception. Rename the confusing function and make it protected with appropriate deprecations and tests.
| Comment | File | Size | Author |
|---|---|---|---|
| #37 | interdiff.2584729.24-37.txt | 5.79 KB | mikelutz |
| #37 | 2584729-37.drupal.Hook-entityfieldstorageinfo-is-broken.patch | 3.56 KB | mikelutz |
Comments
Comment #3
sebastian-lenz commentedComment #4
sebastian-lenz commentedComment #5
berdirComment #6
ckaotikThe problem still persists as of 8.2.7 and the provided patch works wonderfully.
Setting to needs work as tests are still needed.
Comment #8
kristiaanvandeneyndeCan confirm the problem exists and that this patch cures it. But yeah: Needs tests :)
Comment #10
szato commentedDrupal core 8.5.5: same problem, patch works.
Comment #11
amateescu commentedStill needs tests.
Comment #12
hchonovComment #13
hchonovI hope it is fine to add the additional test coverage to the existing unit test
\Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTestinstead of implementing an entity kernel test to add the fields through the hookentity_field_storage_info.Comment #14
amateescu commentedThe issue summary says that the problem exists when you implement
hook_entity_field_storage_infoto define a field storage, and then try to save the entity.But if you implement
hook_entity_field_storage_info, you are also required to implementhook_entity_bundle_field_info(), otherwise there are no field definition attached to your field storage.Comment #15
amateescu commentedMaybe it's worth continuing the explanation in #14: if you want to define a field storage which is also automatically available to all the bundles of an entity type, you need to implement
hook_entity_base_field_info()instead.Comment #16
hchonovThis should be
$this->additionalFieldStorageDefinitions = $this->mockFieldDefinitions(['bundle_field']);and then the test will fail as expected.@amateescu, but even if you do so then
\Drupal\Core\Entity\Sql\SqlContentEntityStorage::getFieldStorageDefinitions()will still be returning only the base field definitions. Actually this is also what the method documentation states, but it is a little bit too late to change the method name to::getBaseFieldDefinitions().Comment #17
amateescu commentedHm, then I think we need to deprecate it and have a new
getBaseFieldDefinitions()method instead.In any case, the reproduction steps from the current issue summary are based on a wrong assumption, so they're not really accurate..
Comment #18
hchonovCan we probably completely remove the method? It should not even be public. It is only used in
\Drupal\Core\Entity\Sql\SqlContentEntityStorage::mapToStorageRecord().What happens if you implement both
hook_entity_field_storage_infoandhook_entity_bundle_field_infofor a field of the type BaseFieldDefinition, but that field is not returned by\Drupal\Core\Entity\Sql\SqlContentEntityStorage::getFieldStorageDefinitions()? In this case an exception will be thrown by\Drupal\Core\Entity\Sql\SqlContentEntityStorage::mapToStorageRecord(), right?I am looking at the following code inside
\Drupal\Core\Entity\Sql\SqlContentEntityStorage::mapToStorageRecord():This makes me think that we still need all the field storage definitions as returned by
\Drupal\Core\Entity\EntityFieldManagerInterface::getFieldStorageDefinitions()instead only the ones as returned by\Drupal\Core\Entity\EntityFieldManagerInterface::getBaseFieldDefinitions().Comment #19
amateescu commentedThat would be a hard BC break, I think just deprecating it is fine :)
As for the second point, like I said above, I would support having a
getFieldStorageDefinitions()helper inSqlContentEntityStorage.Comment #20
hchonovWell, we already have it :). The only question is what should it return.
Comment #22
kristiaanvandeneyndeJudging from the code I think it's obvious the problem is the documentation and implementation of getFieldStorageDefinitions() is wrong. The actual method name is correct. So simply changing the method implementation to match the name, should suffice.
What I'm missing in this patch is a test-only patch that actually fails and then goes green with the change. This could be done by implementing the field storage/bundle field info hooks in a test module and then enabling that module for the test.
Happy to RTBC when the tests actually show what's broken :)
Comment #23
kristiaanvandeneyndeP.S.: I've seen other places in core where the fact that bundle fields are both storage and field info causes issues. Maybe this was yet another one of those places :) One of them being #2898635-6: bundleFieldDefinitions() are not added in EntityViewsData, which made me write #2930736: EntityViewsData assumes BaseFieldDefinitions where it should use FieldDefinitionInterface
Comment #24
abrammHere's a patch from #12 updated according to #16, just to make sure new test works as expected.
Comment #26
kristiaanvandeneyndeYup seems good. As explained in #22, the method name was correct but the documentation and implementation wasn't. This fixes that.
Comment #27
catchI can see just changing what the method returns, but would like a second opinion from an entity maintainer on this one - while the method might not be doing what it was supposed to, there could be code relying on the current behaviour out there.
Comment #28
plachI'm not sure there's anything actually broken here, aside from the inconsistency between the method name and its implementation/documentation:
::mapToStorageRecordis meant to work with shared field tables (e.g.node,node_field_data,node_revision,node_field_revision), so retrieving only the base field definitions is the right thing to do, because only (single-value) base fields are stored in shared field tables. In a way it also makes sense to call the method::getFieldStorageDefinitions(), because those are the storage definitions the storage handler needs to deal with in this particular context.As pointed out by @amateescu in #14/#15, a storage definition alone makes no sense from the Entity Field API point of view: it either needs to be "part of" a base field definition or complement a set of bundle field definitions.
Therefore, any storage definition being also a base field definition will be correctly handled by
::mapToStorageRecord, while any other one should be ignored because bundle fields are stored in dedicated tables, and so they should not be returned when dealing with shared field tables. The error currently reported in the IS should be fixable by implementinghook_entity_bundle_field_info()besideshook_entity_field_storage_info(), see the (recently updated) docs.If I'm missing something and things are actually broken, it would be good to update the IS with correct STR and provide a test-only patch that actually shows what's wrong. The current one is just asserting a different expected return value for
::getFieldStorageDefinitions(), which in itself tells us nothing about what's actually broken.Otherwise, I'd just rescope this to deprecate
::getFieldStorageDefinitions(), we should make it a protected helper in D9.Comment #29
plachActually, the fact that the patch in #3 is not failing tests makes me think that we are missing some test coverage.
Comment #30
kristiaanvandeneyndeFrom #16:
SqlContentEntityStorage::getFieldStorageDefinitions() is public. So even if there is some undocumented logic you need to be aware of as shown by #28, the method name is still very much misleading for what it's actually doing.
Also, could you please answer #18, @plach?
Also, a test was added in #24 that fails on the current code base and passes with the changes. But that test does not go as far as to implement both hooks to demonstrate that saving an entity with a bundle field currently causes an exception to be thrown.
---
Finally, at a glance it really seems like SqlContentEntityStorage tries to save all fields on an entity, including bundle fields. So if we don't load their storage records, we will get an exception. This seems to match my findings from a couple months ago where I implemented both a storage and bundle field hook and still got errors when trying to save my entities.
Comment #31
kristiaanvandeneyndeEntityBundleFieldTest seems to cover everything just fine, though. But I do recall having serious issues with custom defined bundle fields/storages a while ago (early 2018). That said, the test's module seems to do more than simply implement those two hooks, maybe that's the key?
I'll see if I can cook up something to confirm it's actually still broken.
Comment #32
kristiaanvandeneyndeThe following was enough for it to work properly:
So I guess whatever was broken a while back seems to work fine now. There are still other woes with bundle fields (see #23), but this no longer seems to be one of them.
Which leaves us at method name vs method implementation and how we might fix that? Maybe the IS and title need a change?
Comment #33
plach@kristiaanvandeneynde:
#30:
Agreed. I think we should just deprecate it since it offers no real value over retrieving base field definitions from the entity field manager. Ideally it would have a been a protected method named
::getBaseFieldStorageDefinitions(), or something along those lines.If you define a field via the
hook_entity_field_storage_info()/hook_entity_bundle_field_info()hooks, you are defining abundlefield, regardless of the class you are using to provide the definition.::getFieldStorageDefinitions()is not meant to deal with bundle fields, because it was introduced and is used only to deal with shared-field table field records, which store only (single-value) base fields. I don't expect any exception to be thrown because the new field won't be listed among base fields.Of course it does, but it treats (single-value) base fields and all the other fields in different ways: the former are stored in shared-field tables, while all the other fields get dedicated tables.
Nope, sorry :)
As long as
::getFieldStorageDefinitions()is only used by the::map*methods, it's perfectly fine to return only base fields, actually it's the right thing to do. The only problem here is that the method was introduced as public and with a misleading name, so people might actually be using it in unintended contexts and with expectations not matching the original intents. However that's not the official API to retrieve field storage definitions, so there's little we can do for them, besides not breaking their code by changing the method's behavior.#32
As mentioned above, I think we should rescope the issue to introduce a protected
::getBaseFieldStorageDefinitions()method and deprecate::getFieldStorageDefinitions()in favor of the official API :)It would also be good to add a test making sure that if in the future we attempt to do something like #3, we actually get some test failures. That is, we should assert that a code-defined bundle field is a) stored in a dedicated table b) loaded from a dedicated table.
We might also try to leverage this issue to throw an exception in the entity field manager, if we have an orphan storage definition with no corresponding bundle definition or viceversa. @amateescu, what do you think?
Comment #34
plachThe reason why I'm asking confirmation is that I think that, for configurable fields, it might be ok to have only a field storage definition without bundle field definitions, at least in very specific parts of their life-cyle, that is while purging the data of the last occurrence of a bundle field. Not 100% sure about this, though.
Comment #35
kristiaanvandeneyndeGreat feedback, thanks @plach! I agree with what you suggested at the bottom of #33 (in reply of #32).
The exception might need more input from others, though. I'm thinking of a module shipping with an exported field storage definition that may be used by the module further down the line. For instance, Group ships with a
field.storage.group_content.group_roles.ymlthat is only instantiated whenever a group type is created. I didn't use the hooks because Views is still choking on bundle fields. When using exported field config, it does work.Comment #36
amateescu commentedI think the same reasoning from #1426804: Allow field storages to be persisted when they have no fields. applies to in-code field storage definitions as well, so it should be valid to have a storage definition without any bundle definitions. However, the other way around is not valid, so we should throw an exception if we encounter any bundle definitions without a storage definition.
Comment #37
mikelutzThis is as far as I got tonight, still would like to add a test that fails on #3
Comment #38
mikelutzComment #40
amateescu commentedThis method is being deprecated in #2554235: Make the content entity storage and entity query use the last installed definitions instead of the ones living in code, so I think we can close this one.
Comment #41
ddavisboxleitner commented