Updated: Comment #29

Problem/Motivation

We have two distinct implementations of entity storage controllers: ConfigStorageController and DatabaseStorageController (with some subclasses).
These are each used for one of ConfigEntityInterface and ContentEntityInterface. You cannot store a content entity in Config, and you cannot (AFAIK) put a config entity into the DB.

This has led to some assumptions in the entity storage code, tying each controller to a specific subset of entity types.

Proposed resolution

Provide a third implementation that works for all entity types.
Do not use this storage out of the box, except in tests.

Remaining tasks

N/A

User interface changes

N/A

API changes

API additions only.

CommentFileSizeAuthor
#59 interdiff.txt17.87 KBtim.plunkett
#59 kv-entitystorage-2208617-59.patch46.35 KBtim.plunkett
#54 interdiff.txt1.54 KBtim.plunkett
#54 kv-entity-2208617-54.patch47.49 KBtim.plunkett
#45 interdiff.txt4.43 KBtim.plunkett
#45 kv-entity-2208617-45.patch47.18 KBtim.plunkett
#42 kv-entity-2208617-42.patch44.45 KBtim.plunkett
#42 interdiff.txt3.44 KBtim.plunkett
#35 interdiff.txt1.78 KBtim.plunkett
#35 kv-entity-storage-2208617-35.patch43.54 KBtim.plunkett
#28 kv-entity-2208617-28.patch43.22 KBtim.plunkett
#27 kv-entity-2208617-27.patch43.23 KBtim.plunkett
#25 kv-entity-2208617-25.patch42.71 KBtim.plunkett
#24 kv-entity-2208617-24.patch40.81 KBtim.plunkett
#23 kv-entity-2208617-23.patch40.75 KBtim.plunkett
#20 kv-entity-2208617-20.patch51.11 KBtim.plunkett
#20 interdiff.txt1.67 KBtim.plunkett
#16 kv-entity-2208617-16-do-not-test.patch37.03 KBtim.plunkett
#16 kv-entity-2208617-16-combined.patch50.92 KBtim.plunkett
#14 interdiff.txt3.28 KBtim.plunkett
#14 kv-entity-2208617-14.patch49.83 KBtim.plunkett
#13 interdiff.txt16.13 KBtim.plunkett
#13 kv-entity-2208617-13.patch47.92 KBtim.plunkett
#11 interdiff.txt9.43 KBtim.plunkett
#11 kv-entity-2208617-11.patch26.64 KBtim.plunkett
#9 interdiff.txt447 bytestim.plunkett
#9 kv-entity-2208617-9.patch22.49 KBtim.plunkett
#6 kv-entity-2208617-5.patch22.53 KBtim.plunkett
#3 kv-entity-2208617-3.patch19.15 KBtim.plunkett
#3 kv-entity-2208617-3-combined.patch32.06 KBtim.plunkett
#2 kv-entity-2208617-2.patch18.54 KBtim.plunkett
#2 kv-entity-2208617-2-combined.patch31.45 KBtim.plunkett

Comments

dawehner’s picture

Category: Task » Feature request

Technically it would be a feature request.

tim.plunkett’s picture

Status: Active » Needs review
StatusFileSize
new31.45 KB
new18.54 KB

One patch with just these changes, one with the other issues from https://drupal.org/project/issues/search?issue_tags=KV%20Entity%20Storage

tim.plunkett’s picture

StatusFileSize
new32.06 KB
new19.15 KB

Duh, the standalone won't fail until I use it somewhere...

Let's try this!

diff --git a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php
index 09e098f..652e6d7 100644
--- a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php
+++ b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php
@@ -29,6 +29,7 @@
  *     },
  *     "list" = "Drupal\filter\FilterFormatListController",
  *     "access" = "Drupal\filter\FilterFormatAccessController",
+ *     "storage" = "Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage"
  *   },
  *   config_prefix = "format",
  *   admin_permission = "administer filters",
