diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 1fd73e7..a9b6d97 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -17,8 +17,6 @@
 use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\EntityStorageException;
-use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -56,11 +54,9 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
   const MAX_ID_LENGTH = 166;
 
   /**
-   * The UUID service.
-   *
-   * @var \Drupal\Component\Uuid\UuidInterface
+   * {@inheritdoc}
    */
-  protected $uuidService;
+  protected $uuidKey = 'uuid';
 
   /**
    * Name of the entity's status key or FALSE if a status is not supported.
@@ -107,7 +103,6 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
   public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
     parent::__construct($entity_type);
 
-    $this->idKey = $this->entityType->getKey('id');
     $this->statusKey = $this->entityType->getKey('status');
 
     $this->configFactory = $config_factory;
@@ -130,53 +125,6 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-
-    // Load any remaining entities. This is the case if $ids is set to NULL (so
-    // we load all entities).
-    if ($ids === NULL || $ids) {
-      $queried_entities = $this->buildQuery($ids);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which calls the
-    // entity type specific load callback, for example hook_node_type_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
-  }
-
-  /**
    * Implements Drupal\Core\Entity\EntityStorageInterface::loadRevision().
    */
   public function loadRevision($revision_id) {
@@ -205,28 +153,9 @@ public static function getIDFromConfigName($config_name, $config_prefix) {
   }
 
   /**
-   * Builds the query to load the entity.
-   *
-   * This has full revision support. For entities requiring special queries,
-   * the class can be extended, and the default query can be constructed by
-   * calling parent::buildQuery(). This is usually necessary when the object
-   * being loaded needs to be augmented with additional data from another
-   * table, such as loading node type into comments or vocabulary machine name
-   * into terms, however it can also support $conditions on different tables.
-   * See Drupal\comment\CommentStorage::buildQuery() or
-   * Drupal\taxonomy\TermStorage::buildQuery() for examples.
-   *
-   * @param $ids
-   *   An array of entity IDs, or NULL to load all entities.
-   * @param $revision_id
-   *   The ID of the revision to load, or FALSE if this query is asking for the
-   *   most current revision(s).
-   *
-   * @return SelectQuery
-   *   A SelectQuery object for loading the entity.
+   * {@inheritdoc}
    */
-  protected function buildQuery($ids, $revision_id = FALSE) {
-    $config_class = $this->entityType->getClass();
+  protected function doLoadMultiple(array $ids = NULL) {
     $prefix = $this->getConfigPrefix();
 
     // Get the names of the configuration entities we are going to load.
@@ -242,70 +171,36 @@ protected function buildQuery($ids, $revision_id = FALSE) {
     }
 
     // Load all of the configuration entities.
-    $result = array();
+    $records = array();
     foreach ($this->configFactory->loadMultiple($names) as $config) {
-      $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityTypeId);
+      $records[$config->get($this->idKey)] = $config->get();
     }
-    return $result;
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
-   * Implements Drupal\Core\Entity\EntityStorageInterface::create().
+   * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $class = $this->entityType->getClass();
-    $class::preCreate($this, $values);
-
+  protected function doCreate(array $values) {
     // Set default language to site default if not provided.
     $values += array('langcode' => $this->languageManager->getDefaultLanguage()->id);
-
-    $entity = new $class($values, $this->entityTypeId);
-    // Mark this entity as new, so isNew() returns TRUE. This does not check
-    // whether a configuration entity with the same ID (if any) already exists.
-    $entity->enforceIsNew();
-
-    // Assign a new UUID if there is none yet.
-    if (!$entity->uuid()) {
-      $entity->set('uuid', $this->uuidService->generate());
-    }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
+    $entity = new $this->entityClass($values, $this->entityTypeId);
 
     // Default status to enabled.
     if (!empty($this->statusKey) && !isset($entity->{$this->statusKey})) {
       $entity->{$this->statusKey} = TRUE;
     }
-
     return $entity;
   }
 
   /**
-   * Implements Drupal\Core\Entity\EntityStorageInterface::delete().
+   * {@inheritdoc}
    */
-  public function delete(array $entities) {
-    if (!$entities) {
-      // If no IDs or invalid IDs were passed, do nothing.
-      return;
-    }
-
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preDelete($this, $entities);
-    foreach ($entities as $entity) {
-      $this->invokeHook('predelete', $entity);
-    }
-
+  protected function doDelete($entities) {
     foreach ($entities as $entity) {
       $config = $this->configFactory->get($this->getConfigPrefix() . $entity->id());
       $config->delete();
     }
-
-    $entity_class::postDelete($this, $entities);
-    foreach ($entities as $entity) {
-      $this->invokeHook('delete', $entity);
-    }
   }
 
   /**
@@ -315,32 +210,16 @@ public function delete(array $entities) {
    *   When attempting to save a configuration entity that has no ID.
    */
   public function save(EntityInterface $entity) {
-    $prefix = $this->getConfigPrefix();
-
     // Configuration entity IDs are strings, and '0' is a valid ID.
     $id = $entity->id();
     if ($id === NULL || $id === '') {
       throw new EntityMalformedException('The entity does not have an ID.');
     }
 
-    // Load the stored entity, if any.
-    // At this point, the original ID can only be NULL or a valid ID.
-    if ($entity->getOriginalId() !== NULL) {
-      $id = $entity->getOriginalId();
-    }
-    $config = $this->configFactory->get($prefix . $id);
-
-    // Prevent overwriting an existing configuration file if the entity is new.
-    if ($entity->isNew() && !$config->isNew()) {
-      throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
-    }
-
-    if (!$config->isNew() && !isset($entity->original)) {
-      $entity->original = $this->loadUnchanged($id);
-    }
-
     // Check the configuration entity ID length.
     // @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
+    // @todo Consider moving this to a protected method on the parent class, and
+    //   abstracting it for all entity types.
     if (strlen($entity->{$this->idKey}) > self::MAX_ID_LENGTH) {
       throw new ConfigEntityIdLengthException(String::format('Configuration entity ID @id exceeds maximum allowed length of @length characters.', array(
         '@id' => $entity->{$this->idKey},
@@ -348,9 +227,15 @@ public function save(EntityInterface $entity) {
       )));
     }
 
-    $entity->preSave($this);
-    $this->invokeHook('presave', $entity);
+    return parent::save($entity);
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+    $is_new = $entity->isNew();
+    $prefix = $this->getConfigPrefix();
     if ($id !== $entity->id()) {
       // Renaming a config object needs to cater for:
       // - Storage needs to access the original object.
@@ -358,34 +243,26 @@ public function save(EntityInterface $entity) {
       // - All instances of the object need to be renamed.
       $config = $this->configFactory->rename($prefix . $id, $prefix . $entity->id());
     }
+    else {
+      $config = $this->configFactory->get($prefix . $id);
+    }
 
     // Retrieve the desired properties and set them in config.
     foreach ($entity->toArray() as $key => $value) {
       $config->set($key, $value);
     }
-
-    if (!$config->isNew()) {
-      $return = SAVED_UPDATED;
-      $config->save();
-      $entity->postSave($this, TRUE);
-      $this->invokeHook('update', $entity);
-    }
-    else {
-      $return = SAVED_NEW;
-      $config->save();
-      $entity->enforceIsNew(FALSE);
-      $entity->postSave($this, FALSE);
-      $this->invokeHook('insert', $entity);
-    }
-
-    // After saving, this is now the "original entity", and subsequent saves
-    // will be updates instead of inserts, and updates must always be able to
-    // correctly identify the original entity.
-    $entity->setOriginalId($entity->id());
-
-    unset($entity->original);
-
-    return $return;
+    $config->save();
+
+    return $is_new ? SAVED_NEW : SAVED_UPDATED;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    $prefix = $this->getConfigPrefix();
+    $config = $this->configFactory->get($prefix . $id);
+    return !$config->isNew();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
index 1731ac4..7960edc 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
@@ -58,6 +58,11 @@ class ConfigEntityType extends EntityType {
   /**
    * {@inheritdoc}
    */
+  protected $static_cache = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
   public function getControllerClasses() {
     return parent::getControllerClasses() + array(
       'storage' => 'Drupal\Core\Config\Entity\ConfigEntityStorage',
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index 22cf527..5ff59ec 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -59,13 +59,6 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
   protected $revisionDataTable;
 
   /**
-   * Whether this entity type should use the static cache.
-   *
-   * @var boolean
-   */
-  protected $cache;
-
-  /**
    * Active database connection.
    *
    * @var \Drupal\Core\Database\Connection
@@ -106,11 +99,6 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
     $this->database = $database;
     $this->fieldInfo = $field_info;
 
-    // Check if the entity type supports IDs.
-    if ($this->entityType->hasKey('id')) {
-      $this->idKey = $this->entityType->getKey('id');
-    }
-
     // Check if the entity type supports UUIDs.
     $this->uuidKey = $this->entityType->getKey('uuid');
 
@@ -134,74 +122,27 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
   /**
    * {@inheritdoc}
    */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed. The $ids array is reduced as items are loaded from cache,
-    // and we need to know if it's empty for this reason to avoid querying the
-    // database when all requested entities are loaded from cache.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-    // Try to load entities from the static cache, if the entity type supports
-    // static caching.
-    if ($this->cache && $ids) {
-      $entities += $this->cacheGet($ids);
-      // If any entities were loaded, remove them from the ids still to load.
-      if ($passed_ids) {
-        $ids = array_keys(array_diff_key($passed_ids, $entities));
-      }
-    }
-
-    // Load any remaining entities from the database. This is the case if $ids
-    // is set to NULL (so we load all entities) or if there are any ids left to
-    // load.
-    if ($ids === NULL || $ids) {
-      // Build and execute the query.
-      $query_result = $this->buildQuery($ids)->execute();
-      $queried_entities = $query_result->fetchAllAssoc($this->idKey);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    if ($this->cache) {
-      // Add entities to the cache.
-      if (!empty($queried_entities)) {
-        $this->cacheSet($queried_entities);
-      }
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
+  protected function doLoadMultiple(array $ids = NULL) {
+    // Build and execute the query.
+    $records = $this
+      ->buildQuery($ids)
+      ->execute()
+      ->fetchAllAssoc($this->idKey);
+
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
    * Maps from storage records to entity objects.
    *
+   * This will attach fields, if the entity is fieldable. It calls
+   * hook_entity_load() for modules which need to add data to all entities.
+   * It also calls hook_TYPE_load() on the loaded entities. For example
+   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+   * expects special parameters apart from the queried entities, you can set
+   * $this->hookLoadArguments prior to calling the method.
+   * See Drupal\node\NodeStorage::attachLoad() for an example.
+   *
    * @param array $records
    *   Associative array of query results, keyed on the entity ID.
    *
@@ -237,6 +178,12 @@ protected function mapFromStorageRecords(array $records) {
       }
     }
     $this->attachPropertyData($entities);
+
+    // Attach field values.
+    if ($this->entityType->isFieldable()) {
+      $this->loadFieldItems($entities);
+    }
+
     return $entities;
   }
 
@@ -319,15 +266,14 @@ protected function attachPropertyData(array &$entities) {
   public function loadRevision($revision_id) {
     // Build and execute the query.
     $query_result = $this->buildQuery(array(), $revision_id)->execute();
-    $queried_entities = $query_result->fetchAllAssoc($this->idKey);
+    $records = $query_result->fetchAllAssoc($this->idKey);
 
-    // Pass the loaded entities from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
+    if (!empty($records)) {
+      // Convert the raw records to entity objects.
+      $entities = $this->mapFromStorageRecords($records);
+      $this->postLoad($entities);
+      return reset($entities);
     }
-    return reset($queried_entities);
   }
 
   /**
@@ -388,7 +334,7 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
    *   The ID of the revision to load, or FALSE if this query is asking for the
    *   most current revision(s).
    *
-   * @return SelectQuery
+   * @return \Drupal\Core\Database\Query\Select
    *   A SelectQuery object for loading the entity.
    */
   protected function buildQuery($ids, $revision_id = FALSE) {
@@ -438,32 +384,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   }
 
   /**
-   * Attaches data to entities upon loading.
-   *
-   * This will attach fields, if the entity is fieldable. It calls
-   * hook_entity_load() for modules which need to add data to all entities.
-   * It also calls hook_TYPE_load() on the loaded entities. For example
-   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
-   * expects special parameters apart from the queried entities, you can set
-   * $this->hookLoadArguments prior to calling the method.
-   * See Drupal\node\NodeStorage::attachLoad() for an example.
-   *
-   * @param $queried_entities
-   *   Associative array of query results, keyed on the entity ID.
-   */
-  protected function postLoad(array &$queried_entities) {
-    // Map the loaded records into entity objects and according fields.
-    $queried_entities = $this->mapFromStorageRecords($queried_entities);
-
-    // Attach field values.
-    if ($this->entityType->isFieldable()) {
-      $this->loadFieldItems($queried_entities);
-    }
-
-    parent::postLoad($queried_entities);
-  }
-
-  /**
    * Implements \Drupal\Core\Entity\EntityStorageInterface::delete().
    */
   public function delete(array $entities) {
@@ -474,48 +394,8 @@ public function delete(array $entities) {
 
     $transaction = $this->database->startTransaction();
     try {
-      $entity_class = $this->entityClass;
-      $entity_class::preDelete($this, $entities);
+      parent::delete($entities);
 
-      foreach ($entities as $entity) {
-        $this->invokeHook('predelete', $entity);
-      }
-      $ids = array_keys($entities);
-
-      $this->database->delete($this->entityType->getBaseTable())
-        ->condition($this->idKey, $ids)
-        ->execute();
-
-      if ($this->revisionTable) {
-        $this->database->delete($this->revisionTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      if ($this->dataTable) {
-        $this->database->delete($this->dataTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      if ($this->revisionDataTable) {
-        $this->database->delete($this->revisionDataTable)
-          ->condition($this->idKey, $ids)
-          ->execute();
-      }
-
-      foreach ($entities as $entity) {
-        $this->invokeFieldMethod('delete', $entity);
-        $this->deleteFieldItems($entity);
-      }
-
-      // Reset the cache as soon as the changes have been applied.
-      $this->resetCache($ids);
-
-      $entity_class::postDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('delete', $entity);
-      }
       // Ignore slave server temporarily.
       db_ignore_slave();
     }
@@ -529,92 +409,53 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
+  protected function doDelete($entities) {
+    $ids = array_keys($entities);
+
+    $this->database->delete($this->entityType->getBaseTable())
+      ->condition($this->idKey, $ids)
+      ->execute();
+
+    if ($this->revisionTable) {
+      $this->database->delete($this->revisionTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
+
+    if ($this->dataTable) {
+      $this->database->delete($this->dataTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
+
+    if ($this->revisionDataTable) {
+      $this->database->delete($this->revisionDataTable)
+        ->condition($this->idKey, $ids)
+        ->execute();
+    }
+
+    foreach ($entities as $entity) {
+      $this->invokeFieldMethod('delete', $entity);
+      $this->deleteFieldItems($entity);
+    }
+
+    // Reset the cache as soon as the changes have been applied.
+    $this->resetCache($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(EntityInterface $entity) {
     $transaction = $this->database->startTransaction();
     try {
       // Sync the changes made in the fields array to the internal values array.
       $entity->updateOriginalValues();
 
-      // Load the stored entity, if any.
-      if (!$entity->isNew() && !isset($entity->original)) {
-        $id = $entity->id();
-        if ($entity->getOriginalId() !== NULL) {
-          $id = $entity->getOriginalId();
-        }
-        $entity->original = $this->loadUnchanged($id);
-      }
-
-      $entity->preSave($this);
-      $this->invokeFieldMethod('preSave', $entity);
-      $this->invokeHook('presave', $entity);
-
-      // Create the storage record to be saved.
-      $record = $this->mapToStorageRecord($entity);
-
-      if (!$entity->isNew()) {
-        if ($entity->isDefaultRevision()) {
-          $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
-        }
-        else {
-          // @todo, should a different value be returned when saving an entity
-          // with $isDefaultRevision = FALSE?
-          $return = FALSE;
-        }
-        if ($this->revisionTable) {
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
-        }
-        if ($this->dataTable) {
-          $this->savePropertyData($entity);
-        }
-        if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
-        }
-        if ($this->revisionTable) {
-          $entity->setNewRevision(FALSE);
-        }
-        $this->invokeFieldMethod('update', $entity);
-        $this->saveFieldItems($entity, TRUE);
-        $this->resetCache(array($entity->id()));
-        $entity->postSave($this, TRUE);
-        $this->invokeHook('update', $entity);
-        if ($this->dataTable) {
-          $this->invokeTranslationHooks($entity);
-        }
-      }
-      else {
-        // Ensure the entity is still seen as new after assigning it an id,
-        // while storing its data.
-        $entity->enforceIsNew();
-        $return = drupal_write_record($this->entityType->getBaseTable(), $record);
-        $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
-        if ($this->revisionTable) {
-          $entity->setNewRevision();
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
-        }
-        if ($this->dataTable) {
-          $this->savePropertyData($entity);
-        }
-        if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
-        }
-
-        $entity->enforceIsNew(FALSE);
-        if ($this->revisionTable) {
-          $entity->setNewRevision(FALSE);
-        }
-
-        $this->invokeFieldMethod('insert', $entity);
-        $this->saveFieldItems($entity, FALSE);
-        // Reset general caches, but keep caches specific to certain entities.
-        $this->resetCache(array());
-        $entity->postSave($this, FALSE);
-        $this->invokeHook('insert', $entity);
-      }
+      $return = parent::save($entity);
 
       // Ignore slave server temporarily.
       db_ignore_slave();
-      unset($entity->original);
-
       return $return;
     }
     catch (\Exception $e) {
@@ -625,6 +466,77 @@ public function save(EntityInterface $entity) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+    // Create the storage record to be saved.
+    $record = $this->mapToStorageRecord($entity);
+
+    $is_new = $entity->isNew();
+    if (!$is_new) {
+      if ($entity->isDefaultRevision()) {
+        $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
+      }
+      else {
+        // @todo, should a different value be returned when saving an entity
+        // with $isDefaultRevision = FALSE?
+        $return = FALSE;
+      }
+      if ($this->revisionTable) {
+        $record->{$this->revisionKey} = $this->saveRevision($entity);
+      }
+      if ($this->dataTable) {
+        $this->savePropertyData($entity);
+      }
+      if ($this->revisionDataTable) {
+        $this->savePropertyData($entity, 'revision_data_table');
+      }
+      if ($this->revisionTable) {
+        $entity->setNewRevision(FALSE);
+      }
+      $cache_ids = array($entity->id());
+    }
+    else {
+      // Ensure the entity is still seen as new after assigning it an id,
+      // while storing its data.
+      $entity->enforceIsNew();
+      $return = drupal_write_record($this->entityType->getBaseTable(), $record);
+      $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
+      if ($this->revisionTable) {
+        $entity->setNewRevision();
+        $record->{$this->revisionKey} = $this->saveRevision($entity);
+      }
+      if ($this->dataTable) {
+        $this->savePropertyData($entity);
+      }
+      if ($this->revisionDataTable) {
+        $this->savePropertyData($entity, 'revision_data_table');
+      }
+      $entity->enforceIsNew(FALSE);
+      if ($this->revisionTable) {
+        $entity->setNewRevision(FALSE);
+      }
+      // Reset general caches, but keep caches specific to certain entities.
+      $cache_ids = array();
+    }
+    $this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
+    $this->saveFieldItems($entity, !$is_new);
+    $this->resetCache($cache_ids);
+
+    if (!$is_new && $this->dataTable) {
+      $this->invokeTranslationHooks($entity);
+    }
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    return !$entity->isNew();
+  }
+
+  /**
    * Stores the entity property language-aware data.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
@@ -661,6 +573,16 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function invokeHook($hook, EntityInterface $entity) {
+    if ($hook == 'presave') {
+      $this->invokeFieldMethod('preSave', $entity);
+    }
+    parent::invokeHook($hook, $entity);
+  }
+
+  /**
    * Maps from an entity object to the storage record.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 3a7e7e5..98dbd2f 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -27,6 +27,12 @@ public function loadMultiple(array $ids = NULL) {
   /**
    * {@inheritdoc}
    */
+  protected function doLoadMultiple(array $ids = NULL) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function load($id) {
     return NULL;
   }
@@ -60,6 +66,12 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
+  protected function doDelete($entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(EntityInterface $entity) {
   }
 
@@ -106,4 +118,16 @@ protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceC
   protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index cf43ee6..b40f73a 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -23,13 +23,6 @@
   protected $bundleKey = FALSE;
 
   /**
-   * Name of the entity class.
-   *
-   * @var string
-   */
-  protected $entityClass;
-
-  /**
    * Constructs a ContentEntityStorageBase object.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -39,7 +32,6 @@ public function __construct(EntityTypeInterface $entity_type) {
     parent::__construct($entity_type);
 
     $this->bundleKey = $this->entityType->getKey('bundle');
-    $this->entityClass = $this->entityType->getClass();
   }
 
   /**
@@ -54,10 +46,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   /**
    * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preCreate($this, $values);
-
+  protected function doCreate(array $values) {
     // We have to determine the bundle first.
     $bundle = FALSE;
     if ($this->bundleKey) {
@@ -66,8 +55,7 @@ public function create(array $values = array()) {
       }
       $bundle = $values[$this->bundleKey];
     }
-    $entity = new $entity_class(array(), $this->entityTypeId, $bundle);
-    $entity->enforceIsNew();
+    $entity = new $this->entityClass(array(), $this->entityTypeId, $bundle);
 
     foreach ($entity as $name => $field) {
       if (isset($values[$name])) {
@@ -83,12 +71,6 @@ public function create(array $values = array()) {
     foreach ($values as $name => $value) {
       $entity->$name = $value;
     }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
-
     return $entity;
   }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
index 180dfef..7fb61a7 100644
--- a/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/EntityDatabaseStorage.php
@@ -9,16 +9,7 @@
 
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\Query\QueryInterface;
-use Drupal\Core\Language\Language;
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Uuid\Uuid;
-use Drupal\field\FieldInfo;
-use Drupal\field\FieldConfigUpdateForbiddenException;
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldInstanceConfigInterface;
-use Drupal\field\Entity\FieldConfig;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -83,9 +74,6 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
     $this->database = $database;
     $this->uuidService = $uuid_service;
 
-    // Check if the entity type supports IDs.
-    $this->idKey = $this->entityType->getKey('id');
-
     // Check if the entity type supports UUIDs.
     $this->uuidKey = $this->entityType->getKey('uuid');
   }
@@ -93,76 +81,14 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
   /**
    * {@inheritdoc}
    */
-  public function loadMultiple(array $ids = NULL) {
-    $entities = array();
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed. The $ids array is reduced as items are loaded from cache,
-    // and we need to know if it's empty for this reason to avoid querying the
-    // database when all requested entities are loaded from cache.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-    // Try to load entities from the static cache, if the entity type supports
-    // static caching.
-    if ($this->cache && $ids) {
-      $entities += $this->cacheGet($ids);
-      // If any entities were loaded, remove them from the ids still to load.
-      if ($passed_ids) {
-        $ids = array_keys(array_diff_key($passed_ids, $entities));
-      }
-    }
-
-    // Load any remaining entities from the database. This is the case if $ids
-    // is set to NULL (so we load all entities) or if there are any ids left to
-    // load.
-    if ($ids === NULL || $ids) {
-      // Build and execute the query.
-      $query_result = $this->buildQuery($ids)->execute();
-
-      if ($class = $this->entityType->getClass()) {
-        // We provide the necessary arguments for PDO to create objects of the
-        // specified entity class.
-        // @see \Drupal\Core\Entity\EntityInterface::__construct()
-        $query_result->setFetchMode(\PDO::FETCH_CLASS, $class, array(array(), $this->entityTypeId));
-      }
-      $queried_entities = $query_result->fetchAllAssoc($this->idKey);
-    }
-
-    // Pass all entities loaded from the database through $this->postLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->postLoad($queried_entities);
-      $entities += $queried_entities;
-    }
-
-    if ($this->cache) {
-      // Add entities to the cache.
-      if (!empty($queried_entities)) {
-        $this->cacheSet($queried_entities);
-      }
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->id()] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function load($id) {
-    $entities = $this->loadMultiple(array($id));
-    return isset($entities[$id]) ? $entities[$id] : NULL;
+  protected function doLoadMultiple(array $ids = NULL) {
+    // Build and execute the query.
+    $records = $this
+      ->buildQuery($ids)
+      ->execute()
+      ->fetchAllAssoc($this->idKey, \PDO::FETCH_ASSOC);
+
+    return $this->mapFromStorageRecords($records);
   }
 
   /**
@@ -180,41 +106,15 @@ public function deleteRevision($revision_id) {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function loadByProperties(array $values = array()) {
-    // Build a query to fetch the entity IDs.
-    $entity_query = \Drupal::entityQuery($this->entityTypeId);
-    $this->buildPropertyQuery($entity_query, $values);
-    $result = $entity_query->execute();
-    return $result ? $this->loadMultiple($result) : array();
-  }
-
-  /**
-   * Builds an entity query.
-   *
-   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
-   *   EntityQuery instance.
-   * @param array $values
-   *   An associative array of properties of the entity, where the keys are the
-   *   property names and the values are the values those properties must have.
-   */
-  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
-    foreach ($values as $name => $value) {
-      $entity_query->condition($name, $value);
-    }
-  }
-
-  /**
    * Builds the query to load the entity.
    *
    * @param array|null $ids
    *   An array of entity IDs, or NULL to load all entities.
    *
-   * @return SelectQuery
+   * @return \Drupal\Core\Database\Query\Select
    *   A SelectQuery object for loading the entity.
    */
-  protected function buildQuery($ids, $revision_id = FALSE) {
+  protected function buildQuery($ids) {
     $query = $this->database->select($this->entityType->getBaseTable(), 'base');
 
     $query->addTag($this->entityTypeId . '_load_multiple');
@@ -233,29 +133,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   /**
    * {@inheritdoc}
    */
-  public function create(array $values = array()) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::preCreate($this, $values);
-
-    $entity = new $entity_class($values, $this->entityTypeId);
-    $entity->enforceIsNew();
-
-    // Assign a new UUID if there is none yet.
-    if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
-      $entity->{$this->uuidKey} = $this->uuidService->generate();
-    }
-    $entity->postCreate($this);
-
-    // Modules might need to add or change the data initially held by the new
-    // entity object, for instance to fill-in default values.
-    $this->invokeHook('create', $entity);
-
-    return $entity;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function delete(array $entities) {
     if (!$entities) {
       // If no IDs or invalid IDs were passed, do nothing.
@@ -264,24 +141,8 @@ public function delete(array $entities) {
     $transaction = $this->database->startTransaction();
 
     try {
-      $entity_class = $this->entityType->getClass();
-      $entity_class::preDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('predelete', $entity);
-      }
-      $ids = array_keys($entities);
+      parent::delete($entities);
 
-      $this->database->delete($this->entityType->getBaseTable())
-        ->condition($this->idKey, $ids, 'IN')
-        ->execute();
-
-      // Reset the cache as soon as the changes have been applied.
-      $this->resetCache($ids);
-
-      $entity_class::postDelete($this, $entities);
-      foreach ($entities as $entity) {
-        $this->invokeHook('delete', $entity);
-      }
       // Ignore slave server temporarily.
       db_ignore_slave();
     }
@@ -295,41 +156,27 @@ public function delete(array $entities) {
   /**
    * {@inheritdoc}
    */
+  protected function doDelete($entities) {
+    $ids = array_keys($entities);
+
+    $this->database->delete($this->entityType->getBaseTable())
+      ->condition($this->idKey, $ids, 'IN')
+      ->execute();
+
+    // Reset the cache as soon as the changes have been applied.
+    $this->resetCache($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(EntityInterface $entity) {
     $transaction = $this->database->startTransaction();
     try {
-      // Load the stored entity, if any.
-      if (!$entity->isNew() && !isset($entity->original)) {
-        $id = $entity->id();
-        if ($entity->getOriginalId() !== NULL) {
-          $id = $entity->getOriginalId();
-        }
-        $entity->original = $this->loadUnchanged($id);
-      }
-
-      $entity->preSave($this);
-      $this->invokeHook('presave', $entity);
-
-      if (!$entity->isNew()) {
-        $return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey);
-        $this->resetCache(array($entity->id()));
-        $entity->postSave($this, TRUE);
-        $this->invokeHook('update', $entity);
-      }
-      else {
-        $return = drupal_write_record($this->entityType->getBaseTable(), $entity);
-        // Reset general caches, but keep caches specific to certain entities.
-        $this->resetCache(array());
-
-        $entity->enforceIsNew(FALSE);
-        $entity->postSave($this, FALSE);
-        $this->invokeHook('insert', $entity);
-      }
+      $return = parent::save($entity);
 
       // Ignore slave server temporarily.
       db_ignore_slave();
-      unset($entity->original);
-
       return $return;
     }
     catch (\Exception $e) {
@@ -342,6 +189,30 @@ public function save(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
+  protected function doSave($id, EntityInterface $entity) {
+    if (!$entity->isNew()) {
+      $return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey);
+      $this->resetCache(array($entity->id()));
+    }
+    else {
+      $return = drupal_write_record($this->entityType->getBaseTable(), $entity);
+      // Reset general caches, but keep caches specific to certain entities.
+      $this->resetCache(array());
+    }
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function has($id, EntityInterface $entity) {
+    return !$entity->isNew();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getQueryServiceName() {
     return 'entity.query.sql';
   }
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 1befefc..7b69656 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -6,9 +6,9 @@
  */
 
 namespace Drupal\Core\Entity;
+
+use Drupal\Component\Utility\String;
 use Drupal\Core\Entity\Query\QueryInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * A base entity storage class.
@@ -20,14 +20,7 @@
    *
    * @var array
    */
-  protected $entityCache = array();
-
-  /**
-   * Whether this entity type should use the static cache.
-   *
-   * @var boolean
-   */
-  protected $cache;
+  protected $entities = array();
 
   /**
    * Entity type ID for this controller instance.
@@ -60,6 +53,20 @@
   protected $uuidKey;
 
   /**
+   * The UUID service.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuidService;
+
+  /**
+   * Name of the entity class.
+   *
+   * @var string
+   */
+  protected $entityClass;
+
+  /**
    * Constructs an EntityStorageBase instance.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -68,8 +75,8 @@
   public function __construct(EntityTypeInterface $entity_type) {
     $this->entityTypeId = $entity_type->id();
     $this->entityType = $entity_type;
-    // Check if the entity type supports static caching of loaded entities.
-    $this->cache = $this->entityType->isStaticallyCacheable();
+    $this->idKey = $this->entityType->getKey('id');
+    $this->entityClass = $this->entityType->getClass();
   }
 
   /**
@@ -98,30 +105,30 @@ public function loadUnchanged($id) {
    * {@inheritdoc}
    */
   public function resetCache(array $ids = NULL) {
-    if ($this->cache && isset($ids)) {
+    if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
       foreach ($ids as $id) {
-        unset($this->entityCache[$id]);
+        unset($this->entities[$id]);
       }
     }
     else {
-      $this->entityCache = array();
+      $this->entities = array();
     }
   }
 
   /**
    * Gets entities from the static cache.
    *
-   * @param $ids
+   * @param array $ids
    *   If not empty, return entities that match these IDs.
    *
-   * @return
+   * @return \Drupal\Core\Entity\EntityInterface[]
    *   Array of entities from the entity cache.
    */
-  protected function cacheGet($ids) {
+  protected function getFromStaticCache(array $ids) {
     $entities = array();
     // Load any available entities from the internal cache.
-    if ($this->cache && !empty($this->entityCache)) {
-      $entities += array_intersect_key($this->entityCache, array_flip($ids));
+    if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
+      $entities += array_intersect_key($this->entities, array_flip($ids));
     }
     return $entities;
   }
@@ -129,12 +136,12 @@ protected function cacheGet($ids) {
   /**
    * Stores entities in the static entity cache.
    *
-   * @param $entities
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
    *   Entities to store in the cache.
    */
-  protected function cacheSet($entities) {
-    if ($this->cache) {
-      $this->entityCache += $entities;
+  protected function setStaticCache(array $entities) {
+    if ($this->entityType->isStaticallyCacheable()) {
+      $this->entities += $entities;
     }
   }
 
@@ -155,27 +162,265 @@ protected function invokeHook($hook, EntityInterface $entity) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function create(array $values = array()) {
+    $entity_class = $this->entityClass;
+    $entity_class::preCreate($this, $values);
+
+    // Assign a new UUID if there is none yet.
+    if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
+      $values[$this->uuidKey] = $this->uuidService->generate();
+    }
+
+    $entity = $this->doCreate($values);
+    $entity->enforceIsNew();
+
+    $entity->postCreate($this);
+
+    // Modules might need to add or change the data initially held by the new
+    // entity object, for instance to fill-in default values.
+    $this->invokeHook('create', $entity);
+
+    return $entity;
+  }
+
+  /**
+   * Performs storage-specific creation of entities.
+   *
+   * @param array $values
+   *   An array of values to set, keyed by property name.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   */
+  protected function doCreate(array $values) {
+    return new $this->entityClass($values, $this->entityTypeId);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($id) {
+    $entities = $this->loadMultiple(array($id));
+    return isset($entities[$id]) ? $entities[$id] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids = NULL) {
+    $entities = array();
+
+    // Create a new variable which is either a prepared version of the $ids
+    // array for later comparison with the entity cache, or FALSE if no $ids
+    // were passed. The $ids array is reduced as items are loaded from cache,
+    // and we need to know if it's empty for this reason to avoid querying the
+    // database when all requested entities are loaded from cache.
+    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
+    // Try to load entities from the static cache, if the entity type supports
+    // static caching.
+    if ($this->entityType->isStaticallyCacheable() && $ids) {
+      $entities += $this->getFromStaticCache($ids);
+      // If any entities were loaded, remove them from the ids still to load.
+      if ($passed_ids) {
+        $ids = array_keys(array_diff_key($passed_ids, $entities));
+      }
+    }
+
+    // Load any remaining entities from the database. This is the case if $ids
+    // is set to NULL (so we load all entities) or if there are any ids left to
+    // load.
+    if ($ids === NULL || $ids) {
+      $queried_entities = $this->doLoadMultiple($ids);
+    }
+
+    // Pass all entities loaded from the database through $this->postLoad(),
+    // which attaches fields (if supported by the entity type) and calls the
+    // entity type specific load callback, for example hook_node_load().
+    if (!empty($queried_entities)) {
+      $this->postLoad($queried_entities);
+      $entities += $queried_entities;
+    }
+
+    if ($this->entityType->isStaticallyCacheable()) {
+      // Add entities to the cache.
+      if (!empty($queried_entities)) {
+        $this->setStaticCache($queried_entities);
+      }
+    }
+
+    // Ensure that the returned array is ordered the same as the original
+    // $ids array if this was passed in and remove any invalid ids.
+    if ($passed_ids) {
+      // Remove any invalid ids from the array.
+      $passed_ids = array_intersect_key($passed_ids, $entities);
+      foreach ($entities as $entity) {
+        $passed_ids[$entity->id()] = $entity;
+      }
+      $entities = $passed_ids;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Performs storage-specific loading of entities.
+   *
+   * @param array|null $ids
+   *   (optional) An array of entity IDs, or NULL to load all entities.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   Associative array of entities, keyed on the entity ID.
+   */
+  abstract protected function doLoadMultiple(array $ids = NULL);
+
+  /**
    * Attaches data to entities upon loading.
    *
-   * @param array $queried_entities
+   * @param array $entities
    *   Associative array of query results, keyed on the entity ID.
    */
-  protected function postLoad(array &$queried_entities) {
-    $entity_class = $this->entityType->getClass();
-    $entity_class::postLoad($this, $queried_entities);
+  protected function postLoad(array &$entities) {
+    $entity_class = $this->entityClass;
+    $entity_class::postLoad($this, $entities);
     // Call hook_entity_load().
     foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
       $function = $module . '_entity_load';
-      $function($queried_entities, $this->entityTypeId);
+      $function($entities, $this->entityTypeId);
     }
     // Call hook_TYPE_load().
     foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
       $function = $module . '_' . $this->entityTypeId . '_load';
-      $function($queried_entities);
+      $function($entities);
     }
   }
 
   /**
+   * Maps from storage records to entity objects.
+   *
+   * @param array $records
+   *   Associative array of query results, keyed on the entity ID.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   An array of entity objects implementing the EntityInterface.
+   */
+  protected function mapFromStorageRecords(array $records) {
+    $entities = array();
+    foreach ($records as $record) {
+      $entity = new $this->entityClass($record, $this->entityTypeId);
+      $entities[$entity->id()] = $entity;
+    }
+    return $entities;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $entities) {
+    if (!$entities) {
+      // If no IDs or invalid IDs were passed, do nothing.
+      return;
+    }
+
+    $entity_class = $this->entityClass;
+    $entity_class::preDelete($this, $entities);
+    foreach ($entities as $entity) {
+      $this->invokeHook('predelete', $entity);
+    }
+
+    $this->doDelete($entities);
+
+    $entity_class::postDelete($this, $entities);
+    foreach ($entities as $entity) {
+      $this->invokeHook('delete', $entity);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(EntityInterface $entity) {
+    $id = $entity->id();
+
+    // Track the original ID.
+    if ($entity->getOriginalId() !== NULL) {
+      $id = $entity->getOriginalId();
+    }
+
+    // Track if this entity is new.
+    $is_new = $entity->isNew();
+    // Track if this entity exists already.
+    $id_exists = $this->has($id, $entity);
+
+    // A new entity should not already exist.
+    if ($id_exists && $is_new) {
+      throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
+    }
+
+    // Load the original entity, if any.
+    if ($id_exists && !isset($entity->original)) {
+      $entity->original = $this->loadUnchanged($id);
+    }
+
+    // Allow code to run before saving.
+    $entity->preSave($this);
+    $this->invokeHook('presave', $entity);
+
+    // Perform the save.
+    $return = $this->doSave($id, $entity);
+
+    // The entity is no longer new.
+    $entity->enforceIsNew(FALSE);
+
+    // Allow code to run after saving.
+    $entity->postSave($this, !$is_new);
+    $this->invokeHook($is_new ? 'insert' : 'update', $entity);
+
+    // After saving, this is now the "original entity", and subsequent saves
+    // will be updates instead of inserts, and updates must always be able to
+    // correctly identify the original entity.
+    $entity->setOriginalId($entity->id());
+
+    unset($entity->original);
+
+    return $return;
+  }
+
+  /**
+   * Performs storage-specific saving of the entity.
+   *
+   * @param int|string $id
+   *   The original entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to save.
+   *
+   * @return bool|int
+   *   If the record insert or update failed, returns FALSE. If it succeeded,
+   *   returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
+   */
+  abstract protected function doSave($id, EntityInterface $entity);
+
+  /**
+   * Determines if this entity already exists in storage.
+   *
+   * @param int|string $id
+   *   The original entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being saved.
+   *
+   * @return bool
+   */
+  abstract protected function has($id, EntityInterface $entity);
+
+  /**
+   * Performs storage-specific entity deletion.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
+   *   An array of entity objects to delete.
+   */
+  abstract protected function doDelete($entities);
+
+  /**
    * Builds an entity query.
    *
    * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorage.php b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
index d05b2c3..f59a2be 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentStorage.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
@@ -74,12 +74,12 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   /**
    * {@inheritdoc}
    */
-  protected function postLoad(array &$queried_entities) {
+  protected function mapFromStorageRecords(array $records) {
     // Prepare standard comment fields.
-    foreach ($queried_entities as &$record) {
+    foreach ($records as $record) {
       $record->name = $record->uid ? $record->registered_name : $record->name;
     }
-    parent::postLoad($queried_entities);
+    return parent::mapFromStorageRecords($records);
   }
 
   /**
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
index ea05968..60871d0 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorage.php
@@ -138,12 +138,10 @@ public function loadUpdatedCustomized(array $router_paths) {
       );
     $query_result = $query->execute();
 
-    if ($class = $this->entityType->getClass()) {
-      // We provide the necessary arguments for PDO to create objects of the
-      // specified entity class.
-      // @see \Drupal\Core\Entity\EntityInterface::__construct()
-      $query_result->setFetchMode(\PDO::FETCH_CLASS, $class, array(array(), $this->entityTypeId));
-    }
+    // We provide the necessary arguments for PDO to create objects of the
+    // specified entity class.
+    // @see \Drupal\Core\Entity\EntityInterface::__construct()
+    $query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityClass, array(array(), $this->entityTypeId));
 
     return $query_result->fetchAllAssoc($this->idKey);
   }
diff --git a/core/modules/user/lib/Drupal/user/UserStorage.php b/core/modules/user/lib/Drupal/user/UserStorage.php
index d3f4990..afd53b3 100644
--- a/core/modules/user/lib/Drupal/user/UserStorage.php
+++ b/core/modules/user/lib/Drupal/user/UserStorage.php
@@ -79,23 +79,20 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   /**
    * {@inheritdoc}
    */
-  function postLoad(array &$queried_users) {
-    foreach ($queried_users as $key => $record) {
-      $queried_users[$key]->roles = array();
+  function mapFromStorageRecords(array $records) {
+    foreach ($records as $record) {
+      $record->roles = array();
       if ($record->uid) {
-        $queried_users[$record->uid]->roles[] = DRUPAL_AUTHENTICATED_RID;
+        $record->roles[] = DRUPAL_AUTHENTICATED_RID;
       }
       else {
-        $queried_users[$record->uid]->roles[] = DRUPAL_ANONYMOUS_RID;
+        $record->roles[] = DRUPAL_ANONYMOUS_RID;
       }
     }
 
     // Add any additional roles from the database.
-    $this->addRoles($queried_users);
-
-    // Call the default postLoad() method. This will add fields and call
-    // hook_user_load().
-    parent::postLoad($queried_users);
+    $this->addRoles($records);
+    return parent::mapFromStorageRecords($records);
   }
 
   /**
@@ -129,9 +126,11 @@ public function saveRoles(UserInterface $account) {
    * {@inheritdoc}
    */
   public function addRoles(array $users) {
-    $result = $this->database->query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
-    foreach ($result as $record) {
-      $users[$record->uid]->roles[] = $record->rid;
+    if ($users) {
+      $result = $this->database->query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
+      foreach ($result as $record) {
+        $users[$record->uid]->roles[] = $record->rid;
+      }
     }
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
index 20f4a09..37019f5 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
@@ -110,6 +110,8 @@ public static function getInfo() {
 
   /**
    * {@inheritdoc}
+   *
+   * @covers ::__construct()
    */
   protected function setUp() {
     parent::setUp();
@@ -128,6 +130,10 @@ protected function setUp() {
     $this->entityType->expects($this->any())
       ->method('getConfigPrefix')
       ->will($this->returnValue('the_config_prefix'));
+    $this->entityType->expects($this->any())
+      ->method('getClass')
+      ->will($this->returnValue(get_class($this->getMockEntity())));
+
 
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
@@ -171,12 +177,9 @@ protected function setUp() {
 
   /**
    * @covers ::create()
+   * @covers ::doCreate()
    */
   public function testCreateWithPredefinedUuid() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $this->cacheBackend->expects($this->never())
       ->method('invalidateTags');
 
@@ -197,14 +200,11 @@ public function testCreateWithPredefinedUuid() {
 
   /**
    * @covers ::create()
+   * @covers ::doCreate()
    *
    * @return \Drupal\Core\Entity\EntityInterface
    */
   public function testCreate() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $this->cacheBackend->expects($this->never())
       ->method('invalidateTags');
 
@@ -227,6 +227,7 @@ public function testCreate() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *
@@ -252,7 +253,7 @@ public function testSaveInsert(EntityInterface $entity) {
         $this->entityTypeId . 's' => TRUE, // List cache tag.
       ));
 
-    $this->configFactory->expects($this->once())
+    $this->configFactory->expects($this->exactly(2))
       ->method('get')
       ->with('the_config_prefix.foo')
       ->will($this->returnValue($config_object));
@@ -285,6 +286,7 @@ public function testSaveInsert(EntityInterface $entity) {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *
@@ -315,7 +317,7 @@ public function testSaveUpdate(EntityInterface $entity) {
       ->method('loadMultiple')
       ->with(array('the_config_prefix.foo'))
       ->will($this->returnValue(array()));
-    $this->configFactory->expects($this->once())
+    $this->configFactory->expects($this->exactly(2))
       ->method('get')
       ->with('the_config_prefix.foo')
       ->will($this->returnValue($config_object));
@@ -348,6 +350,7 @@ public function testSaveUpdate(EntityInterface $entity) {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @depends testSaveInsert
    */
@@ -416,6 +419,7 @@ public function testSaveInvalid() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Entity\EntityStorageException
    */
@@ -447,6 +451,7 @@ public function testSaveDuplicate() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException
    * @expectedExceptionMessage when this UUID is already used for
@@ -482,6 +487,7 @@ public function testSaveMismatch() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    */
   public function testSaveNoMismatch() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -522,6 +528,7 @@ public function testSaveNoMismatch() {
 
   /**
    * @covers ::save()
+   * @covers ::doSave()
    *
    * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException
    * @expectedExceptionMessage when this entity already exists with UUID
@@ -573,9 +580,6 @@ public function testSaveChangedUuid() {
       ->will($this->returnValue(array('foo')));
 
     $entity = $this->getMockEntity(array('id' => 'foo'));
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($entity)));
 
     $entity->set('uuid', 'baz');
     $this->entityStorage->save($entity);
@@ -584,7 +588,8 @@ public function testSaveChangedUuid() {
   /**
    * @covers ::load()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoad() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -605,10 +610,6 @@ public function testLoad() {
       ->method('getImplementations')
       ->will($this->returnValue(array()));
 
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entity = $this->entityStorage->load('foo');
     $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
     $this->assertSame('foo', $entity->id());
@@ -617,7 +618,8 @@ public function testLoad() {
   /**
    * @covers ::loadMultiple()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoadMultipleAll() {
     $foo_config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -647,10 +649,6 @@ public function testLoadMultipleAll() {
       ->method('getImplementations')
       ->will($this->returnValue(array()));
 
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entities = $this->entityStorage->loadMultiple();
     $expected['foo'] = 'foo';
     $expected['bar'] = 'bar';
@@ -664,7 +662,8 @@ public function testLoadMultipleAll() {
   /**
    * @covers ::loadMultiple()
    * @covers ::postLoad()
-   * @covers ::buildQuery()
+   * @covers ::mapFromStorageRecords()
+   * @covers ::doLoadMultiple()
    */
   public function testLoadMultipleIds() {
     $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
@@ -684,9 +683,6 @@ public function testLoadMultipleIds() {
     $this->moduleHandler->expects($this->exactly(2))
       ->method('getImplementations')
       ->will($this->returnValue(array()));
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
 
     $entities = $this->entityStorage->loadMultiple(array('foo'));
     foreach ($entities as $id => $entity) {
@@ -714,12 +710,9 @@ public function testDeleteRevision() {
 
   /**
    * @covers ::delete()
+   * @covers ::doDelete()
    */
   public function testDelete() {
-    $this->entityType->expects($this->atLeastOnce())
-      ->method('getClass')
-      ->will($this->returnValue(get_class($this->getMockEntity())));
-
     $entities = array();
     $configs = array();
     $config_map = array();
@@ -776,6 +769,7 @@ public function testDelete() {
 
   /**
    * @covers ::delete()
+   * @covers ::doDelete()
    */
   public function testDeleteNothing() {
     $this->moduleHandler->expects($this->never())
