diff --git a/core/core.services.yml b/core/core.services.yml
index f8c428f..f76c2b6 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -281,10 +281,18 @@ services:
     arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@router.builder']
   entity.manager:
     class: Drupal\Core\Entity\EntityManager
-    arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager']
+    arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager', '@entity.definitions.installed']
     parent: container.trait
     tags:
       - { name: plugin_manager_cache_clear }
+  entity.definitions.installed:
+      class: Drupal\Core\KeyValueStore\KeyValueStoreInterface
+      factory_method: get
+      factory_service: keyvalue
+      arguments: ['entity.definitions.installed']
+  entity.definition_update_manager:
+    class: Drupal\Core\Entity\EntityDefinitionUpdateManager
+    arguments: ['@entity.manager']
   entity.form_builder:
     class: Drupal\Core\Entity\EntityFormBuilder
     arguments: ['@entity.manager', '@form_builder']
diff --git a/core/includes/update.inc b/core/includes/update.inc
index f9a88db..125907d 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -10,14 +10,8 @@
 
 use Drupal\Component\Graph\Graph;
 use Drupal\Component\Utility\String;
-use Drupal\Core\Config\FileStorage;
-use Drupal\Core\Config\ConfigException;
-use Drupal\Core\DrupalKernel;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Utility\Error;
-use Drupal\Component\Uuid\Uuid;
-use Drupal\Component\Utility\NestedArray;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Disables any extensions that are incompatible with the current core version.
@@ -258,6 +252,33 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
+ * Performs entity definition updates, which can trigger schema updates.
+ *
+ * @param $module
+ *   The module whose update will be run.
+ * @param $number
+ *   The update number to run.
+ * @param $context
+ *   The batch context array.
+ */
+function update_entity_definitions($module, $number, &$context) {
+  try {
+    \Drupal::service('entity.definition_update_manager')->applyUpdates();
+  }
+  catch (EntityStorageException $e) {
+    watchdog_exception('update', $e);
+    $variables = Error::decodeException($e);
+    unset($variables['backtrace']);
+    // The exception message is run through
+    // \Drupal\Component\Utility\String::checkPlain() by
+    // \Drupal\Core\Utility\Error::decodeException().
+    $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
+    $context['results'][$module][$number] = $ret;
+    $context['results']['#abort'][] = 'update_entity_definitions';
+  }
+}
+
+/**
  * Returns a list of all the pending database updates.
  *
  * @return
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
new file mode 100644
index 0000000..0118039
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -0,0 +1,231 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\EntityDefinitionUpdateManager.
+ */
+
+namespace Drupal\Core\Entity;
+
+use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
+use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Manages entity definition updates.
+ */
+class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * Indicates that a definition has just been created.
+   *
+   * @var int
+   */
+  const DEFINITION_CREATED = 1;
+
+  /**
+   * Indicates that a definition has changes.
+   *
+   * @var int
+   */
+  const DEFINITION_UPDATED = 2;
+
+  /**
+   * Indicates that a definition has just been deleted.
+   *
+   * @var int
+   */
+  const DEFINITION_DELETED = 3;
+
+  /**
+   * The entity manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a new EntityDefinitionUpdateManager.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->entityManager = $entity_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function needsUpdates() {
+    return (bool) $this->getChangeList();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeSummary() {
+    $summary = array();
+
+    foreach ($this->getChangeList() as $entity_type_id => $change_list) {
+      // Process entity type definition changes.
+      if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+        $entity_type = $this->entityManager->getDefinition($entity_type_id);
+        $summary[$entity_type_id][] = $this->t('Update the %entity_type entity type.', array('%entity_type' => $entity_type->getLabel()));
+      }
+
+      // Process field storage definition changes.
+      if (!empty($change_list['field_storage_definitions'])) {
+        $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+        $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+
+        foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+          switch ($change) {
+            case static::DEFINITION_CREATED:
+              $summary[$entity_type_id][] = $this->t('Create the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel()));
+              break;
+
+            case static::DEFINITION_UPDATED:
+              $summary[$entity_type_id][] = $this->t('Update the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel()));
+              break;
+
+            case static::DEFINITION_DELETED:
+              $summary[$entity_type_id][] = $this->t('Delete the %field_name field.', array('%field_name' => $original_storage_definitions[$field_name]->getLabel()));
+              break;
+          }
+        }
+      }
+    }
+
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyUpdates() {
+    foreach ($this->getChangeList() as $entity_type_id => $change_list) {
+      // Process entity type definition changes.
+      if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+        $entity_type = $this->entityManager->getDefinition($entity_type_id);
+        $original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+        $this->entityManager->onEntityTypeUpdate($entity_type, $original);
+      }
+
+      // Process field storage definition changes.
+      if (!empty($change_list['field_storage_definitions'])) {
+        $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+        $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+
+        foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+          switch ($change) {
+            case static::DEFINITION_CREATED:
+              $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions[$field_name]);
+              break;
+
+            case static::DEFINITION_UPDATED:
+              $this->entityManager->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]);
+              break;
+
+            case static::DEFINITION_DELETED:
+              $this->entityManager->onFieldStorageDefinitionDelete($storage_definitions[$field_name]);
+              break;
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns a list of changes to entity type and field storage definitions.
+   *
+   * @return array
+   *   An associative array keyed by entity type id of change descriptors. Every
+   *   entry is an associative array with the following optional keys:
+   *   - entity_type: a scalar having only the DEFINITION_UPDATED value.
+   *   - field_storage_definitions: an associative array keyed by field name of
+   *     scalars having one value among:
+   *     - DEFINITION_CREATED
+   *     - DEFINITION_UPDATED
+   *     - DEFINITION_DELETED
+   */
+  protected function getChangeList() {
+    $this->entityManager->clearCachedDefinitions();
+    $change_list = array();
+
+    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
+      $original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+      // @todo Support non-storage-schema-changing definition updates too:
+      //   https://www.drupal.org/node/2336895.
+      if ($this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
+        $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED;
+      }
+
+      if ($entity_type->isFieldable()) {
+        $field_changes = array();
+        $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+        $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+
+        // Detect created field storage definitions.
+        foreach (array_diff_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) {
+          $field_changes[$field_name] = static::DEFINITION_CREATED;
+        }
+
+        // Detect deleted field storage definitions.
+        foreach (array_diff_key($original_storage_definitions, $storage_definitions) as $field_name => $original_storage_definition) {
+          $field_changes[$field_name] = static::DEFINITION_DELETED;
+        }
+
+        // Detect updated field storage definitions.
+        foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) {
+          // @todo Support non-storage-schema-changing definition updates too:
+          //   https://www.drupal.org/node/2336895.
+          if ($this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) {
+            $field_changes[$field_name] = static::DEFINITION_UPDATED;
+          }
+        }
+
+        if ($field_changes) {
+          $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes;
+        }
+      }
+    }
+
+    return array_filter($change_list);
+  }
+
+  /**
+   * Checks if the changes to the entity type requires storage schema changes.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if storage schema changes are required, FALSE otherwise.
+   */
+  protected function requiresEntityStorageSchemaChanges($entity_type, $original) {
+    $storage = $this->entityManager->getStorage($entity_type->id());
+    return ($storage instanceof EntityStorageSchemaInterface) && $storage->requiresEntityStorageSchemaChanges($entity_type, $original);
+  }
+
+  /**
+   * Checks if the changes to the storage definition requires schema changes.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if storage schema changes are required, FALSE otherwise.
+   */
+  protected function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $storage = $this->entityManager->getStorage($storage_definition->getTargetEntityTypeId());
+    return ($storage instanceof FieldableEntityStorageSchemaInterface) && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
new file mode 100644
index 0000000..ccbe5f2
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface.
+ */
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Defines an interface for managing entity definition updates.
+ *
+ * During the application lifetime, the definitions of various entity types and
+ * their data components (e.g., fields for fieldable entity types) can change.
+ * For example, updated code can be deployed. Some entity handlers may need to
+ * perform complex or long-running logic in response to the change. For
+ * example, a SQL-based storage handler may need to update the database schema.
+ *
+ * The entity manager tracks both the current code-defined definitions and the
+ * most recent definitions that entity handlers have had a chance to react to,
+ * and implements \Drupal\Core\Entity\EntityTypeListenerInterface and
+ * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface for bringing the
+ * latter up to date with the former, but makes no decisions about when to
+ * invoke those events. This interface is for managing that.
+ *
+ * @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
+ * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
+ * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
+ * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
+ * @see \Drupal\Core\Entity\EntityTypeListenerInterface
+ * @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
+ */
+interface EntityDefinitionUpdateManagerInterface {
+
+  /**
+   * Checks if there are any definition updates that need to be applied.
+   *
+   * @return bool
+   *   TRUE if updates are needed.
+   */
+  public function needsUpdates();
+
+  /**
+   * Returns a human readable summary of the detected changes.
+   *
+   * @return array
+   *   An associative array keyed by entity type id. Each entry is an array of
+   *   human-readable strings, each describing a change.
+   */
+  public function getChangeSummary();
+
+  /**
+   * Applies all the detected valid changes.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   This exception is thrown if a change cannot be applied without
+   *   unacceptable data loss. In such a case, the site administrator needs to
+   *   apply some other process, such as a custom update function or a
+   *   migration via the Migrate module.
+   */
+  public function applyUpdates();
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index 2d3fa7a..a2aedf3 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -17,9 +17,12 @@
 use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
 use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
@@ -113,6 +116,13 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
   protected $languageManager;
 
   /**
+   * The keyvalue collection for tracking installed definitions.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $installedDefinitions;
+
+  /**
    * Static cache of bundle information.
    *
    * @var array
@@ -169,8 +179,12 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
    *   The string translationManager.
    * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
    *   The class resolver.
+   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
+   *   The typed data manager.
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $installed_definitions
+   *   The keyvalue collection for tracking installed definitions.
    */
-  public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager) {
+  public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager, KeyValueStoreInterface $installed_definitions) {
     parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface', 'Drupal\Core\Entity\Annotation\EntityType');
 
     $this->setCacheBackend($cache, 'entity_type', array('entity_types' => TRUE));
@@ -180,6 +194,7 @@ public function __construct(\Traversable $namespaces, ModuleHandlerInterface $mo
     $this->translationManager = $translation_manager;
     $this->classResolver = $class_resolver;
     $this->typedDataManager = $typed_data_manager;
+    $this->installedDefinitions = $installed_definitions;
   }
 
   /**
@@ -190,6 +205,7 @@ public function clearCachedDefinitions() {
     $this->clearCachedBundles();
     $this->clearCachedFieldDefinitions();
     $this->classNameEntityTypeMap = array();
+    $this->handlers = array();
   }
 
   /**
@@ -961,36 +977,102 @@ public function getEntityTypeFromClass($class_name) {
    * {@inheritdoc}
    */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+
     // @todo Forward this to all interested handlers, not only storage, once
     //   iterating handlers is possible: https://www.drupal.org/node/2332857.
-    $storage = $this->getStorage($entity_type->id());
+    $storage = $this->getStorage($entity_type_id);
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeCreate($entity_type);
     }
