Problem/Motivation
Field storage and entity type CRUD operations (e.g. install a new field in an update function, delete an entity type in an update function, etc.) need the ability to initialize a table mapping "on demand", mostly because they need to work with the last installed definitions instead of the ones living in code.
This is currently done by switching the entity type definition in the storage itself just for the duration of the CRUD operation, which is a "documented hack" with tons of @todos saying "fix this in #2274017: Make SqlContentEntityStorage table mapping agnostic ".
Proposed resolution
- Move the table mapping initialization code from \Drupal\Core\Entity\Sql\SqlContentEntityStorage::getTableMapping to the table mapping itself.
- Make the table mapping aware of the entity table names, and also generate default table names if the entity type doesn't provide them, just like the storage does.
Remaining tasks
Review.
User interface changes
Nope.
API changes
Nope.
Data model changes
Nope.
| Comment | File | Size | Author |
|---|---|---|---|
| #33 | interdiff.txt | 888 bytes | amateescu |
| #33 | 2916018-33.patch | 23.66 KB | amateescu |
Comments
Comment #2
amateescu commentedHere's a patch lifted from #2274017: Make SqlContentEntityStorage table mapping agnostic which, unlike that one, contains only the minimal changes needed in order to be able to instantiate the table mapping from outside the storage.
Comment #3
tstoecklerI think this makes a lot of sense. We had discussed where to put the initialization code when originally introducing the table mapping and at the time the table mapping was thought to be more of a value object without any logic, which is why we put it in the storage handler. It has since shifted its purpose a bit and the code is now a bit alien to the storage handler. Also, any way to remove complexity from the storage handler - even if it is simply moved verbatim to a different class - is appreciated as the storage handler is quite a monster.
All this is regardless of #2274017: Make SqlContentEntityStorage table mapping agnostic . I just wanted to provide some reasons why I think this makes sense as a separate issue regardless of the progress we make there.
Not RTBC-ing because, as this is both a very complex and a highly critical part of core, I want to scrutinize the moved code and its test coverage a bit more before RTBCing. Didn't see anything noteworthy from a cursory look, though.
Comment #4
plachIf we are moving towards deprecating the ability to specify shared table names in the entity type definition, which is a Good Thing™, and we are also decoupling the instantiation of the table mapping from the storage handler, we should retain the ability for individual entity types to determine the shared table names.
I'm wondering whether it would make sense to use a factory, as we do for entity queries, so that an entity type can provide an alternative table mapping with its own shared table names.
Additionally, a factory may help us in keeping the instantiation logic out of the table mapping itself, which would keep it as a value object (more or less). I remember @yched suggesting that maybe we would need a separate table mapping class for each layout we support (base, translatable, revisionable, both), but maybe encapsulating the instantiation logic in a factory would be enough for now, and could still allow us to implement that later without BC issues.
Thoughts?
Comment #5
amateescu commented@plach, that sounds interesting and I'm curious where exactly would developers be able to specify the shared table names with your proposal :)
Like @tstoeckler said in #3, the actual goal of these issues should be to lower the complexity of the storage and storage_schema classes, and allow us to fix all those outstanding @todos about "do this in a better way when we're able to instantiate a table mapping from outside the storage".
Comment #6
plachWell, by being able to specify a table mapping for each entity type we would be able to individually override table names, either by assigning a different value on construction or by doing that directly in the factory method, I didn't think about the details honestly.
Agreed, so coming up with a proper way to instantiate the table mapping seems definitely in scope to me :)
Comment #7
amateescu commentedOk then, wanna go ahead and try to work on a PoC patch? Probably based on #2 because that one already does a lot of the heavy lifting needed.
Comment #8
plachNope :)
I don't think I have time to work on that, right now. Feel free to move on with the current approach, if you don't have time or will to take #6 into account, but please make sure trying it later doesn't imply a BC break, by using @internal or whatever is needed ;)
Comment #9
amateescu commented@plach, I don't mind working on it but the details matter here, that's why I asked how do you see the factory approach in #5. The issue is that the entity query factory service has been deprecated since 8.3 and the instantiation logic has been moved to the storage handler.
If we apply that pattern here it would mean that it's still the storage's responsibility to instantiate the table mapping, which is exactly what we're trying to change in this issue :)
Comment #10
plachSorry, I misunderstood your earlier comments :)
I wasn't aware about the Entity Query factory deprecation. Maybe it could still be acceptable if we delegated the table mapping factory instantiation to the storage handler? This would only pass the entity type id and the entity type manager to the factory, while the entity type and field storage definitions would still be parameters passed to the instantiation method, e.g.
Edit:
::getTableMapping()would only check that the entity type id matches the one the factory was initialized with.Comment #12
amateescu commentedI thought a lot in the past few days about how to proceed with the factory approach from #4 / #10, but I feel that it really needs a proper discussion on its own, so I will open a dedicated issue for it.
In the meantime, this patch is pretty much the cornerstone of my plan to clean up the entity storage API, so any reviews are very much appreciated :)
To answer #8, this patch does not prevent in any way the possibility of having a dedicated factory for the table mapping (or separate table mapping classes), it simply moves the table instantiation code from the storage class to the default table mapping itself.
A happy consequence of this move is that it makes
DefaultTableMappingan actual value object, since its state can no longer be changed from the outside and immutability is one of the basic characteristics of a value object.Here's a new patch which removes all the unnecessary changes, hopefully making it easier to review.
Comment #13
amateescu commentedSince @tstoeckler said in #3 that he'd like to understand the test changes better, here's an overview of a couple of points that might seem strange before thinking everything through:
This change and the similar ones below it are needed because now the table mapping will *always* have a base table.
I guess this can be seen as a behavior change, but what would be the point of an empty table mapping in the first place?
This is another change that might look awkward, but the "new" behavior is actually in line with the fact that revision metadata fields are the only ones that are stored in the revision table and nowhere else.
Also,
testGetFieldTableName()and its data provider,providerTestGetFieldTableName()are not really testing what they think they are.The point of
TableMappingInterface::getFieldTableName()is to return the proper table name for various field definitions: (non-)revisionable, (non-)translatable and single/multi value, but right nowtestGetFieldTableName()is basically testingDefaultTableMapping::setFieldNames():)Comment #14
berdirVarious comments below, also on parts of the code that is just moved here. I'm not suggesting to change any of that, just trying to find my way back into the topic by going through this a bit and connecting some pieces and thinking out loud. Feel free to ignore anything that is not strictly relevant in your opinion and/or get back to it later in other issues.
Commented over there, this is just moved but I honestly think it's more than unlikely that we will be able to do this any time soon. Best we can hope for for now would be to offer defaults that can easily be overridden?
And if we'd really get there at some point then I guess they would still remain as a read-only API on this object, so this is yet another step where this is going into the right direction.
Interesting that we here treat only very specific keys as actual keys (whitelist) while \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getSharedTableFieldSchema() uses a blacklist approach to make all keys except a few not null. I guess things there will change with/after #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field?
Also wondering if there is a specific reason that the bundle is in this list/the base table, I don't think there is a good reason for that?
I've always wondered if there is a reason for that, avoiding duplication of the fairly long field?
This is one of the reasons why I got somewhat stuck on #2875033: Optimize joins and table selection in SQL entity query implementation as it resulted in a regression for a pretty common entity query.
If we would move this to a factory then I guess this would no longer be true but I guess we'd instead inject it in the constructor to keep it immutable so just wrong on the details.
It is apparently still not actually required to pass field definitions but at least the comment is no longer correct like that?
And as you said, this is not really testing that much
Nice cleanup of the mocking required here.
I guess the EntityType classes are not quite plain value objects enough to just use them directly of mocking it and changing that would also result in large changes to the whole class.
Part of the mocking seems a bit backwards to where we want to go in regards to #2232465: Deprecate table names from entity definitions, e.g. defining isTranslatable/isRevisionble based on whether we have the respective tables but we can change that later.
Yep, exactly the BC layer in there is I guess one of if not the main problem to using them as non-mocked value objects in these tests. Too bad :)
(Although I'm wondering if we could instead provide explicit values to skip the BC layer)
Comment #15
amateescu commentedThanks for re-starting the discussion :)
Re #14:
SqlContentEntityStorageSchema::getSharedTableFieldSchema(), so nothing to worry about here :)I'm pretty sure we keep the bundle in the base table for historical reasons, for queries like "give me all nodes of this content type". We could open a separate issue to discuss if that still makes sense with the current core table layout.
setFieldNames()andsetExtraColumns()will no longer be @deprecated, just @internal, so we can still say that the table mapping is a value object.Comment #16
amateescu commented@Berdir convinced me to change the comment for #14.5 :D
Comment #17
berdirThanks, I think this makes sense then as a first step, we have many other/related issues to make things nicer.
Comment #19
amateescu commentedRandom testbot failure.
Comment #20
plachThis is missing a bit: "[...] and has multilingual support."
This looks potentially BC-breaking to me: what if a
DefaultTableMappingclass is instantiated and expected to be empty/clean?Very unlikely scenario, I guess, but what if we added an
@internalstatic::create()method encapsulating the logic of::initializeMapping()instead?This would allow to keep the constructor "clean", and would be closer to an actual factory method.
Isn't
@deprecatedenough here? Are we adding those in case we need to un-deprecate these methods, if we end up introducing a factory? In that case I'd rely on constructor parameters/initial values and keep those methods as @deprecated.Comment #21
amateescu commentedRe #20:
1. Fixed.
2. Adding a static
create()method means that we also need to add getters for base/data/revision/revision data tables, and I wanted to minimize the changes in this issue/patch as much as possible, because all the other work on entity storage depends on it.So.. how about we move all the discussion about table mapping instantiation to a new issue? :)
3.
That's right. And, same as above, we can figure out what needs to happen with these two methods in the instantiation issue. For the moment, I just took the safest approach.
Comment #22
plachWhy is that so? A static method can access protected and private members of a class instance. You just need to instantiate a
$table_mappingin the::create()method and replace all occurrences of$thiswith$table_mapping:I was asking because both @xjm and I found weird that we are adding both the
@deprecatedand the@internaltags:@deprecatedshould be enough to tell people they are not supposed to use those methods anymore.Comment #23
amateescu commentedOhh, that's right, I forgot we can do that!
Comment #25
amateescu commentedInteresting failures :) Since we are now supposed to instantiate the table mapping via the new
create()method, we also need to update all the test coverage to use it. @plach, do you agree?Comment #26
plachInteresting indeed! I had a closer look and I discovered one thing that I didn't realize earlier: in HEAD the table mapping retrieves the base table names internally in
::getFieldTableName(), which makes sense, since it should ultimately be responsible to provide this knowledge to the outside world :)Hence I think we should always initialize the table names upon construction, instead of doing that in the
createmethod. This should be more compatible with the HEAD behavior and in line with the plan of deprecating the table name entity type keys.In fact, if we do so, we should be able to revert all changes in
DefaultTableMappingTest, except for the mocking on the entity type keys.Comment #27
amateescu commentedI also thought about initializing the table names in the constructor, not sure why I didn't do it at the time but I'm glad you brought it up :)
Comment #29
amateescu commentedThere we go.
Comment #30
berdirI think the review has been addressed and this is RTBC again.
Comment #31
plachLooks great, thanks, I'm planning to commit this tomorrow!
If you really wish to listen to my OCD, we could simplify these to just
->willReturn(isset($table_names['revision']));;)Comment #32
plachSaving credit
Comment #33
amateescu commentedSure, why not :)
Comment #35
plachCommitted 295e297 and pushed to 8.6.x. Thanks!
Comment #36
andypostneeds follow-up and CR
Comment #37
plachRight, sorry.
I forgot about those because this is fairly internal code and I don't expect it to be heavily used directly.
Comment #38
plachOn a second thought I'm not sure what to recommend in the CR: the new
::create()method is @internal and should not be used directly as well. The only legitimate way to instantiate a table mapping is still through the storage at the moment.Comment #39
andypostI mean that @deprecated needs link to CR https://www.drupal.org/core/deprecation#how-method
Comment #40
plachYes, but I'm not sure what an hypothetical CR would day: those setters were kind of internal before...
Comment #41
amateescu commented@andypost, there's nothing really to say in a CR for this specific issue. We will have to write a big one for #2960147: Finalize the entity storage instead :)
Comment #42
plachAdding the deprecated tag to make sure we don't lose track of this issue and reference it in the CR, thanks for the reminder @andypost!
Comment #44
amateescu commentedWe forgot to update the storage to not generate table names by itself: #2981220: SqlContentEntityStorage should use the table names provided by its table mapping