tim.plunkett’s picture

StatusFileSize
new22.53 KB

Leaving NW until the last blocker goes in.

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
@@ -0,0 +1,195 @@
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   (optional) A normalizer for an entity.
...
+      $container->get('serializer',  ContainerInterface::NULL_ON_INVALID_REFERENCE)
...
+    // Prefer the entity normalizer.
+    // @todo When serialization.module is moved to \Drupal\Core, remove this
+    //   check and subsequent handling.
+    if ($this->normalizer) {
+      $data = $this->normalizer->normalize($entity);
+    }
+    // We know how to handle ConfigEntity.
+    elseif ($entity instanceof ConfigEntityInterface) {
+      $data = $entity->getExportProperties();
+    }
+    else {
+      throw new EntityStorageException(String::format('Cannot normalize entity of type @entity_type, enable the serialization module.', array('@entity_type' => $this->getEntityTypeId())));
+    }

This would be really nice if normalizer was not in an optional module.

tim.plunkett’s picture

tim.plunkett’s picture

Title: Consider adding key value entity storage » Add key value entity storage
Category: Feature request » Task
Issue summary: View changes
Status: Needs work » Needs review
Issue tags: +Needs tests, +Needs change record
StatusFileSize
new22.49 KB
new447 bytes

Okay, here it is. I'm still changing FilterFormat here until I write tests.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new26.64 KB
new9.43 KB

That was quite the interdiff I provided there...

Ideally this will only Drupal\system\Tests\Entity\ConfigEntityImportTest and I can remove the FilterFormat change

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new47.92 KB
new16.13 KB

