Commit 5dad0b86 authored by catch's avatar catch

Issue #597236 by Berdir, catch, msonnabaum, Xano, Wim Leers, jhedstrom,...

Issue #597236 by Berdir, catch, msonnabaum, Xano, Wim Leers, jhedstrom, amateescu, corvus_ch, swentel, moshe weitzman, Gábor Hojtsy, riccardoR, killes@www.drop.org, et al: Add entity caching to core.
parent 937be05a
......@@ -158,7 +158,14 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans
foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
if (isset($this->values[$field_name])) {
if (is_array($this->values[$field_name]) && isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
}
}
else {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
}
}
}
}
......@@ -563,6 +570,11 @@ public function language() {
$language = $this->languages[$this->activeLangcode];
}
else {
// @todo Avoid this check by getting the language from the language
// manager directly in https://www.drupal.org/node/2303877.
if (!isset($this->languages[$this->defaultLangcode])) {
$this->languages += $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL);
}
$language = $this->languages[$this->defaultLangcode];
}
return $language;
......
......@@ -75,196 +75,6 @@ protected function doCreate(array $values) {
return $entity;
}
/**
* Loads values of configurable fields for a group of entities.
*
* Loads all fields for each entity object in a group of a single entity type.
* The loaded field values are added directly to the entity objects.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doLoadFieldItems() method with the actual storage
* logic.
*
* @param array $entities
* An array of entities keyed by entity ID.
*/
protected function loadFieldItems(array $entities) {
if (empty($entities)) {
return;
}
$age = static::FIELD_LOAD_CURRENT;
foreach ($entities as $entity) {
if (!$entity->isDefaultRevision()) {
$age = static::FIELD_LOAD_REVISION;
break;
}
}
// Only the most current revision of non-deleted fields for cacheable entity
// types can be cached.
$load_current = $age == static::FIELD_LOAD_CURRENT;
$use_cache = $load_current && $this->entityType->isFieldDataCacheable();
// Assume all entities will need to be queried. Entities found in the cache
// will be removed from the list.
$queried_entities = $entities;
// Fetch available entities from cache, if applicable.
if ($use_cache) {
// Build the list of cache entries to retrieve.
$cids = array();
foreach ($entities as $id => $entity) {
$cids[] = "field:{$this->entityTypeId}:$id";
}
$cache = \Drupal::cache('entity')->getMultiple($cids);
// Put the cached field values back into the entities and remove them from
// the list of entities to query.
foreach ($entities as $id => $entity) {
$cid = "field:{$this->entityTypeId}:$id";
if (isset($cache[$cid])) {
unset($queried_entities[$id]);
foreach ($cache[$cid]->data as $langcode => $values) {
$translation = $entity->getTranslation($langcode);
// We do not need to worry about field translatability here, the
// translation object will manage that automatically.
foreach ($values as $field_name => $items) {
$translation->$field_name = $items;
}
}
}
}
}
// Fetch other entities from their storage location.
if ($queried_entities) {
// Let the storage actually load the values.
$this->doLoadFieldItems($queried_entities, $age);
// Build cache data.
// @todo: Improve this logic to avoid instantiating field objects once
// the field logic is improved to not do that anyway.
if ($use_cache) {
foreach ($queried_entities as $id => $entity) {
$data = array();
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
foreach ($translation as $field_name => $items) {
if ($items->getFieldDefinition() instanceof FieldInstanceConfigInterface && !$items->isEmpty()) {
$data[$langcode][$field_name] = $items->getValue();
}
}
}
$cid = "field:{$this->entityTypeId}:$id";
\Drupal::cache('entity')->set($cid, $data, Cache::PERMANENT, array('entity_field_info' => TRUE));
}
}
}
}
/**
* Saves values of configurable fields for an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doSaveFieldItems() method with the actual storage
* logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param bool $update
* TRUE if the entity is being updated, FALSE if it is being inserted.
*/
protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
$this->doSaveFieldItems($entity, $update);
if ($update) {
$entity_type = $entity->getEntityType();
if ($entity_type->isFieldDataCacheable()) {
\Drupal::cache('entity')->delete('field:' . $entity->getEntityTypeId() . ':' . $entity->id());
}
}
}
/**
* Deletes values of configurable fields for all revisions of an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doDeleteFieldItems() method with the actual storage
* logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
protected function deleteFieldItems(EntityInterface $entity) {
$this->doDeleteFieldItems($entity);
$entity_type = $entity->getEntityType();
if ($entity_type->isFieldDataCacheable()) {
\Drupal::cache('entity')->delete('field:' . $entity->getEntityTypeId() . ':' . $entity->id());
}
}
/**
* Deletes values of configurable fields for a single revision of an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doDeleteFieldItemsRevision() method with the actual
* storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity. It must have a revision ID attribute.
*/
protected function deleteFieldItemsRevision(EntityInterface $entity) {
$this->doDeleteFieldItemsRevision($entity);
}
/**
* Loads values of configurable fields for a group of entities.
*
* This is the method that holds the actual storage logic.
*
* @param array $entities
* An array of entities keyed by entity ID.
* @param int $age
* EntityStorageInterface::FIELD_LOAD_CURRENT to load the most
* recent revision for all fields, or
* EntityStorageInterface::FIELD_LOAD_REVISION to load the version
* indicated by each entity.
*/
abstract protected function doLoadFieldItems($entities, $age);
/**
* Saves values of configurable fields for an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param bool $update
* TRUE if the entity is being updated, FALSE if it is being inserted.
*/
abstract protected function doSaveFieldItems(EntityInterface $entity, $update);
/**
* Deletes values of configurable fields for all revisions of an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
abstract protected function doDeleteFieldItems(EntityInterface $entity);
/**
* Deletes values of configurable fields for a single revision of an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
abstract protected function doDeleteFieldItemsRevision(EntityInterface $entity);
/**
* {@inheritdoc}
*/
......
......@@ -40,7 +40,7 @@ class EntityType implements EntityTypeInterface {
*
* @var bool
*/
protected $field_cache;
protected $persistent_cache;
/**
* An array of entity keys.
......@@ -260,8 +260,8 @@ public function isRenderCacheable() {
/**
* {@inheritdoc}
*/
public function isFieldDataCacheable() {
return isset($this->field_cache) ? $this->field_cache: TRUE;
public function isPersistentlyCacheable() {
return isset($this->persistent_cache) ? $this->persistent_cache: TRUE;
}
/**
......
......@@ -153,7 +153,7 @@ public function isRenderCacheable();
*
* @return bool
*/
public function isFieldDataCacheable();
public function isPersistentlyCacheable();
/**
* Sets the name of the entity type class.
......
......@@ -656,11 +656,11 @@ function comment_form_field_ui_field_edit_form_alter(&$form, $form_state) {
}
/**
* Implements hook_entity_load().
* Implements hook_entity_storage_load().
*
* @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions()
*/
function comment_entity_load($entities, $entity_type) {
function comment_entity_storage_load($entities, $entity_type) {
// Comments can only be attached to content entities, so skip others.
if (!\Drupal::entityManager()->getDefinition($entity_type)->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) {
return;
......
......@@ -251,6 +251,10 @@ public function update(CommentInterface $comment) {
->condition('field_name', $comment->getFieldName())
->execute();
}
// Reset the cache of the commented entity so that when the entity is loaded
// the next time, the statistics will be loaded again.
$this->entityManager->getStorage($comment->getCommentedEntityTypeId())->resetCache(array($comment->getCommentedEntityId()));
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\comment;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
......@@ -46,13 +47,15 @@ class CommentStorage extends ContentEntityDatabaseStorage implements CommentStor
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\comment\CommentStatisticsInterface $comment_statistics
* The comment statistics service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, CommentStatisticsInterface $comment_statistics, AccountInterface $current_user) {
parent::__construct($entity_info, $database, $entity_manager);
public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, CommentStatisticsInterface $comment_statistics, AccountInterface $current_user, CacheBackendInterface $cache) {
parent::__construct($entity_info, $database, $entity_manager, $cache);
$this->statistics = $comment_statistics;
$this->currentUser = $current_user;
}
......@@ -66,7 +69,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
$container->get('database'),
$container->get('entity.manager'),
$container->get('comment.statistics'),
$container->get('current_user')
$container->get('current_user'),
$container->get('cache.entity')
);
}
......
......@@ -506,9 +506,9 @@ function content_translation_language_fallback_candidates_entity_view_alter(&$ca
}
/**
* Implements hook_entity_load().
* Implements hook_entity_storage_load().
*/
function content_translation_entity_load(array $entities, $entity_type) {
function content_translation_entity_storage_load(array $entities, $entity_type) {
$enabled_entities = array();
if (content_translation_enabled($entity_type)) {
......
......@@ -170,17 +170,19 @@ function testEntityDisplayViewMultiple() {
}
/**
* Test field cache.
* Test entity cache.
*
* Complements unit test coverage in
* \Drupal\Tests\Core\Entity\ContentEntityDatabaseStorageTest.
*/
function testFieldAttachCache() {
function testEntityCache() {
// Initialize random values and a test entity.
$entity_init = entity_create('entity_test', array('type' => $this->instance->bundle));
$langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$values = $this->_generateTestFieldValues($this->field->getCardinality());
// Non-cacheable entity type.
$entity_type = 'entity_test';
$cid = "field:$entity_type:" . $entity_init->id();
$cid = "values:$entity_type:" . $entity_init->id();
// Check that no initial cache entry is present.
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no initial cache entry');
......@@ -189,7 +191,7 @@ function testFieldAttachCache() {
$entity = clone($entity_init);
$entity->{$this->field_name}->setValue($values);
$entity = $this->entitySaveReload($entity);
$cid = "field:$entity_type:" . $entity->id();
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no cache entry on insert and load');
// Cacheable entity type.
......@@ -201,22 +203,22 @@ function testFieldAttachCache() {
));
// Check that no initial cache entry is present.
$cid = "field:$entity_type:" . $entity->id();
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no initial cache entry');
// Save, and check that no cache entry is present.
$entity = clone($entity_init);
$entity->{$this->field_name_2} = $values;
$entity->save();
$cid = "field:$entity_type:" . $entity->id();
$cid = "values:$entity_type:" . $entity->id();
$this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on insert');
// Load, and check that a cache entry is present with the expected values.
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$controller->load($entity->id());
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load');
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Update with different values, and check that the cache entry is wiped.
$values = $this->_generateTestFieldValues($this->field_2->getCardinality());
......@@ -226,9 +228,9 @@ function testFieldAttachCache() {
// Load, and check that a cache entry is present with the expected values.
$controller->resetCache();
$controller->load($entity->id());
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load');
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Create a new revision, and check that the cache entry is wiped.
$values = $this->_generateTestFieldValues($this->field_2->getCardinality());
......@@ -239,9 +241,9 @@ function testFieldAttachCache() {
// Load, and check that a cache entry is present with the expected values.
$controller->resetCache();
$controller->load($entity->id());
$cached_entity = $controller->load($entity->id());
$cache = \Drupal::cache('entity')->get($cid);
$this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load');
$this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load');
// Delete, and check that the cache entry is wiped.
$entity->delete();
......
......@@ -1893,15 +1893,14 @@ function file_get_file_references(FileInterface $file, FieldDefinitionInterface
$file_fields[$entity_type_id][$bundle] = array();
// This contains the possible field names.
foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
$field_type = $field_definition->getType();
// If this is the first time this field type is seen, check
// whether it references files.
if (!isset($field_columns[$field_type])) {
$field_columns[$field_type] = file_field_find_file_reference_column($field_definition);
if (!isset($field_columns[$field_definition->getType()])) {
$field_columns[$field_definition->getType()] = file_field_find_file_reference_column($field_definition);
}
// If the field type does reference files then record it.
if ($field_columns[$field_type]) {
$file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_type];
if ($field_columns[$field_definition->getType()]) {
$file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_definition->getType()];
}
}
}
......
......@@ -23,6 +23,7 @@ class FilePrivateTest extends FileFieldTestBase {
public function setUp() {
parent::setUp();
node_access_test_add_field(entity_load('node_type', 'article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
......
......@@ -328,9 +328,9 @@ function forum_node_predelete(EntityInterface $node) {
}
/**
* Implements hook_ENTITY_TYPE_load() for node entities.
* Implements hook_ENTITY_TYPE_storage_load() for node entities.
*/
function forum_node_load($nodes) {
function forum_node_storage_load($nodes) {
$node_vids = array();
foreach ($nodes as $node) {
if (\Drupal::service('forum_manager')->checkNodeType($node)) {
......
......@@ -26,6 +26,7 @@ class ForumNodeAccessTest extends WebTestBase {
function setUp() {
parent::setUp();
node_access_rebuild();
node_access_test_add_field(entity_load('node_type', 'forum'));
\Drupal::state()->set('node_access_test.private', TRUE);
}
......@@ -48,7 +49,7 @@ function testForumNodeAccess() {
$edit = array(
'title[0][value]' => $private_node_title,
'body[0][value]' => $this->randomName(200),
'private' => TRUE,
'private[0][value]' => TRUE,
);
$this->drupalPostForm('node/add/forum', $edit, t('Save'), array('query' => array('forum_id' => 1)));
$private_node = $this->drupalGetNodeByTitle($private_node_title);
......
......@@ -33,6 +33,8 @@ class NodeAccessBaseTableTest extends NodeTestBase {
public function setUp() {
parent::setUp();
node_access_test_add_field(entity_load('node_type', 'article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
......@@ -69,7 +71,7 @@ function testNodeAccessBasic() {
'title[0][value]' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $this->webUser->getUsername())),
);
if ($is_private) {
$edit['private'] = TRUE;
$edit['private[0][value]'] = TRUE;
$edit['body[0][value]'] = 'private node';
$edit['field_tags'] = 'private';
}
......@@ -79,14 +81,13 @@ function testNodeAccessBasic() {
}
$this->drupalPostForm('node/add/article', $edit, t('Save'));
$nid = db_query('SELECT nid FROM {node_field_data} WHERE title = :title', array(':title' => $edit['title[0][value]']))->fetchField();
$private_status = db_query('SELECT private FROM {node_access_test} where nid = :nid', array(':nid' => $nid))->fetchField();
$this->assertTrue($is_private == $private_status, 'The private status of the node was properly set in the node_access_test table.');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEqual($is_private, (int)$node->private->value, 'The private status of the node was properly set in the node_access_test table.');
if ($is_private) {
$private_nodes[] = $nid;
$private_nodes[] = $node->id();
}
$titles[$nid] = $edit['title[0][value]'];
$this->nodesByUser[$this->webUser->id()][$nid] = $is_private;
$titles[$node->id()] = $edit['title[0][value]'];
$this->nodesByUser[$this->webUser->id()][$node->id()] = $is_private;
}
}
$this->publicTid = db_query('SELECT tid FROM {taxonomy_term_data} WHERE name = :name', array(':name' => 'public'))->fetchField();
......
......@@ -48,6 +48,8 @@ class NodeAccessLanguageAwareCombinationTest extends NodeTestBase {
public function setUp() {
parent::setUp();
node_access_test_add_field(entity_load('node_type', 'page'));
// Create the 'private' field, which allows the node to be marked as private
// (restricted access) in a given translation.
$field_private = entity_create('field_config', array(
......
......@@ -28,6 +28,8 @@ class NodeAccessLanguageTest extends NodeTestBase {
function setUp() {
parent::setUp();
node_access_test_add_field(entity_load('node_type', 'page'));
// After enabling a node access module, the access table has to be rebuild.
node_access_rebuild();
......
<?php
/**
* @file
* Install, update and uninstall functions for the node_access_test module.
*/
/**
* Implements hook_schema().
*/
function node_access_test_schema() {
$schema['node_access_test'] = array(
'description' => 'The base table for node_access_test.',
'fields' => array(
'nid' => array(
'description' => 'The {node}.nid this record affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'private' => array(
'description' => 'Boolean indicating whether the node is private (visible to administrator) or not (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'indexes' => array(
'nid' => array('nid'),
),
'primary key' => array('nid'),
'foreign keys' => array(
'versioned_node' => array(
'table' => 'node',
'columns' => array('nid' => 'nid'),
),
),
);
return $schema;
}
\ No newline at end of file
......@@ -19,8 +19,9 @@
* @see \Drupal\node\Tests\NodeAccessBaseTableTest
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldInstanceConfig;
use Drupal\node\NodeTypeInterface;
use Drupal\node\NodeInterface;
/**
......@@ -127,74 +128,32 @@ function node_access_test_permission() {
}
/**
* Implements hook_entity_base_field_info().
*/
function node_access_test_entity_base_field_info(EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'node') {
$fields['private'] = FieldDefinition::create('boolean')
->setLabel(t('Private'))
->setComputed(TRUE)
->setCustomStorage(TRUE);
return $fields;
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function node_access_test_form_node_form_alter(&$form, $form_state) {
// Only show this checkbox for NodeAccessBaseTableTestCase.
if (\Drupal::state()->get('node_access_test.private')) {
$node = $form_state['controller']->getEntity($form_state);
$form['private'] = array