diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 2ece2b5..f3a8f78 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -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.
+      if (!isset($this->languages[$this->defaultLangcode])) {
+        $this->languages += $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL);
+      }
       $language = $this->languages[$this->defaultLangcode];
     }
     return $language;
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index 6f246f7..ca4d862 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -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;
@@ -112,13 +113,21 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements S
   protected $schemaHandler;
 
   /**
+   * Cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cacheBackend;
+
+  /**
    * {@inheritdoc}
    */
   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
     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,158 @@ 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);
 
-    return $this->mapFromStorageRecords($records);
+    // 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;
+  }
+
+  /**
+   * 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.
+    $cids = array();
+    foreach ($ids as $id) {
+      $cids[] = $this->buildCacheId($id);
+    }
+    if ($cache = $this->cacheBackend->getMultiple($cids)) {
+      // Get the entities that were found in the cache.
+      foreach ($ids as $index => $id) {
+        $cid = $this->buildCacheId($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;
+    }
+
+    foreach ($entities as $id => $entity) {
+      $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, array($this->entityTypeId . '_values' => TRUE, 'entity_field_info' => TRUE));
+    }
+  }
+
+  /**
+   * 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 +1088,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 +1180,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 +1273,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 +1296,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) {
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
index f07936c..8906239 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Entity;
 
+use Drupal\Core\Field\PrepareCacheInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index dad9674..a4849be 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -76,196 +76,6 @@ protected function doCreate(array $values) {
   }
 
   /**
-   * 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}
    */
   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { }
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 728b3ec..5ebb0b6 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -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;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
index a6e39aa..d92e00b 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
@@ -153,7 +153,7 @@ public function isRenderCacheable();
    *
    * @return bool
    */
-  public function isFieldDataCacheable();
+  public function isPersistentlyCacheable();
 
   /**
    * Sets the name of the entity type class.
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 03c32db..bb95be4 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -682,11 +682,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;
diff --git a/core/modules/comment/src/CommentStatistics.php b/core/modules/comment/src/CommentStatistics.php
index c2ac73d..164e8aa 100644
--- a/core/modules/comment/src/CommentStatistics.php
+++ b/core/modules/comment/src/CommentStatistics.php
@@ -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()));
   }
 
 }
diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php
index 945ff9d..72ca80d 100644
--- a/core/modules/comment/src/CommentStorage.php
+++ b/core/modules/comment/src/CommentStorage.php
@@ -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')
     );
   }
 
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 858ffba..f717941 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -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)) {
diff --git a/core/modules/field/src/Tests/FieldAttachOtherTest.php b/core/modules/field/src/Tests/FieldAttachOtherTest.php
index a058dd6..b396eda 100644
--- a/core/modules/field/src/Tests/FieldAttachOtherTest.php
+++ b/core/modules/field/src/Tests/FieldAttachOtherTest.php
@@ -170,17 +170,18 @@ function testEntityDisplayViewMultiple() {
   }
 
   /**
-   * Test field cache.
+   * Test entity cache.
+   *
+   * @todo Remove this when there is equal unit test coverage.
    */
-  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 +190,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 +202,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 +227,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 +240,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();
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 97fa6ff..750dd35 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1892,15 +1892,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()];
             }
           }
         }
diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/src/Tests/FilePrivateTest.php
index dcdf910..0159a59 100644
--- a/core/modules/file/src/Tests/FilePrivateTest.php
+++ b/core/modules/file/src/Tests/FilePrivateTest.php
@@ -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);
   }
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 23293fe..0c80786 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -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)) {
diff --git a/core/modules/forum/src/Tests/ForumNodeAccessTest.php b/core/modules/forum/src/Tests/ForumNodeAccessTest.php
index 7e23ae0..ad51808 100644
--- a/core/modules/forum/src/Tests/ForumNodeAccessTest.php
+++ b/core/modules/forum/src/Tests/ForumNodeAccessTest.php
@@ -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);
diff --git a/core/modules/node/src/Tests/NodeAccessBaseTableTest.php b/core/modules/node/src/Tests/NodeAccessBaseTableTest.php
index ec05c8c..0a008e1 100644
--- a/core/modules/node/src/Tests/NodeAccessBaseTableTest.php
+++ b/core/modules/node/src/Tests/NodeAccessBaseTableTest.php
@@ -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();
diff --git a/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php b/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php
index 1eeb584..ace0d5d 100644
--- a/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php
+++ b/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php
@@ -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(
diff --git a/core/modules/node/src/Tests/NodeAccessLanguageTest.php b/core/modules/node/src/Tests/NodeAccessLanguageTest.php
index 526439f..9683cca 100644
--- a/core/modules/node/src/Tests/NodeAccessLanguageTest.php
+++ b/core/modules/node/src/Tests/NodeAccessLanguageTest.php
@@ -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();
 
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.install b/core/modules/node/tests/modules/node_access_test/node_access_test.install
deleted file mode 100644
index 6b3ef5d..0000000
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.install
+++ /dev/null
@@ -1,42 +0,0 @@
-<?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
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index 64c7476..3d2c1ab 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -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().
+ * Adds the private field to a node type.
+ *
+ * @param \Drupal\node\NodeTypeInterface $type
+ *   A node type entity.
  */
-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);
+function node_access_test_add_field(NodeTypeInterface $type) {
+  $field = FieldConfig::create(array(
+    'name' => 'private',
+    'entity_type' => 'node',
+    'type' => 'integer',
+  ));
+  $field->save();
+  $instance = FieldInstanceConfig::create(array(
+    'field_name' => 'private',
+    'entity_type' => 'node',
+    'bundle' => $type->id(),
+    'label' => 'Private',
+  ));
+  $instance->save();
 
-    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(
-      '#type' => 'checkbox',
-      '#title' => t('Private'),
-      '#description' => t('Check here if this content should be set private and only shown to privileged users.'),
-      '#default_value' => $node->private->value,
-    );
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for node entities.
- */
-function node_access_test_node_load($nodes) {
-  $result = db_query('SELECT nid, private FROM {node_access_test} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
-  foreach ($result as $record) {
-    $nodes[$record->nid]->private = $record->private;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for node entities.
- */
-function node_access_test_node_predelete(NodeInterface $node) {
-  db_delete('node_access_test')->condition('nid', $node->id())->execute();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- */
-function node_access_test_node_insert(NodeInterface $node) {
-  _node_access_test_node_write($node);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
- */
-function node_access_test_node_update(NodeInterface $node) {
-  _node_access_test_node_write($node);
-}
-
-/**
- * Saves the private status of the node in the database.
- */
-function _node_access_test_node_write(NodeInterface $node) {
-  db_merge('node_access_test')
-    ->key('nid', $node->id())
-    ->fields(array('private' => (int) $node->private->value))
-    ->execute();
+  // Assign widget settings for the 'default' form mode.
+  entity_get_form_display('node', $type->id(), 'default')
+    ->setComponent('private', array(
+      'type' => 'number',
+    ))
+    ->save();
 }
 
 /**
diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module
index 1221147..8fa81ef 100644
--- a/core/modules/rdf/rdf.module
+++ b/core/modules/rdf/rdf.module
@@ -224,9 +224,9 @@ function rdf_entity_prepare_view($entity_type, array $entities, array $displays)
 }
 
 /**
- * Implements hook_ENTITY_TYPE_load() for comment entities.
+ * Implements hook_ENTITY_TYPE_storage_load() for comment entities.
  */
-function rdf_comment_load($comments) {
+function rdf_comment_storage_load($comments) {
   foreach ($comments as $comment) {
     // Pages with many comments can show poor performance. This information
     // isn't needed until rdf_preprocess_comment() is called, but set it here
diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php
index 5388dab..458f080 100644
--- a/core/modules/system/entity.api.php
+++ b/core/modules/system/entity.api.php
@@ -537,7 +537,10 @@ function hook_ENTITY_TYPE_create(\Drupal\Core\Entity\EntityInterface $entity) {
  * This is a generic load hook called for all entity types loaded via the
  * entity API.
  *
- * @param array $entities
+ * hook_entity_storage_load() should be used to load additional data for
+ * content entities.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface[] $entities
  *   The entities keyed by entity ID.
  * @param string $entity_type_id
  *   The type of entities being loaded (i.e. node, user, comment).
@@ -545,7 +548,7 @@ function hook_ENTITY_TYPE_create(\Drupal\Core\Entity\EntityInterface $entity) {
  * @ingroup entity_crud
  * @see hook_ENTITY_TYPE_load()
  */
-function hook_entity_load($entities, $entity_type_id) {
+function hook_entity_load(array $entities, $entity_type_id) {
   foreach ($entities as $entity) {
     $entity->foo = mymodule_add_something($entity);
   }
@@ -567,6 +570,40 @@ function hook_ENTITY_TYPE_load($entities) {
 }
 
 /**
+ * Act on content entities when loaded from the storage.
+ *
+ * The results of this hook will be cached.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface[] $entities
+ *   The entities keyed by entity ID.
+ * @param string $entity_type
+ *   The type of entities being loaded (i.e. node, user, comment).
+ *
+ * @see hook_entity_load()
+ */
+function hook_entity_storage_load(array $entities, $entity_type) {
+  foreach ($entities as $entity) {
+    $entity->foo = mymodule_add_something_uncached($entity);
+  }
+}
+
+/**
+ * Act on content entities of a given type when loaded from the storage.
+ *
+ * The results of this hook will be cached if the entity type supports it.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface[] $entities
+ *   The entities keyed by entity ID.
+ *
+ * @see hook_entity_storage_load()
+ */
+function hook_ENTITY_TYPE_storage_load(array $entities) {
+  foreach ($entities as $entity) {
+    $entity->foo = mymodule_add_something_uncached($entity);
+  }
+}
+
+/**
  * Act on an entity before it is created or updated.
  *
  * @param \Drupal\Core\Entity\EntityInterface $entity
diff --git a/core/modules/system/src/Tests/Update/UpdateScriptTest.php b/core/modules/system/src/Tests/Update/UpdateScriptTest.php
index a040529..7c1fa5a 100644
--- a/core/modules/system/src/Tests/Update/UpdateScriptTest.php
+++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php
@@ -77,11 +77,7 @@ function testUpdateAccess() {
     $this->assertResponse(200);
 
     // Access the update page as user 1.
-    $user1 = user_load(1);
-    $user1->pass_raw = user_password();
-    $user1->pass = $this->container->get('password')->hash(trim($user1->pass_raw));
-    db_query("UPDATE {users} SET pass = :pass WHERE uid = :uid", array(':pass' => $user1->getPassword(), ':uid' => $user1->id()));
-    $this->drupalLogin($user1);
+    $this->drupalLogin($this->root_user);
     $this->drupalGet($this->update_url, array('external' => TRUE));
     $this->assertResponse(200);
   }
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
index 01e6872..7b7efaf 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php
@@ -32,7 +32,7 @@
  *   },
  *   base_table = "entity_test",
  *   fieldable = TRUE,
- *   field_cache = FALSE,
+ *   persistent_cache = FALSE,
  *   entity_keys = {
  *     "id" = "id",
  *     "uuid" = "uuid",
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php
index 07d0e75..ab5fdab 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php
@@ -22,7 +22,6 @@
  *   },
  *   base_table = "entity_test",
  *   fieldable = TRUE,
- *   field_cache = TRUE,
  *   entity_keys = {
  *     "id" = "id",
  *     "uuid" = "uuid",
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php
index e881b9a..e15d389 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php
@@ -13,7 +13,7 @@
  * @ContentEntityType(
  *   id = "entity_test_label_callback",
  *   label = @Translation("Entity test label callback"),
- *   field_cache = FALSE,
+ *   persistent_cache = FALSE,
  *   base_table = "entity_test",
  *   label_callback = "entity_test_label_callback",
  *   fieldable = TRUE,
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php
index 6dc35d4..5ad3ba7 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php
@@ -13,7 +13,7 @@
  * @ContentEntityType(
  *   id = "entity_test_no_label",
  *   label = @Translation("Entity Test without label"),
- *   field_cache = FALSE,
+ *   persistent_cache = FALSE,
  *   base_table = "entity_test",
  *   entity_keys = {
  *     "id" = "id",
diff --git a/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php b/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php
index f5e00b1..486471e 100644
--- a/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php
+++ b/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php
@@ -28,6 +28,7 @@ public function setUp() {
     parent::setUp();
     node_access_rebuild();
     $this->drupalCreateContentType(array('type' => 'page'));
+    node_access_test_add_field(entity_load('node_type', 'page'));
     $this->container->get('comment.manager')->addDefaultField('node', 'page', 'comment', CommentItemInterface::OPEN);
     \Drupal::state()->set('node_access_test.private', TRUE);
   }
diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php
index 44d323f..3a9d9f5 100644
--- a/core/modules/user/src/UserStorage.php
+++ b/core/modules/user/src/UserStorage.php
@@ -8,6 +8,7 @@
 namespace Drupal\user;
 
 use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -32,13 +33,6 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt
   protected $password;
 
   /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
    * Constructs a new UserStorage object.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -47,16 +41,15 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt
    *   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\Core\Password\PasswordInterface $password
    *   The password hashing service.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   The user data service.
    */
-  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, PasswordInterface $password, UserDataInterface $user_data) {
-    parent::__construct($entity_type, $database, $entity_manager);
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, PasswordInterface $password) {
+    parent::__construct($entity_type, $database, $entity_manager, $cache);
 
     $this->password = $password;
-    $this->userData = $user_data;
   }
 
   /**
@@ -67,8 +60,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $entity_type,
       $container->get('database'),
       $container->get('entity.manager'),
-      $container->get('password'),
-      $container->get('user.data')
+      $container->get('cache.entity'),
+      $container->get('password')
     );
   }
 
@@ -149,6 +142,8 @@ public function updateLastLoginTimestamp(UserInterface $account) {
       ->fields(array('login' => $account->getLastLoginTime()))
       ->condition('uid', $account->id())
       ->execute();
+    // Ensure that the entity cache is cleared.
+    $this->resetCache(array($account->id()));
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
index 960f735..9d77068 100644
--- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Tests\Core\Entity;
 
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldDefinition;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -47,15 +50,58 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase {
   protected $entityManager;
 
   /**
+   * The entity type ID.
+   *
+   * @var string
+   */
+  protected $entityTypeId = 'entity_test';
+
+  /**
+   * The dependency injection container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The cache backend to use.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cache;
+
+  /**
+   * The database connection to use.
+   *
+   * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $connection;
+
+  /**
    * {@inheritdoc}
    */
   public function setUp() {
     $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface');
     $this->entityType->expects($this->any())
       ->method('id')
-      ->will($this->returnValue('entity_test'));
+      ->will($this->returnValue($this->entityTypeId));
+
+    $this->container = new ContainerBuilder();
+    \Drupal::setContainer($this->container);
 
     $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
   }
 
   /**
@@ -905,10 +951,8 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
   public function testFieldSqlSchemaForEntityWithStringIdentifier() {
     $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
 
-    $container = new ContainerBuilder();
-    $container->set('plugin.manager.field.field_type', $field_type_manager);
-    $container->set('entity.manager', $this->entityManager);
-    \Drupal::setContainer($container);
+    $this->container->set('plugin.manager.field.field_type', $field_type_manager);
+    $this->container->set('entity.manager', $this->entityManager);
 
     $this->entityType->expects($this->any())
       ->method('getKey')
@@ -937,7 +981,7 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
       ->method('getDefinition')
       ->with('test_entity')
       ->will($this->returnValue($this->entityType));
-    $this->entityManager->expects($this->once())
+    $this->entityManager->expects($this->any())
       ->method('getBaseFieldDefinitions')
       ->will($this->returnValue($this->fieldDefinitions));
 
@@ -981,13 +1025,10 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
    */
   public function testCreate() {
     $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
-    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
-    $container = new ContainerBuilder();
-    $container->set('language_manager', $language_manager);
-    $container->set('entity.manager', $this->entityManager);
-    $container->set('module_handler', $module_handler);
-    \Drupal::setContainer($container);
+    $this->container->set('language_manager', $language_manager);
+    $this->container->set('entity.manager', $this->entityManager);
+    $this->container->set('module_handler', $this->moduleHandler);
 
     $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
       ->disableOriginalConstructor()
@@ -995,6 +1036,9 @@ public function testCreate() {
       ->getMockForAbstractClass();
 
     $this->entityType->expects($this->atLeastOnce())
+      ->method('id')
+      ->will($this->returnValue($this->entityTypeId));
+    $this->entityType->expects($this->atLeastOnce())
       ->method('getClass')
       ->will($this->returnValue(get_class($entity)));
     $this->entityType->expects($this->atLeastOnce())
@@ -1031,15 +1075,177 @@ public function testCreate() {
    * Sets up the content entity database storage.
    */
   protected function setUpEntityStorage() {
-    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
-      ->disableOriginalConstructor()
-      ->getMock();
 
     $this->entityManager->expects($this->any())
       ->method('getBaseFieldDefinitions')
       ->will($this->returnValue($this->fieldDefinitions));
 
-    $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager);
+    $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $this->connection, $this->entityManager, $this->cache);
   }
 
+  /**
+   * @covers ::doLoadMultiple
+   * @covers ::buildCacheId
+   * @covers ::getFromPersistentCache
+   */
+  public function testLoadMultiplePersistentCached() {
+    $this->setUpModuleHandlerNoImplementations();
+
+    $key = 'values:' . $this->entityTypeId . ':1';
+    $id = 1;
+    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\ContentEntityDatabaseStorageTestEntityInterface')
+      ->getMockForAbstractClass();
+    $entity->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($id));
+
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('isPersistentlyCacheable')
+      ->will($this->returnValue(TRUE));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('id')
+      ->will($this->returnValue($this->entityTypeId));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('getClass')
+      ->will($this->returnValue(get_class($entity)));
+
+    $this->cache->expects($this->once())
+      ->method('getMultiple')
+      ->with(array($key))
+      ->will($this->returnValue(array($key => (object) array(
+          'data' => $entity,
+        ))));
+    $this->cache->expects($this->never())
+      ->method('set');
+
+    $this->setUpEntityStorage();
+    $entities = $this->entityStorage->loadMultiple(array($id));
+    $this->assertEquals($entity, $entities[$id]);
+  }
+
+  /**
+   * @covers ::doLoadMultiple
+   * @covers ::buildCacheId
+   * @covers ::getFromPersistentCache
+   * @covers ::setPersistentCache
+   */
+  public function testLoadMultipleNoPersistentCache() {
+    $this->setUpModuleHandlerNoImplementations();
+
+    $id = 1;
+    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\ContentEntityDatabaseStorageTestEntityInterface')
+      ->getMockForAbstractClass();
+    $entity->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($id));
+
+    $this->entityType->expects($this->any())
+      ->method('isPersistentlyCacheable')
+      ->will($this->returnValue(FALSE));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('id')
+      ->will($this->returnValue($this->entityTypeId));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('getClass')
+      ->will($this->returnValue(get_class($entity)));
+
+    // There should be no calls to the cache backend for an entity type without
+    // persistent caching.
+    $this->cache->expects($this->never())
+      ->method('getMultiple');
+    $this->cache->expects($this->never())
+      ->method('set');
+
+    $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
+      ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
+      ->setMethods(array('getFromStorage'))
+      ->getMock();
+    $entity_storage->expects($this->once())
+      ->method('getFromStorage')
+      ->with(array($id))
+      ->will($this->returnValue(array($id => $entity)));
+
+    $entities = $entity_storage->loadMultiple(array($id));
+    $this->assertEquals($entity, $entities[$id]);
+
+  }
+
+  /**
+   * @covers ::doLoadMultiple
+   * @covers ::buildCacheId
+   * @covers ::getFromPersistentCache
+   * @covers ::setPersistentCache
+   */
+  public function testLoadMultiplePersistentCacheMiss() {
+    $this->setUpModuleHandlerNoImplementations();
+
+    $id = 1;
+    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\ContentEntityDatabaseStorageTestEntityInterface')
+      ->getMockForAbstractClass();
+    $entity->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($id));
+
+    $this->entityType->expects($this->any())
+      ->method('isPersistentlyCacheable')
+      ->will($this->returnValue(TRUE));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('id')
+      ->will($this->returnValue($this->entityTypeId));
+    $this->entityType->expects($this->atLeastOnce())
+      ->method('getClass')
+      ->will($this->returnValue(get_class($entity)));
+
+    // In case of a cache miss, the entity is loaded from the storage and then
+    // set in the cache.
+    $key = 'values:' . $this->entityTypeId . ':1';
+    $this->cache->expects($this->once())
+      ->method('getMultiple')
+      ->with(array($key))
+      ->will($this->returnValue(array()));
+    $this->cache->expects($this->once())
+      ->method('set')
+      ->with($key, $entity, CacheBackendInterface::CACHE_PERMANENT, array($this->entityTypeId . '_values' => TRUE, 'entity_field_info' => TRUE));
+
+    $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
+      ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
+      ->setMethods(array('getFromStorage'))
+      ->getMock();
+    $entity_storage->expects($this->once())
+      ->method('getFromStorage')
+      ->with(array($id))
+      ->will($this->returnValue(array($id => $entity)));
+
+    $entities = $entity_storage->loadMultiple(array($id));
+    $this->assertEquals($entity, $entities[$id]);
+
+  }
+
+  /**
+   * Sets up the module handler with no implementations.
+   */
+  protected function setUpModuleHandlerNoImplementations() {
+    $this->moduleHandler->expects($this->any())
+      ->method('getImplementations')
+      ->will($this->returnValueMap(array(
+        array('entity_load', array()),
+        array($this->entityTypeId . '_load', array())
+      )));
+
+    $this->container->set('module_handler', $this->moduleHandler);
+  }
+
+}
+
+/**
+ * Provides an entity with dummy implementations of static methods, because
+ * those cannot be mocked.
+ */
+abstract class ContentEntityDatabaseStorageTestEntityInterface implements EntityInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
+  }
 }