Had to put an ugly hack in to deal with ContentEntityBase::__construct() being lazy and assuming that FieldableEntityStorageControllerBase::create() will handle all of its values :(

But we have tests! And I found a cool ConfigEntity bug that I'll probably split out.

tim.plunkett’s picture

StatusFileSize
new49.83 KB
new3.28 KB
berdir’s picture

Yeah, not sure about the create() flow going through the storage controller.

#2096899: Add $EntityType::create() to simplify creating new entities and #1867228: Make EntityTypeManager provide an entity factory and #1867228: Make EntityTypeManager provide an entity factory are related to that, but I'm also a bit worried about too many public static methods on an entity class (with those two issues, you need to be aware that Node::create() is supposed to be called from the outside but not Node::createInstance().. huh?)

tim.plunkett’s picture

Related issues: +#1867228: Make EntityTypeManager provide an entity factory
StatusFileSize
new50.92 KB
new37.03 KB

This has 4 explicit blockers (and contains those patches), see the "Referenced by" section in the sidebar.
The 2 issues in the "Related issues" section are nice-to-haves, and have corresponding @todos in code.
Those @todos are also the only places I have checks for ConfigEntityInterface and ContentEntityInterface.

I rebased on the 4 blockers, so there is no interdiff. But I am adding a patch without those other patches applied, to see the scope of "changes" here (additions only!)

sun’s picture

+1,000 — This represents an actual proof that it is possible to use a key/value store for entities in D8. As such, I consider this a task, not a feature.

It looks like the storage controller has to copy/duplicate a lot of code, which we should be able to provide in an abstract base class or trait...? — Given how much common/standard logic code appears to be copied/duplicated here, that would certainly be a very good idea (separate issue).

Is there a particular reason for why revisions are not supported?

Do we actually need to normalize entities? Is a serialization not sufficient? (Related: #2216527: Inject a serialization format into database key/value storage)

tim.plunkett’s picture

As such, I consider this a task, not a feature.

Glad you agree, I didn't at first but by #8 I'd come around :)

It looks like the storage controller has to copy/duplicate a lot of code, which we should be able to provide in an abstract base class or trait...?

Yeah maybe 90% of KeyValueEntityStorage is boilerplate, we absolutely need to provide *something* to make that less. I had no idea when I started how many assumptions there were about what entity storage is responsible for.

Is there a particular reason for why revisions are not supported?

Because I'm mostly familiar with ConfigEntity, which doesn't support them, and I didn't want to make this harder than it had to be.
I'm open to adding revision support, but I'd like that to be a follow-up.

Do we actually need to normalize entities? Is a serialization not sufficient?

Well we're working in other issues to have an EntityInterface::toArray() method, which is really what we want.
Both entity types right now load back all of the values to pass back through the constructor, which is different than unserializing the entity itself...

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new1.67 KB
new51.11 KB

#2188565: Test coverage for Comment, Custom Block, Node, Taxonomy Term and User entity cache tags added another method with a \Drupal::entityManager() call, mocking that fixes the test.

tim.plunkett’s picture

Status: Needs work » Postponed

Easy enough fix, but I'm going to stop posting patches until these blockers get in.

tim.plunkett’s picture

Status: Postponed » Needs review
Related issues: -#2216569: Move Entity/TypedData normalization inline +#2223361: Rename ComplexDataInterface::getPropertyValues() to toArray(), remove setPropertyValues()
StatusFileSize
new40.75 KB

This should not be committed until #2223361: Rename ComplexDataInterface::getPropertyValues() to toArray(), remove setPropertyValues() goes in, but I need to rework this patch to use that when it happens anyway. Just retesting now that the four other blockers went in!

tim.plunkett’s picture

Status: Needs review » Postponed
Issue tags: -Needs change record
StatusFileSize
new40.81 KB

All of the API changes have been split out to other issues, this is purely an addition.
Postponing until #2223361: Rename ComplexDataInterface::getPropertyValues() to toArray(), remove setPropertyValues() is in, here's the patch.

tim.plunkett’s picture

Status: Postponed » Needs review
StatusFileSize
new42.71 KB

All blockers in!

This is just nice proof that this is even possible.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new43.23 KB

Rerolled after The Great Entity Storage Rename

tim.plunkett’s picture

StatusFileSize
new43.22 KB

Context conflict with changes in core.services.yml, rerolled.

tim.plunkett’s picture

Issue summary: View changes
larowlan’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
    @@ -0,0 +1,270 @@
    +      throw new EntityMalformedException('The entity does not have an ID.');
    

    Can this be expanded, as this behaviour is different to eg DB storage which most are used to. Something like 'The entity does not have an ID, in order to use KeyValueStorage for entities you must provide and ID.'?

  2. +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/Query/Query.php
    @@ -0,0 +1,78 @@
    +        return ($a[$field] <= $b[$field]) ? $direction : -$direction;
    

    this will only support a single sort, perhaps we should throw an Exception if two are passed?

Other than that, looks ok to me.
What's the next step - enabling this in more tests to save on DDL - anywhere we don't need revisions?

berdir’s picture

What's the next step - enabling this in more tests to save on DDL - anywhere
we don't need revisions?

As a result of the ID thing, this only supports config entities, and this won't make them faster :)

As discussed, we could make a Sequence service that by default is a simple wrapper for $database->nextId() to solve that.

I'm not sure exactly where we want to go with this. Core currently already has 4 entity storage implementations (if you cound the old database one and null ;)) but not an alternative config entity storage, as this issue showed on the other hand.

tim.plunkett’s picture

#30.1 See #31, we probably need to address this.

#30.2 Not sure what you mean, that part was copied straight from the config entity query.

As far as using it in more tests, I don't know if it will help.

#31 I guess I'll open an issue for a Sequence service, unless someone beats me to it.

The null doesn't count for sure, and the old DB one will be going away after MenuLinks are converted, AFAIK.
Yes, this mostly benefits config entities, but ideally there would be no real difference!

sun’s picture

Awesome! :-)

