Problem/Motivation
\Drupal\Core\Field\RequiredFieldStorageDefinitionInterface and its only method ::isStorageRequired() have been introduced in #2390495: Support marking field storage definitions as required.
BaseFieldDefinition implements that interface and method and also provides its own setter method ::setStorageRequired().
The method implementation \Drupal\Core\Field\BaseFieldDefinition::isStorageRequired() however falls back to \Drupal\Core\Field\BaseFieldDefinition::isRequired() if the setter has not been used.
This means that defining a field like this:
$field = BaseFieldDefinition::create('string')
->setRequired(TRUE);will result automatically into a field, whose storage is required and the following will return TRUE even without it being explicitly set:
$field->isStorageRequired();
However ::isStorageRequired() is documented like this:
* If a field storage is required, NOT NULL constraints will be added
* automatically for the required properties of a field type.
Core is not doing that yet automatically, but there is an issue to implement that, which is in an almost finished state - #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field.
So now we have 2 methods for whether a field is required - isRequired and isStorageRequired, where the first is used for form inputs and validation purposes, while the second is used to flag the storage of the field in a shared table as required - for example by adding the NOT NULL constraint to the schema as described in the method's documentation.
Please note that we even already have an implementation in core, which is checking whether a field is storage required and if so a NOT NULL constraint is being added. From \Drupal\comment\CommentStorageSchema::getSharedTableFieldSchema():
case 'entity_type':
case 'field_name':
assert($storage_definition instanceof RequiredFieldStorageDefinitionInterface);
if ($storage_definition->isStorageRequired()) {
// The 'entity_type' and 'field_name' are required so they also need
// to be marked as NOT NULL.
$schema['fields'][$field_name]['not null'] = TRUE;
}
break;
Unfortunately there are 4 problems with ::isStorageRequired() falling back to ::isRequired():
- If we clone a required base field and return it as non-required from an implementation of
hook_entity_bundle_field_info()for a specific bundle, then we'll still not be able to save an entity for that bundle without providing a value for the overridden field as the storage might already be created with aNOT NULLconstraint for that field. - It is impossible to make the base field non-required after it was installed even if we haven't explicitly requested the
NOT NULLconstraint. - We cannot yet properly update
\Drupal\Core\Field\BaseFieldDefinition::createFromFieldStorageDefinition()and let it inherit thestorage_requiredflag, because it is wrong to set the flag if it wasn't set on the passed field storage definition, butTRUEis returned because of the fallback to::isRequired(). - This issue is also a blocker for #3014760: [PP-2] Create an abstract composite field definition/storage definition and update BaseFieldDefinition to use it.
The proposal there is that the field storage definition becomes a property on the base field definition in order to achieve a clean separation between a field storage and a field definition similar to configurable fields. As such it is not possible for the storage definition to call its parent - the field definition from within::isStorageRequired()and access::isRequired().
As the use cases and meaning of a required and storage required are different a check for storage required should not fall back to required.
It is worth mentioning that regarding bundle fields only FieldConfig is aware of the required property and FieldStorageConfig does not know anything about it.
Only FieldConfigInterface extends the FieldDefinitionInterface where ::isRequired() is defined. The FieldStorageDefinitionInterface does not extend FieldDefinitionInterface.
This does not match BaseFieldDefinition where everything belongs together, but only on bundle fields we have a clear separation between what is a field definition setting and what is a field storage setting.
Currently there is no workaround for allowing a field on a specific bundle to not be storage required, if a field has gotten a NOT NULL constraint as documented just because it has been simply flagged as required but not as storage required. Core might not be adding the NOT NULL constraint automatically yet, but has documented that it should be added if the field is storage required and doing that in custom or contrib code is therefore perfectly valid and according to the core's documentation. Considering the 4 different problems this is not to be confused with simply being a documentation issue.
Steps to reproduce
The test from the attached patch in #2 - \Drupal\KernelTests\Core\Entity\EntityFieldTest::testRequiredChangeOnBundleOverride() is showing an example of how core behaves with and without automatic NOT NULL constraint based on the output of ::isStorageRequired().
There we create a required base field and two bundles, where for the second bundle we return an altered base field definition by flagging the field as non-required.
This represents a perfectly valid use case as we might want something to be optional on one bundle, but required on another.
Then we create and save an entity for the bundle where the field is not required.
First we do that with current behaviour where NOT NULL constraints are not yet added automatically based on the output of ::isStorageRequired(). We assert that the entity is saved properly.
Then we recreate the entity tables by using a different storage schema handler, which adds the NOT NULL constraint to storage required fields. As one can see from the test results we cannot anymore save an entity for the bundle without providing a value for a field which isn't required on that bundle, but is required on the first bundle.
Proposed resolution
As the use cases and meaning of a required and storage required are different a check for storage required should not fall back to required.
Therefore we need to remove the fallback to ::isRequired() from within ::isStorageRequired().
If we want a field storage to be required, then we should explicitly request that by calling ::setStorageRequired(TRUE).
Remaining tasks
Review.
User interface changes
None.
API changes
BaseFieldDefinition::isStorageRequired() does not fall back to isRequired anymore.
If we want a field storage to be required, then we should explicitly request that by calling ::setStorageRequired(TRUE).
Data model changes
None.
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #46 | 3083131-nr-bot.txt | 144 bytes | needs-review-queue-bot |
| #28 | 3083131-28.patch | 13.08 KB | hchonov |
| #28 | interdiff-26-28.txt | 1.16 KB | hchonov |
| #26 | interdiff-3-26.txt | 645 bytes | hchonov |
| #26 | 3083131-26.patch | 11.71 KB | hchonov |
Comments
Comment #2
hchonovComment #3
hchonovThis patch removes the fallback to
isRequiredfrom withinisStorageRequired.In #2885809: The 'entity_type' and 'field_name' base fields on Comment are required the
entity_typeandfield_namefields have been flagged as required, but not as storage required, which we should do not explicitly. However even if the fields have been flagged as required only, in the storage schema was checked if they are storage required before adding theNOT NULLconstraint.Comment #4
hchonovComment #5
hchonovComment #6
hchonovAdding a unit test to ensure that a required field is not automatically a storage required field.
Comment #7
hchonovComment #9
hchonovComment #11
hchonovI've just created a change record - https://www.drupal.org/node/3083233 .
Comment #12
hchonovComment #13
darren ohPatch looks good, tests pass, and fixes an annoying bug.
Comment #14
darren ohComment #15
tstoecklerSorry, but I would like to take a look at this first, so moving back to "needs review". In general I think the current fallback is correct and I'm not sure the I agree with problem description. It may be that the patch is in fact correct, but I would like to spend some time reviewing it first. That said it's fine for another Entity API maintainer to sign off as well, so not assigning to me but marking for subsystem maintainer review.
Comment #16
berdirYes, maybe this was problematic when done initially in 8.3 (IIRC).
But, changing it again now is just as problematic in the opposite direction, as evident by all the update functions and changes you have to do.
I still don't quite follow. The logic above this is an isset(), meaning you *can* set setRequiredStorage(FALSE) explicitly and then setRequired(TRUE)?
> If we clone a required base field and return it as non-required from an implementation of hook_entity_bundle_field_info() for a specific bundle, then we'll still not be able to save an entity for that bundle without providing a value for the overridden field as the storage might already be created with a NOT NULL constraint for that field.
As discussed, the storage-level settings are not *meant* to be changed per-bundle. But the above approach should work fine to override it per bundle?
Comment #17
hchonovCould you please elaborate on what you see as problematic? Btw it is only a single update and not multiple update functions.
Requiredis not a storage level setting and it never was. The documentation ofsetRequiredorisRequiredalso was never updated that they will result into databaseNOT NULLconstraints. Only the documentation ofisStorageRequiredexplains that.The test explicitly doesn't touch the
storage requiredflag but changes only therequiredflag. I've also written this in the issue summary that both have different meanings.Once again from the issue summary:
I see this as a regression and the test demonstrates it. It was possible to make a base field
requiredin a form and for validation purposes on one bundle andnon-requiredon another bundle, which is a perfectly valid use case and I hope others see it like this as well.Now because of mixing storage with entity and form validation this makes it impossible and therefore it is clearly a regression and not only this but suddenly
NOT NULLconstraints will be added around without them being explicitly requested (assumed one has implemented storage schema like documented in core).Please note that the issue and the test deal only with the
requiredand not thestorage requiredflag. I have no intentions of changing a storage setting and core should neither have any intentions of putting aNOT NULLin the database constraints only because I've made a field required for the form and for validation purposes. It should be up to the developer to decide.As I am not sure if I understand this correctly, I would ask you to describe exactly what you mean, so that there is no confusion :)
Comment #18
hchonovI am sorry, I've missed that sentence.
It is currently possible to (quoting you) "set setRequiredStorage(FALSE) explicitly and then setRequired(TRUE)". However one should explicitly turn off the
storage requiredand this should happen before the entity schema is installed, because afterwards it will not have any effect and theNOT NULLconstraint will be already there.Quoting @jibran from #3003586-3: [PP-4] Use setStorageRequired() instead of overriding the storage schema to mark fields as NOT NULL in the database:
I fully agree with that. Imagine now that it gets much more complicated to remember that
::setRequired(TRUE)leads to::isStorageRequired()returningTRUEwithout having calledsetStorageRequired(TRUE). And on top of that one has to think of that::setRequired(TRUE)leads to aNOT NULLconstraint on the storage level? This is a pure DX nightmare and makes things much harder to fix in the aftermath.Comment #19
jibranI totally understand the edge case and the issue make sense but it is not a critical IMO. There is a clever way to get around this by using
setRequiredStorage(FALSE)and then write an update function to update the storage.BFD is always been a wired mixture of both field storage definition and field definition. Maybe #3014760: [PP-2] Create an abstract composite field definition/storage definition and update BaseFieldDefinition to use it will clear things up in the future.
Personally. I'm kind of indifferent about this issue and the reason behind that is when you say the base field is required what does it really mean?
Does it mean
FieldDefinitionis required orFieldStorageDefinitionis required?Right now we are making an assumption that if the base field is required then it means storage is required as well. If you don't want storage to be required then set it explicitly to FALSE via
setRequiredStorage(FALSE). Maybe we need to document this better.In #2390495-11: Support marking field storage definitions as required @Berdir said:
If a base field provider is making this decision that storage is not optional then it should set storage required explicitly instead of relying on field definition.
and then
We are only fixing core here and leaving contrib and every other custom entity which is already using this as default behavior in limbo.
So yeah, I'm neither here nor there for this issue but I do agree with this
setRequiredandsetStorageRequireddance is wired and difficult to understand.Comment #20
hchonovIt is really confusing now. However we have two methods for that -
isRequiredandisStorageRequiredcorrespondingly.BaseFieldDefinitionandFieldConfigimplementFieldDefinitionInterfaceon whichisRequired()is documented:This is what a required field / base field is and the main purpose is the validation.
Then we have
RequiredFieldStorageDefinitionInterfacewhich documentsisStorageRequired()asRequiredFieldStorageDefinitionInterfaceis currently implemented only byBaseFieldDefinition.I hope this clarifies the purpose of required and storage required and that as documented in the corresponding interfaces they have different purpose and therefore we should not implicitly fall back from the one to the other.
Exactly my point!
Comment #22
amateescu commentedThe documentation for
\Drupal\Core\Field\RequiredFieldStorageDefinitionInterface::isStorageRequired(), which contains the confusing part:was written with the expectation that this issue would land shortly after it, in the same minor core release. Obviously that didn't happen, so that documentation is wrong at the moment in HEAD, which makes this more a documentation problem rather than a critical bug.
Comment #23
hchonovWell the documentation isn't completely wrong because depending on the output of
\Drupal\Core\Field\RequiredFieldStorageDefinitionInterface::isStorageRequired()NOT NULLconstraints are already being added in HEAD.Even if we automatically do this everywhere the current issue remains a real problem and not only a documentation issue. I've explained in the issue summary why I consider this critical.
Comment #24
hchonovIt is worth mentioning that regarding bundle fields only
FieldConfigis aware of therequiredproperty andFieldStorageConfigdoes not know anything about it.Only
FieldConfigInterfaceextends theFieldDefinitionInterfacewhere::isRequired()is defined. TheFieldStorageDefinitionInterfacedoes not extendFieldDefinitionInterface.This does not match
BaseFieldDefinitionwhere everything belongs together, but only on bundle fields we have a clear separation between what is a field definition setting and what is a field storage setting.This issue is also a blocker for #3014760: [PP-2] Create an abstract composite field definition/storage definition and update BaseFieldDefinition to use it.
The proposal there is that the field storage definition is a property on the base field definition and as such it cannot call its parent from within
::isStorageRequired()and access::isRequired().Comment #25
hchonovComment #26
hchonovComment #28
hchonovLooks like I forgot to add the schema file...
Comment #29
hchonovComment #30
mkalkbrennerI read through the description and all the comments of the issue.
This comment expresses best the "WTF" that came into my mind:
Setting a field to be required in it's definition should not lead to have a NOT NULL constraint at the database level as default.
The default should be that the field exists in the database without a constraint if our API offers an explicit function setStorageRequired(TRUE) to request that constraint.
NOT NULL constraints should never be implicit, but explicitly requested by the developer or architect at some given point.
As a developer for example I don't want to be surprised by database exceptions if I change an application and declare a field to be required for the future. Or if I import existing entities and missing a required field but I want to ensure that it gets added on editing.
Again, adding the NOT NULL constraint at a given point should be an explicit decision. A developer should not be forced to have to suppress it actively. IMHO most contrib developers could deal better with "required" fields and the corresponding checks on higher layers like the form API or entity constraints compared to a database exception. If you get a NOT NULL constraint violation exception from the database layer it would be hard to find out which field caused it and why. And in most cases you want have a chance to deal with it in your business logic.
Therefore this issue and the suggested solution makes totally sense to me. And the tests show that core won't break.
Comment #31
alexpottI don't think #16 has been accounted for. What about code that is relying on the current behaviour. We have code like that in HEAD atm that's why
this update function.
I'm pretty uncomfortable with an change to entity storage that is going to necessitate contrib and custom writing update functions to ensure their expectations around data integrity are maintained. I'm going to ping @catch and @plach as core committers deeply involved in the entity system.
Comment #32
hchonovWhat exactly has not been addressed in my following comments #17 and #18?
I don't think that anyone feels comfortable about this, but we should not forget that the change resolves the introduced regression and mitigates the adverse effects before we start providing
NOT NULLconstraints automatically for all entity types based on::isStorageRequired().We should think about developer reactions when they start receiving
NOT NULLconstraints in the storage just because they've flagged their fields required for entity and form validation. Or even worse, when they realize that it is impossible to save an entity form for a bundle where the requirement has been disabled.I think that all this is much more important than the currently proposed change.
Additionally it also prevents a clear separation that is planned in #3014760: [PP-2] Create an abstract composite field definition/storage definition and update BaseFieldDefinition to use it.
Comment #33
hchonovThe only explanation about the fallback from #2390495-11: Support marking field storage definitions as required is :
Which is not explaining the reason for the fallback in the first place.
In the very same comment you read the following as well, which has not been accounted for with the introduction of the fallback:
This makes me think that it might have been just an oversight adding the fallback.
Comment #34
darren ohI understand that unexpectedly impacting an important site would have a negative effect on future support for Drupal. However, this does appear to have been an unintended behavior that has negative consequences. Is there a way we can deprecate and warn developers who depend on this behavior that they should update before the next release?
Comment #35
hchonovWe could trigger a deprecation if the fallback returns
TRUE, however how many releases from now should go on with this regression?I would be fine with deprecating it in 8.8.x, but remove it in 8.9.0 where hopefully the automatic
NOT NULLhandling will be added.Comment #36
mkalkbrennerI bet that there's not a single developer out there in contrib who explicitly set a field to be required using its defentition in order to get a NOT NULL constraint at the database level!
If the constraint won't be there, the code will still work like expected due to the checks on the PHP level.
If we should warn someone, we should warn developers that they might have created NOT NULL constraints by accident.
We could also tell them to revise their database constraints ;-)
But we must not declare this bug(!) as a feature instead of fixing it.
Comment #37
catchOK a couple of ideas, I'm not sure what the best course of action is:
It's arguable both ways whether this is a pure bug fix or also includes an API change. I do think we can change the behaviour in 9.0.x to what's in the current patch.
For 8.x there are three options I think:
1. Just make the change, with a change record
2. Deprecate the old behaviour with @trigger_error() (might be able to squeeze that into 8.8.x).
3. Make the change, but trigger_error() if we hit the fallback to alert developers who might be relying on it (an actual notice/warning, no @)
I think #2 is the only approach that could really go into a patch release at all, #1 and #3 we might want to move to an 8.9.x follow-up then to get this fixed in 9.0.x and something into 8.8/8.9 first?
Comment #38
hchonovIf we deprecate it in 8.8.x then can we remove it in 8.9.x with a change record?
#2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field is ready (depends only one other issue which is RTBC) and hopefully can be committed to 8.9.x, however the current issue is a blocker for it as if someone decides to use the new schema they will get
NOT NULLconstraints unnoticed.---
I could propose one more solution which might make it easier to commit earlier - next to the deprecation in 8.8.x, write an update that explicitly sets the
storage_requiredflag toTRUEin the installed storage definitions if it has aNOT NULLconstraint in the field schema for all tables it is contained in. This will ensure that the flag is kept on the installed storage definition in the cases someone relied on the fallback to::isRequired(). During the update we could notify through a log message. We could even add a special detection for this in theEntityDefinitionUpdateManager::getChangeList(). This way only new installations will be affected, but not existing ones. Actually even without this existing ones will not be affected as they've already got the NOT NULL constraint in their databases if they relied on::isStorageRequired().---
I would therefore propose something like this.
Add the deprecation in 8.8.x if
::isStorageRequired()falls back to::isRequired()and returnsTRUE.In 8.9.x remove the fallback and through an update set explicitly the
storage_requiredflag toTRUEin the installed field storage definitions for required (::isRequired()) fields which had aNOT NULLconstraints in their installed field schema and write a log message for each updated field. For this we should exclude the entity keys which by default get aNOT NULLconstraint. Prior the release of 8.9.0 publish a change record about the change.Additionally add a special detection for storage_required change in base fields in the
EntityDefinitionUpdateManager::getChangeList(), which will be useful anyway.Comment #39
hchonovActually we could detect even better if a
NOT NULLconstraint is being added because of a fallback to::isRequired()in::isStorageRequired():1. Retrieve all non entity key fields for which
::isStorageRequired()returnsTRUE, but haven't being explicitly set as storage required.2. Intersect those fields with the ones that have a
NOT NULLconstraint in at least one table in their installed field schema.3. Retrieve a clone of the original field storage definitions for those fields and turn off the storage requirement -
->setStorageRequired(FALSE).4. Retrieve the current field schema for the modified storage definition.
5. If a
NOT NULLconstraint is not added anymore then the fallback was used and we should explicitly update the installed storage definition with->setStorageRequired(TRUE).If a
NOT NULLconstraint is still added then this happens based on some other condition like the field name for example and there is nothing to do.------
Edit:
I've proposed this as the most safe way, however given the fact that nothing really will break for existing site installations I don't think that such an update is justified.
New installations will simply not get the NOT NULL constraint in the database, but will still work
Comment #46
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.