+
+    $this->setLastInstalledDefinition($entity_type);
+    if ($entity_type->isFieldable()) {
+      $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id));
+    }
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $entity_type_id = $entity_type->id();
+
     // @todo Forward this to all interested handlers, not only storage, once
     //   iterating handlers is possible: https://www.drupal.org/node/2332857.
-    $storage = $this->getStorage($entity_type->id());
+    $storage = $this->getStorage($entity_type_id);
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeUpdate($entity_type, $original);
     }
+
+    $this->setLastInstalledDefinition($entity_type);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+
     // @todo Forward this to all interested handlers, not only storage, once
     //   iterating handlers is possible: https://www.drupal.org/node/2332857.
-    $storage = $this->getStorage($entity_type->id());
+    $storage = $this->getStorage($entity_type_id);
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeDelete($entity_type);
     }
+
+    $this->deleteLastInstalledDefinition($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($entity_type_id);
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionCreate($storage_definition);
+    }
+
+    $this->setLastInstalledFieldStorageDefinition($storage_definition);
+    $this->clearCachedFieldDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($entity_type_id);
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionUpdate($storage_definition, $original);
+    }
+
+    $this->setLastInstalledFieldStorageDefinition($storage_definition);
+    $this->clearCachedFieldDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($entity_type_id);
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionDelete($storage_definition);
+    }
+
+    $this->deleteLastInstalledFieldStorageDefinition($storage_definition);
+    $this->clearCachedFieldDefinitions();
   }
 
   /**
@@ -1047,4 +1129,81 @@ public function onBundleDelete($entity_type_id, $bundle) {
     $this->clearCachedFieldDefinitions();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastInstalledDefinition($entity_type_id) {
+    return $this->installedDefinitions->get($entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Stores the entity type definition in the application state.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  protected function setLastInstalledDefinition(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $this->installedDefinitions->set($entity_type_id . '.entity_type', $entity_type);
+  }
+
+  /**
+   * Deletes the entity type definition from the application state.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteLastInstalledDefinition($entity_type_id) {
+    $this->installedDefinitions->delete($entity_type_id . '.entity_type');
+    // Clean up field storage definitions as well. Even if the entity type
+    // isn't currently fieldable, there might be legacy definitions or an
+    // empty array stored from when it was.
+    $this->installedDefinitions->delete($entity_type_id . '.field_storage_definitions');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
+    return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions');
+  }
+
+  /**
+   * Stores the entity type's field storage definitions in the application state.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+   *   An array of field storage definitions.
+   */
+  protected function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
+    $this->installedDefinitions->set($entity_type_id . '.field_storage_definitions', $storage_definitions);
+  }
+
+  /**
+   * Stores the field storage definition in the application state.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   */
+  protected function setLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id);
+    $definitions[$storage_definition->getName()] = $storage_definition;
+    $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * Deletes the field storage definition from the application state.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   */
+  protected function deleteLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id);
+    unset($definitions[$storage_definition->getName()]);
+    $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
index 8d3f9f4..7a13fff 100644
--- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
@@ -8,11 +8,12 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 
 /**
  * Provides an interface for entity type managers.
  */
-interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface {
+interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
 
   /**
    * Builds a list of entity type labels suitable for a Form API options list.
@@ -81,6 +82,41 @@ public function getFieldDefinitions($entity_type_id, $bundle);
   public function getFieldStorageDefinitions($entity_type_id);
 
   /**
+   * Gets the entity type's most recently installed field storage definitions.
+   *
+   * During the application lifetime, field storage definitions can change. For
+   * example, updated code can be deployed. The getFieldStorageDefinitions()
+   * method will always return the definitions as determined by the current
+   * codebase. This method, however, returns what the definitions were when the
+   * last time that one of the
+   * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface events was last
+   * fired and completed successfully. In other words, the definitions that
+   * the entity type's handlers have incorporated into the application state.
+   * For example, if the entity type's storage handler is SQL-based, the
+   * definitions for which database tables were created.
+   *
+   * Application management code can check if getFieldStorageDefinitions()
+   * differs from getLastInstalledFieldStorageDefinitions() and decide whether
+   * to:
+   * - Invoke the appropriate
+   *   \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
+   *   events so that handlers react to the new definitions.
+   * - Raise a warning that the application state is incompatible with the
+   *   codebase.
+   * - Perform some other action.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   *   The array of installed field storage definitions for the entity type,
+   *   keyed by field name.
+   *
+   * @see \Drupal\Core\Entity\EntityTypeListenerInterface
+   */
+  public function getLastInstalledFieldStorageDefinitions($entity_type_id);
+
+  /**
    * Returns a lightweight map of fields across bundles.
    *
    * @return array
@@ -286,6 +322,38 @@ public function getTranslationFromContext(EntityInterface $entity, $langcode = N
   public function getDefinition($entity_type_id, $exception_on_invalid = TRUE);
 
   /**
+   * Returns the entity type definition in its most recently installed state.
+   *
+   * During the application lifetime, entity type definitions can change. For
+   * example, updated code can be deployed. The getDefinition() method will
+   * always return the definition as determined by the current codebase. This
+   * method, however, returns what the definition was when the last time that
+   * one of the \Drupal\Core\Entity\EntityTypeListenerInterface events was last
+   * fired and completed successfully. In other words, the definition that
+   * the entity type's handlers have incorporated into the application state.
+   * For example, if the entity type's storage handler is SQL-based, the
+   * definition for which database tables were created.
+   *
+   * Application management code can check if getDefinition() differs from
+   * getLastInstalledDefinition() and decide whether to:
+   * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface
+   *   event so that handlers react to the new definition.
+   * - Raise a warning that the application state is incompatible with the
+   *   codebase.
+   * - Perform some other action.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface|null
+   *   The installed entity type definition, or NULL if the entity type has
+   *   not yet been installed via onEntityTypeCreate().
+   *
+   * @see \Drupal\Core\Entity\EntityTypeListenerInterface
+   */
+  public function getLastInstalledDefinition($entity_type_id);
+
+  /**
    * {@inheritdoc}
    *
    * @return \Drupal\Core\Entity\EntityTypeInterface[]
diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
index 54e2bff..0149ec3 100644
--- a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
+++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Core\Entity\Exception;
 
+use Drupal\Core\Entity\EntityStorageException;
+
 /**
  * Exception thrown when a storage definition update is forbidden.
  */
-class FieldStorageDefinitionUpdateForbiddenException extends \Exception { }
+class FieldStorageDefinitionUpdateForbiddenException extends EntityStorageException {
+}
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
index 2d695c3..aaab687 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
@@ -9,42 +9,9 @@
 
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 
-interface FieldableEntityStorageInterface extends EntityStorageInterface {
-
-  /**
-   * Reacts to the creation of a field storage definition.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The definition being created.
-   */
-  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition);
-
-  /**
-   * Reacts to the update of a field storage definition.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field being updated.
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
-   *   The original storage definition; i.e., the definition before the update.
-   *
-   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
-   *   Thrown when the update to the field is forbidden.
-   */
-  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
-
-  /**
-   * Reacts to the deletion of a field storage definition.
-   *
-   * Stored values should not be wiped at once, but marked as 'deleted' so that
-   * they can go through a proper purge process later on.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field being deleted.
-   *
-   * @see purgeFieldData()
-   */
-  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition);
+interface FieldableEntityStorageInterface extends EntityStorageInterface, FieldStorageDefinitionListenerInterface {
 
   /**
    * Reacts to the creation of a field.
@@ -59,7 +26,7 @@ public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definiti
    *
    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
    *   The field definition being updated.
-   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $original
    *   The original field definition; i.e., the definition before the update.
    */
   public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original);
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
deleted file mode 100644
index 51001bd..0000000
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface.
- */
-
-namespace Drupal\Core\Entity\Schema;
-use Drupal\Core\Entity\EntityTypeListenerInterface;
-
-/**
- * Defines an interface for handling the storage schema of entities.
- */
-interface EntitySchemaHandlerInterface extends EntityTypeListenerInterface {
-}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php
new file mode 100644
index 0000000..c3b20ea
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeListenerInterface;
+
+/**
+ * Defines the interface for entity storage schema handler classes.
+ *
+ * An entity type's storage schema handler is responsible for creating the
+ * storage backend's schema that the entity type's storage handler needs for
+ * storing its entities. For example, if the storage handler is for a SQL
+ * backend, then the storage schema handler is responsible for creating the
+ * needed tables. During the application lifetime, an entity type's definition
+ * can change in a way that requires changes to the storage schema, so this
+ * interface defines methods for that as well.
+ *
+ * @see \Drupal\Core\Entity\EntityStorageInterface
+ */
+interface EntityStorageSchemaInterface extends EntityTypeListenerInterface {
+
+  /**
+   * Checks if the changes to the entity type requires storage schema changes.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if storage schema changes are required, FALSE otherwise.
+   */
+  public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original);
+
+  /**
+   * Checks if existing data would be lost if the schema changes were applied.
+   *
+   * If there are no schema changes needed, then no data needs to be migrated,
+   * but it is not the responsibility of this function to recheck what
+   * requiresEntityStorageSchemaChanges() checks. Rather, the meaning of what
+   * this function returns when requiresEntityStorageSchemaChanges() returns
+   * FALSE is undefined. Callers are expected to only call this function when
+   * requiresEntityStorageSchemaChanges() is TRUE.
+   *
+   * This function can return FALSE if any of these conditions apply:
+   * - There are no existing entities for the entity type.
+   * - There are existing entities, but the schema changes can be applied
+   *   without losing their data (e.g., if the schema changes can be performed
+   *   by altering tables rather than dropping and recreating them).
+   * - The only entity data that would be lost are ones that are not valid for
+   *   the new definition (e.g., if changing an entity type from revisionable
+   *   to non-revisionable, then it's okay to drop data for the non-default
+   *   revision).
+   *
+   * When this function returns FALSE, site administrators will be unable to
+   * perform an automated update, and will instead need to perform a site
+   * migration or invoke some custom update process.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if data migration is required, FALSE otherwise.
+   *
+   * @see self::requiresEntityStorageSchemaChanges()
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php
new file mode 100644
index 0000000..759fb6e
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+/**
+ * Defines the interface for storage schema classes for fieldable entity types.
+ */
+interface FieldableEntityStorageSchemaInterface extends EntityStorageSchemaInterface, FieldStorageDefinitionListenerInterface {
+
+  /**
+   * Checks if the changes to the storage definition requires schema changes.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if storage schema changes are required, FALSE otherwise.
+   */
+  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Checks if existing data would be lost if the schema changes were applied.
+   *
+   * If there are no schema changes needed, then no data needs to be migrated,
+   * but it is not the responsibility of this function to recheck what
+   * requiresFieldStorageSchemaChanges() checks. Rather, the meaning of what
+   * this function returns when requiresFieldStorageSchemaChanges() returns
+   * FALSE is undefined. Callers are expected to only call this function when
+   * requiresFieldStorageSchemaChanges() is TRUE.
+   *
+   * This function can return FALSE if any of these conditions apply:
+   * - There are no existing entities for the entity type to which this field
+   *   is attached.
+   * - There are existing entities, but none with existing values for this
+   *   field.
+   * - There are existing field values, but the schema changes can be applied
+   *   without losing them (e.g., if the schema changes can be performed by
+   *   altering tables rather than dropping and recreating them).
+   * - The only field values that would be lost are ones that are not valid for
+   *   the new definition (e.g., if changing a field from revisionable to
+   *   non-revisionable, then it's okay to drop data for the non-default
+   *   revision).
+   *
+   * When this function returns FALSE, site administrators will be unable to
+   * perform an automated update, and will instead need to perform a site
+   * migration or invoke some custom update process.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if data migration is required, FALSE otherwise.
+   *
+   * @see self::requiresFieldStorageSchemaChanges()
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 1814564..a46df01 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -17,9 +17,9 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\EntityTypeListenerInterface;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -39,7 +39,7 @@
  *
  * @ingroup entity_api
  */
-class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, EntityTypeListenerInterface {
+class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, FieldableEntityStorageSchemaInterface {
 
   /**
    * The mapping of field columns to SQL tables.
@@ -109,7 +109,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
   /**
    * The entity schema handler.
    *
-   * @var \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface
+   * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
    */
   protected $schemaHandler;
 
@@ -269,11 +269,7 @@ public function getTableMapping() {
       ), $all_fields);
 
       $revisionable = $this->entityType->isRevisionable();
-      // @todo Remove the data table check once all entity types are using
-      // entity query and we have a views data handler. See:
-      // - https://drupal.org/node/2068325
-      // - https://drupal.org/node/1740492
-      $translatable = $this->entityType->getDataTable() && $this->entityType->isTranslatable();
+      $translatable = $this->entityType->isTranslatable();
       if (!$revisionable && !$translatable) {
         // The base layout stores all the base field values in the base table.
         $this->tableMapping->setFieldNames($this->baseTable, $all_fields);
@@ -1366,6 +1362,34 @@ protected function usesDedicatedTable(FieldStorageDefinitionInterface $definitio
   /**
    * {@inheritdoc}
    */
+  public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntityStorageSchemaChanges($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldStorageSchemaChanges($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
     $this->schemaHandler()->onEntityTypeCreate($entity_type);
   }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index a8034e8..ca61630 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -7,16 +7,19 @@
 
 namespace Drupal\Core\Entity\Sql;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface;
+use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
  */
-class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
+class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInterface {
 
   /**
    * The entity type this schema builder is responsible for.
@@ -75,7 +78,69 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit
   /**
    * {@inheritdoc}
    */
+  public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return
+      $entity_type->getStorageClass() != $original->getStorageClass() ||
+      $entity_type->getKeys() != $original->getKeys() ||
+      $entity_type->isRevisionable() != $original->isRevisionable() ||
+      $entity_type->isTranslatable() != $original->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return
+      $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
+      $storage_definition->getSchema() != $original->getSchema() ||
+      $storage_definition->isRevisionable() != $original->isRevisionable() ||
+      $storage_definition->isTranslatable() != $original->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    // If we're updating from NULL storage, then there's no stored data that
+    // requires migration.
+    // @todo Remove in https://www.drupal.org/node/2335879.
+    $original_storage_class = $original->getStorageClass();
+    $null_storage_class = 'Drupal\Core\Entity\ContentEntityNullStorage';
+    if ($original_storage_class == $null_storage_class || is_subclass_of($original_storage_class, $null_storage_class)) {
+      return FALSE;
+    }
+
+    return
+      // If the original storage class is different, then there might be
+      // existing entities in that storage even if the new storage's base
+      // table is empty.
+      // @todo Ask the old storage handler rather than assuming:
+      //   https://www.drupal.org/node/2335879.
+      $entity_type->getStorageClass() != $original_storage_class ||
+      !$this->tableIsEmpty($this->storage->getBaseTable());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // If the base table is empty, there are no entities, and therefore, no
+    // field data that we care about preserving.
+    // @todo We might be returning TRUE here in cases where it would be safe
+    //   to return FALSE (for example, if the field is in a dedicated table
+    //   and that table is empty), and thereby preventing automatic updates
+    //   that should be possible, but determining that requires refactoring
+    //   SqlContentEntityStorage::_fieldSqlSchema(), and in the meantime,
+    //   it's safer to return false positives than false negatives:
+    //   https://www.drupal.org/node/1498720.
+    return !$this->tableIsEmpty($this->storage->getBaseTable());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
     $schema = $this->getEntitySchema($entity_type, TRUE);
     foreach ($schema as $table_name => $table_schema) {
@@ -89,11 +154,39 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
    * {@inheritdoc}
    */
   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
-    // @todo Implement proper updates: https://www.drupal.org/node/1498720.
-    //   Meanwhile, treat a change from non-SQL storage to SQL storage as
-    //   identical to creation with respect to SQL schema handling.
-    if (!is_subclass_of($original->getStorageClass(), '\Drupal\Core\Entity\Sql\SqlEntityStorageInterface')) {
-      $this->onEntityTypeCreate($entity_type);
+    $this->checkEntityType($entity_type);
+    $this->checkEntityType($original);
+
+    // If no schema changes are needed, we don't need to do anything.
+    if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
+      return;
+    }
+
+    // If we have no data just recreate the entity schema from scratch.
+    if (!$this->requiresEntityDataMigration($entity_type, $original)) {
+      if ($this->database->supportsTransactionalDDL()) {
+        // If the database supports transactional DDL, we can go ahead and rely
+        // on it. If not, we will have to rollback manually if something fails.
+        $transaction = $this->database->startTransaction();
+      }
+      try {
+        $this->onEntityTypeDelete($original);
+        $this->onEntityTypeCreate($entity_type);
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate original schema.
+          $this->onEntityTypeCreate($original);
+        }
+        throw $e;
+      }
+    }
+    // Otherwise, throw an exception.
+    else {
+      throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.'));
     }
   }
 
@@ -101,9 +194,9 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
    * {@inheritdoc}
    */
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
-    $schema = $this->getEntitySchema($entity_type, TRUE);
-    foreach ($schema as $table_name => $table_schema) {
+    foreach ($this->getEntitySchemaTables() as $table_name) {
       if ($schema_handler->tableExists($table_name)) {
         $schema_handler->dropTable($table_name);
       }
@@ -111,6 +204,51 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionCreate()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionUpdate()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionDelete()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * Checks that we are dealing with the correct entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity type matches the current one.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  protected function checkEntityType(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() != $this->entityType->id()) {
+      throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
+    }
+    return TRUE;
+  }
+
+  /**
    * Returns the entity schema for the specified entity type.
    *
    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
@@ -128,7 +266,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
 
     if (!isset($this->schema[$entity_type_id]) || $reset) {
       // Initialize the table schema.
-      $tables = $this->getTables();
+      $tables = $this->getEntitySchemaTables();
       $schema[$tables['base_table']] = $this->initializeBaseTable();
       if (isset($tables['revision_table'])) {
         $schema[$tables['revision_table']] = $this->initializeRevisionTable();
@@ -180,7 +318,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
    * @return array
    *   A list of entity type tables, keyed by table key.
    */
-  protected function getTables() {
+  protected function getEntitySchemaTables() {
     return array_filter(array(
       'base_table' => $this->storage->getBaseTable(),
       'revision_table' => $this->storage->getRevisionTable(),
@@ -619,4 +757,24 @@ protected function getEntityIndexName($index) {
     return $this->entityType->id() . '__' . $index;
   }
 
+  /**
+   * Checks whether a database table is non-existent or empty.
+   *
+   * Empty tables can be dropped and recreated without data loss.
+   *
+   * @param string $table_name
+   *   The database table to check.
+   *
+   * @return bool
+   *   TRUE if the table is empty, FALSE otherwise.
+   */
+  protected function tableIsEmpty($table_name) {
+    return !$this->database->schema()->tableExists($table_name) ||
+      !$this->database->select($table_name)
+        ->countQuery()
+        ->range(0, 1)
+        ->execute()
+        ->fetchField();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php
new file mode 100644
index 0000000..fcea2e4
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Field\FieldStorageDefinitionListenerInterface.
+ */
+
+namespace Drupal\Core\Field;
+
+/**
+ * Defines an interface for reacting to field storage definition creation, deletion, and updates.
+ *
+ * @todo Convert to Symfony events: https://www.drupal.org/node/2332935
+ */
+interface FieldStorageDefinitionListenerInterface {
+
+  /**
+   * Reacts to the creation of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The definition being created.
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Reacts to the update of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Reacts to the deletion of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field being deleted.
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition);
+
+}
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index f6953b3..128ac01 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -290,8 +290,8 @@ protected function preSaveNew(EntityStorageInterface $storage) {
     // definition is passed to the various hooks and written to config.
      $this->settings += $field_type_manager->getDefaultSettings($this->type);
 
-    // Notify the entity storage.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this);
+    // Notify the entity manager.
+    $entity_manager->onFieldStorageDefinitionCreate($this);
   }
 
   /**
@@ -334,19 +334,16 @@ protected function preSaveUpdated(EntityStorageInterface $storage) {
     // invokes hook_field_storage_config_update_forbid().
     $module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original));
 
-    // Notify the storage. The controller can reject the definition
+    // Notify the entity manager. A listener can reject the definition
     // update as invalid by raising an exception, which stops execution before
     // the definition is written to config.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original);
+    $entity_manager->onFieldStorageDefinitionUpdate($this, $this->original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
-    // Clear the cache.
-    \Drupal::entityManager()->clearCachedFieldDefinitions();
-
     if ($update) {
       // Invalidate the render cache for all affected entities.
       $entity_manager = \Drupal::entityManager();
@@ -406,13 +403,10 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
     // Notify the storage.
     foreach ($fields as $field) {
       if (!$field->deleted) {
-        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field);
+        \Drupal::entityManager()->onFieldStorageDefinitionDelete($field);
         $field->deleted = TRUE;
       }
     }
-
-    // Clear the cache.
-    \Drupal::entityManager()->clearCachedFieldDefinitions();
   }
 
   /**
diff --git a/core/modules/field/tests/modules/field_test/field_test.entity.inc b/core/modules/field/tests/modules/field_test/field_test.entity.inc
index 0c9bf26..d5614ac 100644
--- a/core/modules/field/tests/modules/field_test/field_test.entity.inc
+++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc
@@ -18,11 +18,17 @@ function field_test_entity_type_alter(array &$entity_types) {
 /**
  * Helper function to enable entity translations.
  */
-function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
+function field_test_entity_info_translatable($entity_type_id = NULL, $translatable = NULL) {
   $stored_value = &drupal_static(__FUNCTION__, array());
-  if (isset($entity_type)) {
-    $stored_value[$entity_type] = $translatable;
-    \Drupal::entityManager()->clearCachedDefinitions();
+  if (isset($entity_type_id)) {
+    $entity_manager = \Drupal::entityManager();
+    $original = $entity_manager->getDefinition($entity_type_id);
+    $stored_value[$entity_type_id] = $translatable;
+    if ($translatable != $original->isTranslatable()) {
+      $entity_manager->clearCachedDefinitions();
+      $entity_type = $entity_manager->getDefinition($entity_type_id);
+      $entity_manager->onEntityTypeUpdate($entity_type, $original);
+    }
   }
   return $stored_value;
 }
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index 62752ae..fc0c6b7 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -285,6 +285,23 @@ protected function selection() {
       drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
     }
 
+    // If there are entity definition updates, display their summary.
+    if (\Drupal::service('entity.definition_update_manager')->needsUpdates()) {
+      $entity_build = array();
+      $summary = \Drupal::service('entity.definition_update_manager')->getChangeSummary();
+      foreach ($summary as $entity_type_id => $items) {
+        $entity_update_key = 'entity_type_updates_' . $entity_type_id;
+        $entity_build[$entity_update_key] = array(
+          '#theme' => 'item_list',
+          '#items' => $items,
+          '#title' => $entity_type_id . ' entity type',
+        );
+        $count++;
+      }
+      // Display these above the module updates, since they will be run first.
+      $build['start'] = $entity_build + $build['start'];
+    }
+
     if (empty($count)) {
       drupal_set_message($this->t('No pending updates.'));
       unset($build);
@@ -501,9 +518,18 @@ protected function triggerBatch(Request $request) {
       $this->state->set('system.maintenance_mode', TRUE);
     }
 
-    $start = $this->getModuleUpdates();
+    $operations = array();
+
+    // First of all perform entity definition updates, which will update
+    // storage schema if needed, so that module update functions work with
+    // the correct entity schema.
+    if (\Drupal::service('entity.definition_update_manager')->needsUpdates()) {
+      $operations[] = array('update_entity_definitions', array('system', '0 - Update entity definitions'));
+    }
+
     // Resolve any update dependencies to determine the actual updates that will
     // be run and the order they will be run in.
+    $start = $this->getModuleUpdates();
     $updates = update_resolve_dependencies($start);
 
     // Store the dependencies for each update function in an array which the
@@ -515,7 +541,7 @@ protected function triggerBatch(Request $request) {
       $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
     }
 
-    $operations = array();
+    // Determine updates to be performed.
     foreach ($updates as $update) {
       if ($update['allowed']) {
         // Set the installed version of each module so updates will start at the
diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
new file mode 100644
index 0000000..655aadb
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system\Tests\Entity\EntityDefinitionUpdateTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\Core\Entity\EntityStorageException;
+
+/**
+ * Tests EntityDefinitionUpdateManager functionality.
+ *
+ * @group Entity
+ */
+class EntityDefinitionUpdateTest extends EntityUnitTestBase {
+
+  /**
+   * The entity definition update manager.
+   *
+   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+   */
+  protected $entityDefinitionUpdateManager;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
+    $this->database = $this->container->get('database');
+  }
+
+  /**
+   * Tests when no definition update is needed.
+   */
+  public function testNoUpdates() {
+    // Install every entity type's schema.
+    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
+      $this->installEntitySchema($entity_type_id);
+    }
+
+    // Ensure that the definition update manager reports no updates.
+    $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), array(), 'EntityDefinitionUpdateManager reports an empty change summary.');
+
+    // Ensure that applyUpdates() runs without error (it's not expected to do
+    // anything when there aren't updates).
+    $this->entityDefinitionUpdateManager->applyUpdates();
+  }
+
+  /**
+   * Tests updating entity schema when there are no existing entities.
+   */
+  public function testUpdateWithoutData() {
+    // Install every entity type's schema. Start with entity_test_rev not
+    // supporting revisions, and ensure its revision table isn't created.
+    $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE);
+    $this->entityManager->clearCachedDefinitions();
+    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
+      $this->installEntitySchema($entity_type_id);
+    }
+    $this->assertFalse($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table not created for entity_test_rev.');
+
+    // Restore entity_test_rev back to supporting revisions and ensure the
+    // definition update manager reports that an update is needed.
+    $this->state->delete('entity_test.entity_test_rev.disable_revisable');
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_rev' => array(
+        t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_rev')->getLabel())),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+    // Run the update and ensure the revision table is created.
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table created for entity_test_rev.');
+  }
+
+  /**
+   * Tests updating entity schema when there are existing entities.
+   */
+  public function testUpdateWithData() {
+    // Install every entity type's schema. Start with entity_test_rev not
+    // supporting revisions.
+    $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE);
+    $this->entityManager->clearCachedDefinitions();
+    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
+      $this->installEntitySchema($entity_type_id);
+    }
+
+    // Save an entity.
+    $this->entityManager->getStorage('entity_test_rev')->create()->save();
+
+    // Restore entity_test_rev back to supporting revisions and try to apply
+    // the update. It's expected to throw an exception.
+    $this->state->delete('entity_test.entity_test_rev.disable_revisable');
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('EntityStorageException thrown when trying to apply an update that requires data migration.');
+    }
+    catch (EntityStorageException $e) {
+      $this->pass('EntityStorageException thrown when trying to apply an update that requires data migration.');
+    }
+  }
+
+}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8e7b534..b9c1b83 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -452,6 +452,11 @@ function system_requirements($phase) {
         }
       }
     }
+    if (!isset($requirements['update']['severity']) && \Drupal::service('entity.definition_update_manager')->needsUpdates()) {
+      $requirements['update']['severity'] = REQUIREMENT_ERROR;
+      $requirements['update']['value'] = t('Out of date');
+      $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => base_path() . 'update.php'));
+    }
   }
 
   // Verify the update.php access setting
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index df110c2..b896603 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -71,6 +71,14 @@ function entity_test_entity_type_alter(array &$entity_types) {
       $entity_types[$entity_type]->set('translation', $translation);
     }
   }
+
+  // Optionally allow testing an entity type definition being updated from
+  // revisable to not or vice versa.
+  if (\Drupal::state()->get('entity_test.entity_test_rev.disable_revisable')) {
+    $keys = $entity_types['entity_test_rev']->getKeys();
+    unset($keys['revision']);
+    $entity_types['entity_test_rev']->set('entity_keys', $keys);
+  }
 }
 
 /**
diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install
index 98c279f..c2b5582 100644
--- a/core/profiles/standard/standard.install
+++ b/core/profiles/standard/standard.install
@@ -14,6 +14,12 @@
  * @see system_install()
  */
 function standard_install() {
+  // Now that all modules are installed, make sure the entity storage and other
+  // handlers are up to date with the current entity and field definitions. For
+  // example, Path module adds a base field to nodes and taxonomy terms after
+  // those modules are already installed.
+  \Drupal::service('entity.definition_update_manager')->applyUpdates();
+
   // Set front page to "node".
   \Drupal::config('system.site')->set('page.front', 'node')->save();
 
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
index 9163a6d..b8c24a1 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
@@ -82,7 +82,7 @@ class EntityManagerTest extends UnitTestCase {
   /**
    * The controller resolver.
    *
-   * @var \Drupal\Core\Handler\HandlerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
    */
   protected $controllerResolver;
 
@@ -94,6 +94,13 @@ class EntityManagerTest extends UnitTestCase {
   protected $typedDataManager;
 
   /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $state;
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
@@ -128,6 +135,8 @@ protected function setUp() {
     $this->typedDataManager = $this->getMockBuilder('\Drupal\Core\TypedData\TypedDataManager')
       ->disableOriginalConstructor()
       ->getMock();
+
+    $this->state = $this->getMock('Drupal\Core\State\StateInterface');
   }
 
   /**
@@ -158,7 +167,7 @@ protected function setUpEntityManager($definitions = array()) {
       ->method('getDefinitions')
       ->will($this->returnValue($definitions));
 
-    $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cache, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager);
+    $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cache, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager, $this->state);
     $this->entityManager->setContainer($this->container);
     $this->entityManager->setDiscovery($this->discovery);
   }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
index 57aa2c3..24b90d3 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
@@ -590,10 +590,10 @@ public function testGetTableMappingTranslatable(array $entity_keys) {
     // This allows to re-use the data provider.
     $entity_keys['langcode'] = 'langcode';
 
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('isTranslatable')
       ->will($this->returnValue(TRUE));
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
     $this->entityType->expects($this->any())
@@ -650,10 +650,10 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) {
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
     $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('isTranslatable')
       ->will($this->returnValue(TRUE));
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
     $this->entityType->expects($this->any())
@@ -712,13 +712,13 @@ public function testGetTableMappingRevisionableTranslatable(array $entity_keys)
       'langcode' => 'langcode',
     );
 
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('isRevisionable')
       ->will($this->returnValue(TRUE));
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('isTranslatable')
       ->will($this->returnValue(TRUE));
-    $this->entityType->expects($this->exactly(2))
+    $this->entityType->expects($this->atLeastOnce())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
     $this->entityType->expects($this->any())
@@ -833,13 +833,13 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
       $revisionable_field_names = array('description', 'owner');
       $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
-      $this->entityType->expects($this->exactly(2))
+      $this->entityType->expects($this->atLeastOnce())
         ->method('isRevisionable')
         ->will($this->returnValue(TRUE));
-      $this->entityType->expects($this->exactly(2))
+      $this->entityType->expects($this->atLeastOnce())
         ->method('isTranslatable')
         ->will($this->returnValue(TRUE));
-      $this->entityType->expects($this->exactly(2))
+      $this->entityType->expects($this->atLeastOnce())
         ->method('getDataTable')
         ->will($this->returnValue('entity_test_field_data'));
       $this->entityType->expects($this->any())