This looks pretty much ready to me — aside from the ID thing being discussed above, I guess...?

  1. +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
    @@ -0,0 +1,270 @@
    + * Provides a key value backend for entities.
    + */
    +class KeyValueEntityStorage extends EntityStorageBase {
    

    Can we add a description to the class to clarify that revisions are not supported currently? (possibly as a @todo even?)

    Ideally plus any other high-level information on potential pitfalls and noteworthy stuff that you think would be worth to share for ensuing ages :-)

  2. +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php
    @@ -0,0 +1,35 @@
    + * Contains \Drupal\system\Tests\KeyValueStore\KeyValueConfigEntityStorageTest.
    ...
    +use Drupal\config\Tests\ConfigEntityTest;
    ...
    +class KeyValueConfigEntityStorageTest extends ConfigEntityTest {
    

    Oha. :) I found the location of this class and cross-component class inheritance a little bit confusing here — but huh, yeah, I assume you wanted to bundle all the related tests into a single spot, and I guess the situation is going to be weird regardless where we place 'em... ;)

    So I think this is fine, even tho a little bit hard to spot

  3. +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueContentEntityStorageTest.php
    @@ -0,0 +1,164 @@
    +class KeyValueContentEntityStorageTest extends WebTestBase {
    

    Any particular reason for why this is a WebTest and not DUTB?

    The test doesn't seem to contain any web/requests, just API level assertions? Therefore, it should work as DUTB?

That said, regarding tests, I wonder whether we should actually add one web test that extends an arbitrary existing web test that covers some basic add/edit/delete operations of a common entity that isn't revisionable (e.g., user or comment). — I.e., just extending that existing test, add $modules, and overriding the content entity storage in the test module; the existing test should pass. It would certainly not be a complete coverage, but at least some basic proof that it actually works with non-test-entities, too? (also happy to defer that to a follow-up issue tho)

sun’s picture

Discussed some more with @tim.plunkett today:

  1. This patch actually presents a working proof of concept for a key value entity storage implementation as-is. — It is awesome that we actually managed to make it possible.

  2. Due to current feature constraints of our KeyValueStore backends, it is only able to work with string identifiers (machine names) right now. — However, that is sufficient for config entities.

  3. For content entities - or to be more precise, entities using serial IDs - the storage backend must be capable of understanding a concept of nextId():

    1. The consuming code makes an assumption that the keys of stored items are numeric (despite being strings in a hashtable).
    2. It is fine for consuming code to make such an assumption on a particular k/v collection.
    3. A key/value store provides no guarantee on sequential order of keys (or any other order for that matter).
    4. Therefore, the task is: (1) List all keys, (2) sort numerically, (3) nextId() = max() + 1.

    Due to that, #2161643: Add a KeyValueStore\IterableKeysInterface (allowing to retrieve keys with a given prefix) is very closely related:

    The new interface introduced there only focuses on retrieving a filtered sub-set of key/value pairs for now, but technically the requirement here is asking the k/v backend to be capable of iterating over keys, too; just with a slightly different purpose:

    Ignoring values, just focusing on keys, enable nextId() within a collection.

    As such, that could be added to the new IterableKeysInterface in a separate follow-up issue, once it has landed.

  4. I think it's awesome that we are actually this close already, and I'm confident that we're able to figure out that last remaining issue regarding nextId().

    I think we can move forward in independent steps - since the current incarnation works at least for config entities already.

    → Enabling it to additionally work for content entities (or rather, entities with serial IDs) would make most sense as a separate follow-up issue.

tim.plunkett’s picture

StatusFileSize
new43.54 KB
new1.78 KB

+1 on the path forward described in #34.

Addressed #33.1 and 33.3. Not concerned much about 33.2, nothing really to do about that.

sun’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! :-)

sun’s picture

tim.plunkett’s picture

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new3.44 KB
new44.45 KB
berdir’s picture

Speaking of that and the related issues, this should come with a validation of the id length similar to ConfigEntityStorage, as this is limited to 128 characters right now in HEAD.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new47.18 KB
new4.43 KB

