Commit 5dad0b86 authored by catch's avatar catch
Browse files

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;
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\String;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
......@@ -111,6 +112,13 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements S
*/
protected $schemaHandler;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* {@inheritdoc}
*/
......@@ -118,7 +126,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
return new static(
$entity_type,
$container->get('database'),
$container->get('entity.manager')
$container->get('entity.manager'),
$container->get('cache.entity')
);
}
......@@ -142,12 +151,15 @@ public function getFieldStorageDefinitions() {
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend to be used.
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager) {
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
parent::__construct($entity_type);
$this->database = $database;
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
// @todo Remove table names from the entity type definition in
// https://drupal.org/node/2232465
......@@ -342,13 +354,163 @@ public function getTableMapping() {
* {@inheritdoc}
*/
protected function doLoadMultiple(array $ids = NULL) {
// Build and execute the query.
$records = $this
->buildQuery($ids)
->execute()
->fetchAllAssoc($this->idKey);
// Attempt to load entities from the persistent cache. This will remove IDs
// that were loaded from $ids.
$entities_from_cache = $this->getFromPersistentCache($ids);
// Load any remaining entities from the database.
$entities_from_storage = $this->getFromStorage($ids);
$this->setPersistentCache($entities_from_storage);
return $entities_from_cache + $entities_from_storage;
}
/**
* Gets entities from the storage.
*
* @param array|null $ids
* If not empty, return entities that match these IDs. Return all entities
* when NULL.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of entities from the storage.
*/
protected function getFromStorage(array $ids = NULL) {
$entities = array();
if ($ids === NULL || $ids) {
// Build and execute the query.
$query_result = $this->buildQuery($ids)->execute();
$records = $query_result->fetchAllAssoc($this->idKey);
// Map the loaded records into entity objects and according fields.
if ($records) {
$entities = $this->mapFromStorageRecords($records);
// Call hook_entity_storage_load().
foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
$function = $module . '_entity_storage_load';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_storage_load().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
$function = $module . '_' . $this->entityTypeId . '_storage_load';
$function($entities);
}
}
}
return $entities;
}
return $this->mapFromStorageRecords($records);
/**
* Gets entities from the persistent cache backend.
*
* @param array|null &$ids
* If not empty, return entities that match these IDs. IDs that were found
* will be removed from the list.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of entities from the persistent cache.
*/
protected function getFromPersistentCache(array &$ids = NULL) {
if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
return array();
}
$entities = array();
// Build the list of cache entries to retrieve.
$cid_map = array();
foreach ($ids as $id) {
$cid_map[$id] = $this->buildCacheId($id);
}
$cids = array_values($cid_map);
if ($cache = $this->cacheBackend->getMultiple($cids)) {
// Get the entities that were found in the cache.
foreach ($ids as $index => $id) {
$cid = $cid_map[$id];
if (isset($cache[$cid])) {
$entities[$id] = $cache[$cid]->data;
unset($ids[$index]);
}
}
}
return $entities;
}
/**
* Stores entities in the persistent cache backend.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* Entities to store in the cache.
*/
protected function setPersistentCache($entities) {
if (!$this->entityType->isPersistentlyCacheable()) {
return;
}
$cache_tags = array(
$this->entityTypeId . '_values' => TRUE,
'entity_field_info' => TRUE,
);
foreach ($entities as $id => $entity) {
$this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
}
}
/**
* Invokes hook_entity_load_uncached().
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* List of entities, keyed on the entity ID.
*/
protected function invokeLoadUncachedHook(array &$entities) {
if (!empty($entities)) {
// Call hook_entity_load_uncached().
foreach ($this->moduleHandler()->getImplementations('entity_load_uncached') as $module) {
$function = $module . '_entity_load_uncached';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_load_uncached().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load_uncached') as $module) {
$function = $module . '_' . $this->entityTypeId . '_load_uncached';
$function($entities);
}
}
}
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
if ($ids) {
$cids = array();
foreach ($ids as $id) {
unset($this->entities[$id]);
$cids[] = $this->buildCacheId($id);
}
if ($this->entityType->isPersistentlyCacheable()) {
$this->cacheBackend->deleteMultiple($cids);
}
}
else {
$this->entities = array();
if ($this->entityType->isPersistentlyCacheable()) {
$this->cacheBackend->deleteTags(array($this->entityTypeId . '_values' => TRUE));
}
}
}
/**
* Returns the cache ID for the passed in entity ID.
*
* @param int $id
* Entity ID for which the cache ID should be built.
*
* @return string
* Cache ID that can be passed to the cache backend.
*/
protected function buildCacheId($id) {
return "values:{$this->entityTypeId}:$id";
}
/**
......@@ -931,9 +1093,26 @@ public function getQueryServiceName() {
}
/**
* {@inheritdoc}
* 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.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* An array of entities keyed by entity ID.
*/
protected function doLoadFieldItems($entities, $age) {
protected function loadFieldItems(array $entities) {
if (empty($entities) || !$this->entityType->isFieldable()) {
return;
}
$age = static::FIELD_LOAD_CURRENT;
foreach ($entities as $entity) {
if (!$entity->isDefaultRevision()) {
$age = static::FIELD_LOAD_REVISION;
break;
}
}
$load_current = $age == static::FIELD_LOAD_CURRENT;
// Collect entities ids, bundles and languages.
......@@ -1006,9 +1185,14 @@ protected function doLoadFieldItems($entities, $age) {
}
/**
* {@inheritdoc}
* Saves values of configurable fields for an entity.
*
* @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 doSaveFieldItems(EntityInterface $entity, $update) {
protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
$vid = $entity->getRevisionId();
$id = $entity->id();
$bundle = $entity->bundle();
......@@ -1094,9 +1278,12 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
}
/**
* {@inheritdoc}
* Deletes values of configurable fields for all revisions of an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
protected function doDeleteFieldItems(EntityInterface $entity) {
protected function deleteFieldItems(EntityInterface $entity) {
foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if (!$this->usesDedicatedTable($storage_definition)) {
......@@ -1114,9 +1301,12 @@ protected function doDeleteFieldItems(EntityInterface $entity) {
}
/**
* {@inheritdoc}
* Deletes values of configurable fields for a single revision of an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity. It must have a revision ID.
*/
protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
protected function deleteFieldItemsRevision(EntityInterface $entity) {
$vid = $entity->getRevisionId();
if (isset($vid)) {
foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
......
......@@ -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;