Yep.
Also updated the existing test to allow my subclass to pass.

sun’s picture

45: kv-entity-2208617-45.patch queued for re-testing.

tim.plunkett’s picture

jibran’s picture

+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php
@@ -0,0 +1,41 @@
+class KeyValueConfigEntityStorageTest extends ConfigEntityTest {

Why are there no test methods in this class?

tim.plunkett’s picture

Because they're all in the parent class. It just re-runs that tests but with the different storage backend.
See keyvalue_test_entity_type_alter().

tim.plunkett’s picture

45: kv-entity-2208617-45.patch queued for re-testing.

The last submitted patch, 45: kv-entity-2208617-45.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new47.49 KB
new1.54 KB
sun’s picture

Status: Needs review » Reviewed & tested by the community

Yay, finally green again! :-)

alexpott’s picture

54: kv-entity-2208617-54.patch queued for re-testing.

tim.plunkett’s picture

Just thought I should call this out:

+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigEntityTest.php
@@ -21,6 +21,11 @@
+  const MAX_ID_LENGTH = ConfigEntityStorage::MAX_ID_LENGTH;

@@ -164,7 +169,7 @@ function testCRUD() {
     $id_length_config_test = entity_create('config_test', array(
-      'id' => $this->randomName(ConfigEntityStorage::MAX_ID_LENGTH),
+      'id' => $this->randomName(static::MAX_ID_LENGTH),

+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php
@@ -0,0 +1,41 @@
+  const MAX_ID_LENGTH = KeyValueEntityStorage::MAX_ID_LENGTH;

This is the only change, the rest are all additions. It's not a functional change, just allowing the subclassed test to specify its own max length (128 vs 166).

We have 1 unit test, and two webtests, one for entity_test and one for config_test (content vs config entity)

tim.plunkett’s picture

Status: Reviewed & tested by the community » Postponed

This was RTBC first, but #2225955: Improve the DX of writing entity storage classes overhauls how entity storage classes work, and will need a bit of adjustment once that goes in. So I'm preemptively postponing this on that one.

tim.plunkett’s picture

Status: Postponed » Needs review
StatusFileSize
new46.35 KB
new17.87 KB

Rerolled since that went in. Here are the relevant changes (removed 80 lines from the storage class!).

neclimdul’s picture

Status: Needs review » Reviewed & tested by the community

lets do it.

xjm’s picture

I think this might merit a change record for the feature addition?

+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
@@ -0,0 +1,209 @@
+   * Length limit of the entity ID.
+   */
+  const MAX_ID_LENGTH = 128;

I started to ask why we were overriding this, but then realized we aren't -- ConfigEntityStorage specifies its own and content entities in DB storage have serial IDs. :) Right.

sun’s picture

I don't think this is in a state to be announced as a feature of its own — as long as content entities aren't supported yet (cf. #34), this implementation only covers <50% (= entities using a machine name string/key as IDs). IMO, we should only announce this when content entities are supported (requires separate follow-up issues).

xjm’s picture

Issue tags: -Needs change record

Thanks @sun. @Berdir said something similar in IRC. Makes sense.

berdir’s picture

Based on my discussion with @timplunkett earlier this week, I would go even further and say that this is 90% a test-only implementation (and will stay that way), a fancier version of the array based test field storage that we had in 7.x, if you will. (as the issue summary says)

You can't really use it for config entities, as they have to be in config, and it can not scale for content entities that need any kind of querying or even listing...

  • Commit 95c69f5 on 8.x by catch:
    Issue #2208617 by tim.plunkett: Add key value entity storage.
    
catch’s picture

Status: Reviewed & tested by the community » Fixed

Read through this a few times and couldn't find much to complain about, looks like it'll come in useful and help us avoid introducing assumptions into the other entity storage.

Committed/pushed to 8.x, thanks!

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.