diff --git a/core/core.services.yml b/core/core.services.yml
index c43da8c..3bf30ab 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -285,6 +285,9 @@ services:
     parent: container.trait
     tags:
       - { name: plugin_manager_cache_clear }
+  entity.schema.manager:
+    class: Drupal\Core\Entity\Schema\ContentEntitySchemaManager
+    arguments: ['@entity.manager', '@state']
   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..c00f591 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -13,6 +13,7 @@
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Config\ConfigException;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Page\DefaultHtmlPageRenderer;
 use Drupal\Core\Utility\Error;
 use Drupal\Component\Uuid\Uuid;
@@ -258,6 +259,33 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
+ * Performs entity 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_schema($module, $number, &$context) {
+  try {
+    \Drupal::service('entity.schema.manager')->applyChanges();
+  }
+  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_schema';
+  }
+}
+
+/**
  * Returns a list of all the pending database updates.
  *
  * @return
@@ -428,15 +456,17 @@ function update_get_update_function_list($starting_updates) {
   // Go through each module and find all updates that we need (including the
   // first update that was requested and any updates that run after it).
   $update_functions = array();
-  foreach ($starting_updates as $module => $version) {
-    $update_functions[$module] = array();
-    $updates = drupal_get_schema_versions($module);
-    if ($updates !== FALSE) {
-      $max_version = max($updates);
-      if ($version <= $max_version) {
-        foreach ($updates as $update) {
-          if ($update >= $version) {
-            $update_functions[$module][$update] = $module . '_update_' . $update;
+  if ($starting_updates) {
+    foreach ($starting_updates as $module => $version) {
+      $update_functions[$module] = array();
+      $updates = drupal_get_schema_versions($module);
+      if ($updates !== FALSE) {
+        $max_version = max($updates);
+        if ($version <= $max_version) {
+          foreach ($updates as $update) {
+            if ($update >= $version) {
+              $update_functions[$module][$update] = $module . '_update_' . $update;
+            }
           }
         }
       }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index 2d45275..620c474 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface;
 use Drupal\Core\Entity\Sql\DefaultTableMapping;
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
@@ -34,7 +35,7 @@
  *
  * @ingroup entity_api
  */
-class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface {
+class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, EntityTypeListenerInterface, ContentEntitySchemaProviderInterface {
 
   /**
    * The mapping of field columns to SQL tables.
@@ -178,6 +179,19 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function hasData() {
+    // We cannot use an entity query as it relies on the entity type definition,
+    // which is not available while updating the entity schema.
+    return (bool) $this->database->select($this->baseTable)
+      ->countQuery()
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+  }
+
+  /**
    * Returns the base table name.
    *
    * @return string
@@ -218,13 +232,6 @@ public function getRevisionDataTable() {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function getSchema() {
-    return $this->schemaHandler()->getSchema();
-  }
-
-  /**
    * Gets the schema handler for this entity storage.
    *
    * @return \Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema
@@ -233,7 +240,7 @@ public function getSchema() {
   protected function schemaHandler() {
     if (!isset($this->schemaHandler)) {
       $schema_handler_class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema';
-      $this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this);
+      $this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this, $this->database);
     }
     return $this->schemaHandler;
   }
@@ -1368,11 +1375,66 @@ protected function usesDedicatedTable(FieldStorageDefinitionInterface $definitio
   /**
    * {@inheritdoc}
    */
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldSchemaChanges($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntityDataMigration($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldDataMigration($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    $this->schemaHandler()->onEntityTypeCreate($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $this->schemaHandler()->onEntityTypeUpdate($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $this->schemaHandler()->onEntityTypeDelete($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
     $schema = $this->_fieldSqlSchema($storage_definition);
     foreach ($schema as $name => $table) {
       $this->database->schema()->createTable($name, $table);
     }
+
+    // @todo Move all of the above code into the schema handler:
+    //   https://www.drupal.org/node/1498720.
+    // Even before doing that, we still forward to the schema handler so that
+    // it can notify the schema manager.
+    $this->schemaHandler()->onFieldStorageDefinitionCreate($storage_definition);
   }
 
   /**
@@ -1459,6 +1521,12 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $
         }
       }
     }
+
+    // @todo Move all of the above code into the schema handler:
+    //   https://www.drupal.org/node/1498720.
+    // Even before doing that, we still forward to the schema handler so that
+    // it can notify the schema manager.
+    $this->schemaHandler()->onFieldStorageDefinitionUpdate($storage_definition, $original);
   }
 
   /**
@@ -1480,6 +1548,12 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
     $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
     $this->database->schema()->renameTable($table, $new_table);
     $this->database->schema()->renameTable($revision_table, $revision_new_table);
+
+    // @todo Move all of the above code into the schema handler:
+    //   https://www.drupal.org/node/1498720.
+    // Even before doing that, we still forward to the schema handler so that
+    // it can notify the schema manager.
+    $this->schemaHandler()->onFieldStorageDefinitionDelete($storage_definition);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 12aeca3..5460436 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -20,6 +20,20 @@ class ContentEntityNullStorage extends ContentEntityStorageBase {
   /**
    * {@inheritdoc}
    */
+  public function hasData() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countFieldData($storage_definition, $as_bool = FALSE) {
+    return $as_bool ? FALSE : 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function loadMultiple(array $ids = NULL) {
     return array();
   }
@@ -131,11 +145,4 @@ protected function doSave($id, EntityInterface $entity) {
   protected function has($id, EntityInterface $entity) {
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function countFieldData($storage_definition, $as_bool = FALSE) {
-    return $as_bool ? FALSE : 0;
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index 26eac4e..c8bad96 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -17,6 +17,8 @@
 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;
@@ -958,6 +960,78 @@ public function getEntityTypeFromClass($class_name) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    // @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 EntityTypeListenerInterface) {
+      $storage->onEntityTypeCreate($entity_type);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    // @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 EntityTypeListenerInterface) {
+      $storage->onEntityTypeUpdate($entity_type, $original);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    // @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 EntityTypeListenerInterface) {
+      $storage->onEntityTypeDelete($entity_type);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionCreate($storage_definition);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionUpdate($storage_definition, $original);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionDelete($storage_definition);
+    }
+  }
+
+  /**
    * Acts on entity bundle rename.
    *
    * @param string $entity_type_id
diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
index 5bf03cc..66456bf 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 {
+interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
 
   /**
    * Builds a list of entity type labels suitable for a Form API options list.
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index b181bb4..cca590d 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -97,6 +97,16 @@ public function getEntityType() {
   /**
    * {@inheritdoc}
    */
+  public function hasData() {
+    return (bool) $this->getQuery()
+      ->range(0, 1)
+      ->count()
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function loadUnchanged($id) {
     $this->resetCache(array($id));
     return $this->load($id);
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
index 32867f4..5268505 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
@@ -31,6 +31,14 @@
   const FIELD_LOAD_REVISION = 'FIELD_LOAD_REVISION';
 
   /**
+   * Checks whether the storage contains at least one entity.
+   *
+   * @return bool
+   *   TRUE if the storage has data, FALSE otherwise.
+   */
+  public function hasData();
+
+  /**
    * Resets the internal, static entity cache.
    *
    * @param $ids
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeListenerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeListenerInterface.php
new file mode 100644
index 0000000..c42667c
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityTypeListenerInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\EntityTypeListenerInterface.
+ */
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Defines an interface for reacting to entity type creation, deletion, and updates.
+ *
+ * @todo Convert to Symfony events: https://www.drupal.org/node/2332935
+ */
+interface EntityTypeListenerInterface {
+
+  /**
+   * Reacts to the creation of the entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type being created.
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type);
+
+  /**
+   * Reacts to the update of the entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original);
+
+  /**
+   * Reacts to the deletion of the entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type being deleted.
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type);
+
+}
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/ContentEntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php
new file mode 100644
index 0000000..c7bb557
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+/**
+ * Defines an interface for handling the storage schema of content entities.
+ */
+interface ContentEntitySchemaHandlerInterface extends EntitySchemaHandlerInterface, FieldStorageDefinitionListenerInterface {
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php
new file mode 100644
index 0000000..66f88c1
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php
@@ -0,0 +1,361 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaManager.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Manages changes in the entity and field schema.
+ */
+class ContentEntitySchemaManager implements EntitySchemaManagerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The entity manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   * @param StateInterface $state
+   */
+  public function __construct(EntityManagerInterface $entity_manager, StateInterface $state) {
+    $this->entityManager = $entity_manager;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    // Store the current definitions to be able to track changes.
+    $this->saveEntityTypeDefinition($entity_type);
+    if ($entity_type->isFieldable()) {
+      $entity_type_id = $entity_type->id();
+      $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    // Store the current definitions to be able to track changes.
+    $this->saveEntityTypeDefinition($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $this->deleteEntityTypeDefinition($entity_type_id);
+    // Ensure we delete any data concerning this entity type. It might have
+    // switched from fieldable to non-fieldable during its life cycle.
+    $this->deleteFieldStorageDefinitions($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$storage_definition->getName()] = $storage_definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$storage_definition->getName()] = $storage_definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    unset($definitions[$storage_definition->getName()]);
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeList($entity_type_id = NULL) {
+    $change_list = array();
+    $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; });
+    $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions);
+
+    foreach ($entity_type_ids as $entity_type_id) {
+      $definition = $definitions[$entity_type_id];
+      $storage = $this->entityManager->getStorage($entity_type_id);
+
+      if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntitySchemaProviderInterface) {
+        // Check whether there are changes in the entity type definition that
+        // would affect entity schema.
+        $original = $this->loadEntityTypeDefinition($entity_type_id);
+        if ($storage->requiresEntitySchemaChanges($definition, $original)) {
+          $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED;
+          if ($storage->requiresEntityDataMigration($definition, $original)) {
+            $change_list[$entity_type_id]['data_migration'] = TRUE;
+          }
+        }
+
+        // Check whether there are changes in the field storage definitions that
+        // would affect entity schema. We skip definitions with custom storage
+        // as they do not affect entity schema.
+        if ($definition->isFieldable()) {
+          $field_changes = array();
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+
+          // Detect created field storage definitions.
+          $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_CREATED; }, $created));
+
+          // Detect deleted field storage definitions.
+          $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_DELETED; }, $deleted));
+
+          // Now compare field storage definitions.
+          foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) {
+            /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */
+            if (!$definition->hasCustomStorage()) {
+              $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()];
+              if ($storage->requiresFieldSchemaChanges($original, $definition)) {
+                $field_changes[$field_name] = static::DEFINITION_UPDATED;
+                if ($storage->requiresFieldDataMigration($original, $definition)) {
+                  $change_list[$entity_type_id]['data_migration'] = TRUE;
+                }
+              }
+            }
+          }
+
+          if ($field_changes) {
+            $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes;
+          }
+        }
+      }
+    }
+
+    return array_filter($change_list);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyChanges($entity_type_id = NULL) {
+    foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) {
+      try {
+        $has_data = $this->entityManager->getStorage($entity_type_id)->hasData();
+      }
+      catch (DatabaseExceptionWrapper $e) {
+        // The entity schema might be corrupted. In this case it is safer to
+        // assume there is data available, to avoid performing unrecoverable
+        // operations.
+        $has_data = TRUE;
+      }
+
+      // We do not allow any kind of schema change that would imply a data
+      // migration.
+      if (empty($change_list['data_migration']) || !$has_data) {
+        // 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);
+          $this->entityManager->onEntityTypeUpdate($entity_type, $this->loadEntityTypeDefinition($entity_type_id));
+        }
+
+        // Process field storage definition changes.
+        if (!empty($change_list['field_storage_definitions'])) {
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($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;
+            }
+          }
+        }
+      }
+      else {
+        $args = array('@entity_type_id' => $entity_type_id);
+        $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args);
+        throw new EntityStorageException($message);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeSummary($entity_type_id = NULL) {
+    $summary = array();
+
+    foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) {
+      // Process entity type definition changes.
+      if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+        $definition = $this->entityManager->getDefinition($entity_type_id);
+        $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->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->loadFieldStorageDefinitions($entity_type_id);
+
+        foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+          $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name];
+          $args = array('%field_name' => $definition->getLabel());
+
+          switch ($change) {
+            case static::DEFINITION_CREATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args);
+              break;
+
+            case static::DEFINITION_UPDATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args);
+              break;
+
+            case static::DEFINITION_DELETED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args);
+              break;
+          }
+        }
+      }
+    }
+
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSystemRequirements($phase) {
+    $requirements = array(
+      'title' => t('Entity schema'),
+    );
+
+    if ($this->getChangeList()) {
+      $requirements['value'] = $this->t('Out of date');
+      $requirements['severity'] = REQUIREMENT_ERROR;
+      $requirements['description'] = $requirements['update']['description'] = $this->t('Some entity types have schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => base_path() . 'update.php'));
+    }
+    else {
+      $requirements['value'] = $this->t('Up to date');
+    }
+
+    return $requirements;
+  }
+
+  /**
+   * Returns the specified stored entity type definition.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityTypeInterface
+   *   A stored entity type definition.
+   */
+  protected function loadEntityTypeDefinition($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Stores the specified stored entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The entity type definition.
+   */
+  protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) {
+    $entity_type_id = $definition->id();
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $definition);
+  }
+
+  /**
+   * Deletes the specified stored entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteEntityTypeDefinition($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Returns the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   *   An array of field storage definitions.
+   */
+  protected function loadFieldStorageDefinitions($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+  /**
+   * Stores the field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+   *   An array of field storage definitions.
+   */
+  protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions);
+  }
+
+  /**
+   * Deletes the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteFieldStorageDefinitions($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php
new file mode 100644
index 0000000..d1df87d
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Provides a common interface for a storage classes providing entity schema.
+ */
+interface ContentEntitySchemaProviderInterface {
+
+  /**
+   * Checks whether the definition changes imply entity schema changes.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply entity schema changes, FALSE otherwise.
+   */
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original);
+
+  /**
+   * Checks whether the definition changes imply field schema changes.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply field schema changes, FALSE otherwise.
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Checks whether the entity type definition changes imply a data migration.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original);
+
+
+  /**
+   * Checks whether the field storage definition changes imply a data migration.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
index a38b82c..51001bd 100644
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
@@ -6,9 +6,10 @@
  */
 
 namespace Drupal\Core\Entity\Schema;
+use Drupal\Core\Entity\EntityTypeListenerInterface;
 
 /**
  * Defines an interface for handling the storage schema of entities.
  */
-interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface {
+interface EntitySchemaHandlerInterface extends EntityTypeListenerInterface {
 }
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php
new file mode 100644
index 0000000..84116be
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntitySchemaManagerInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\EntityTypeListenerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+/**
+ * Defines an interface for managing changes in the entity and field schema.
+ */
+interface EntitySchemaManagerInterface extends EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
+
+  /**
+   * 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;
+
+  /**
+   * Returns a list of changes to entity type and field storage definitions.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type to be checked for changes.
+   *   Defaults to all the available entity types.
+   *
+   * @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
+   *   - data_migration: boolean indicating whether the changes imply a data
+   *     migration.
+   */
+  public function getChangeList($entity_type_id = NULL);
+
+  /**
+   * Applies all the detected valid changes.
+   *
+   * Only changes that do not imply a data migration are applied when data is
+   * available for a certain entity type. If any change fails to comply with
+   * this policy the operation is aborted.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type whose changes have to be
+   *   applied. Defaults to all the available entity types.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   This exception is thrown when trying to apply changes that imply a data
+   *   migration, when data is available in the storage.
+   */
+  public function applyChanges($entity_type_id = NULL);
+
+  /**
+   * Returns a human readable summary of the detected changes.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type to be checked for changes.
+   *   Defaults to all the available entity types.
+   */
+  public function getChangeSummary($entity_type_id = NULL);
+
+  /**
+   * Returns a requirements array describing the current status.
+   *
+   * @param string $phase
+   *   The phase identifier.
+   *
+   * @return array
+   *   A requirements array.
+   *
+   * @see hook_requirements()
+   */
+  public function getSystemRequirements($phase);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php
deleted file mode 100644
index c976782..0000000
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Entity\Schema\EntitySchemaProviderInterface.
- */
-
-namespace Drupal\Core\Entity\Schema;
-
-/**
- * Defines a common interface to return the storage schema for entities.
- */
-interface EntitySchemaProviderInterface {
-
-  /**
-   * Gets the full schema array for a given entity type.
-   *
-   * @return array
-   *   A schema array for the entity type's tables.
-   */
-  public function getSchema();
-
-}
diff --git a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
index 830d2ab..1572478 100644
--- a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php
@@ -7,14 +7,19 @@
 
 namespace Drupal\Core\Entity\Schema;
 
+use Drupal\Component\Utility\String;
+use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
  */
-class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
+class SqlContentEntityStorageSchema implements ContentEntitySchemaHandlerInterface, ContentEntitySchemaProviderInterface {
 
   /**
    * The entity type this schema builder is responsible for.
@@ -45,6 +50,27 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
   protected $schema;
 
   /**
+   * The database connection to be used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The schema manager service.
+   *
+   * @var \Drupal\Core\Entity\Schema\EntitySchemaManagerInterface
+   */
+  protected $schemaManager;
+
+  /**
    * Constructs a SqlContentEntityStorageSchema.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -53,18 +79,227 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
    *   The entity type.
    * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage
    *   The storage of the entity type. This must be an SQL-based storage.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
    */
-  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) {
+  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) {
     $this->entityType = $entity_type;
     $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
     $this->storage = $storage;
+    $this->database = $database;
+  }
+
+  /**
+   * @return \Drupal\Core\State\StateInterface
+   */
+  protected function state() {
+    if (!isset($this->state)) {
+      $this->state = \Drupal::state();
+    }
+    return $this->state;
+  }
+
+  /**
+   * @return \Drupal\Core\Entity\Schema\EntitySchemaManagerInterface
+   */
+  protected function schemaManager() {
+    if (!isset($this->schemaManager)) {
+      $this->schemaManager = \Drupal::service('entity.schema.manager');
+    }
+    return $this->schemaManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return !$original ||
+    $original->getStorageClass() != $definition->getStorageClass() ||
+    $original->isRevisionable() != $definition->isRevisionable() ||
+    $original->isTranslatable() != $definition->isTranslatable() ||
+    // Detect changes in key or index definitions.
+    $this->loadEntitySchemaData($original) != $this->getEntitySchemaData($definition, $this->getEntitySchema($definition, TRUE));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return !$original ||
+    $original->getSchema() != $definition->getSchema() ||
+    $original->isRevisionable() != $definition->isRevisionable() ||
+    $original->hasCustomStorage() != $definition->hasCustomStorage() ||
+    $this->requiresFieldDataMigration($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    // A change in the storage class may or may not imply a data migration. We
+    // assume it does. This method should be overridden otherwise. Basically the
+    // only schema change that does not imply a data migration is from
+    // revisionable to non revisionable, as in that case we just need to drop
+    // revision tables.
+    return $original->getStorageClass() != $definition->getStorageClass() ||
+    $original->isRevisionable() != $definition->isRevisionable() ||
+    $original->isTranslatable() != $definition->isTranslatable();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    return $this->getEntitySchema($this->entityType);
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    // If the field changes its custom storage status, we will need to create or
+    // drop its schema. In any case we cannot migrate its data as custom storage
+    // is involved.
+    if ($original->hasCustomStorage() || $definition->hasCustomStorage()) {
+      return TRUE;
+    }
+    // If columns change we may need data manipulation, which we cannot handle.
+    if ($original->getColumns() != $definition->getColumns()) {
+      return TRUE;
+    }
+    // @todo Add additional checks for field storage changes that can affect
+    //   table layout: https://www.drupal.org/node/1498720.
+
+    return FALSE;
+  }
+
+  /**
+   * {@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) {
+      if (!$schema_handler->tableExists($table_name)) {
+        $schema_handler->createTable($table_name, $table_schema);
+      }
+    }
+    $this->saveEntitySchemaData($entity_type, $schema);
+    $this->schemaManager()->onEntityTypeCreate($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $this->checkEntityType($entity_type);
+    $this->checkEntityType($original);
+
+    // If we have no data just recreate the entity schema from scratch.
+    if (!$this->database->schema()->tableExists($this->storage->getBaseTable()) || !$this->storage->hasData()) {
+      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;
+      }
+    }
+    else {
+      $schema_handler = $this->database->schema();
+
+      // Drop original indexes and unique keys.
+      foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) {
+        if (!empty($schema['indexes'])) {
+          foreach ($schema['indexes'] as $name => $specifier) {
+            $schema_handler->dropIndex($table_name, $name);
+          }
+        }
+        if (!empty($schema['unique keys'])) {
+          foreach ($schema['unique keys'] as $name => $specifier) {
+            $schema_handler->dropUniqueKey($table_name, $name);
+          }
+        }
+      }
+
+      // Create new indexes and unique keys.
+      $entity_schema = $this->getEntitySchema($entity_type, TRUE);
+      foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
+        if (!empty($schema['indexes'])) {
+          foreach ($schema['indexes'] as $name => $specifier) {
+            $schema_handler->addIndex($table_name, $name, $specifier);
+          }
+        }
+        if (!empty($schema['unique keys'])) {
+          foreach ($schema['unique keys'] as $name => $specifier) {
+            $schema_handler->addUniqueKey($table_name, $name, $specifier);
+          }
+        }
+      }
+
+      // Store the updated entity schema.
+      $this->saveEntitySchemaData($entity_type, $entity_schema);
+      $this->schemaManager()->onEntityTypeUpdate($entity_type, $original);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
+    $schema_handler = $this->database->schema();
+    foreach ($this->getEntitySchemaTables() as $table_name) {
+      if ($schema_handler->tableExists($table_name)) {
+        $schema_handler->dropTable($table_name);
+      }
+    }
+    $this->schemaManager()->onEntityTypeDelete($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    $this->schemaManager()->onFieldStorageDefinitionCreate($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $this->schemaManager()->onFieldStorageDefinitionUpdate($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    $this->schemaManager()->onFieldStorageDefinitionDelete($storage_definition);
+  }
+
+  /**
+   * Checks that we are dealing with the correct entity type.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $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(ContentEntityTypeInterface $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;
   }
 
   /**
@@ -85,7 +320,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();
@@ -137,7 +372,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(),
@@ -147,6 +382,70 @@ protected function getTables() {
   }
 
   /**
+   * Returns entity schema definitions for index and key definitions.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param array $schema
+   *   The entity schema array.
+   *
+   * @return array
+   *   A stripped down version of the $schema Schema API array containing, for
+   *   each table, only the key and index definitions not derived from field
+   *   storage definitions.
+   */
+  protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
+    $schema_data = array();
+    $entity_type_id = $entity_type->id();
+    $keys = array('indexes', 'unique keys');
+    $unused_keys = array_flip(array('description', 'fields', 'foreign keys'));
+
+    foreach ($schema as $table_name => $table_schema) {
+      $table_schema = array_diff_key($table_schema, $unused_keys);
+      foreach ($keys as $key) {
+        // Exclude data generated from field storage definitions, we will check
+        // that separately.
+        if (!empty($table_schema[$key])) {
+          $data_keys = array_keys($table_schema[$key]);
+          $entity_keys = array_filter($data_keys, function ($key) use ($entity_type_id) {
+            return strpos($key, $entity_type_id . '_field_') !== 0;
+          });
+          $table_schema[$key] = array_intersect_key($table_schema[$key], array_flip($entity_keys));
+        }
+      }
+      $schema_data[$table_name] = array_filter($table_schema);
+    }
+
+    return $schema_data;
+  }
+
+  /**
+   * Loads stored schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   *
+   * @return array
+   *   The entity schema data array.
+   */
+  protected function loadEntitySchemaData(ContentEntityTypeInterface $entity_type) {
+    return $this->state()->get('entity.schema.handler.' . $entity_type->id() . '.schema_data') ?: array();
+  }
+
+  /**
+   * Stores schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param array $schema
+   *   The entity schema data array.
+   */
+  protected function saveEntitySchemaData(ContentEntityTypeInterface $entity_type, $schema) {
+    $data = $this->getEntitySchemaData($entity_type, $schema);
+    $this->state()->set('entity.schema.handler.' . $entity_type->id() . '.schema_data', $data);
+  }
+
+  /**
    * Returns the schema for a single field definition.
    *
    * @param array $schema
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
index 107ed26..b0a9b05 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
@@ -8,12 +8,11 @@
 namespace Drupal\Core\Entity\Sql;
 
 use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
 
 /**
  * A common interface for SQL-based entity storage implementations.
  */
-interface SqlEntityStorageInterface extends EntityStorageInterface, EntitySchemaProviderInterface {
+interface SqlEntityStorageInterface extends EntityStorageInterface {
 
   /**
    * Gets a table mapping for the entity's SQL tables.
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index aa36d42..3acfd91 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -12,7 +12,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\String;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -845,19 +845,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         }
         drupal_set_installed_schema_version($module, $version);
 
-        // Install any entity schemas belonging to the module.
+        // Notify the entity manager that this module's entity types are new,
+        // so that it can install its schemas or whatever else is needed.
         $entity_manager = \Drupal::entityManager();
-        $schema = \Drupal::database()->schema();
         foreach ($entity_manager->getDefinitions() as $entity_type) {
           if ($entity_type->getProvider() == $module) {
-            $storage = $entity_manager->getStorage($entity_type->id());
-            if ($storage instanceof EntitySchemaProviderInterface) {
-              foreach ($storage->getSchema() as $table_name => $table_schema) {
-                if (!$schema->tableExists($table_name)) {
-                  $schema->createTable($table_name, $table_schema);
-                }
-              }
-            }
+            $entity_manager->onEntityTypeCreate($entity_type);
           }
         }
 
@@ -965,19 +958,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // Remove all configuration belonging to the module.
       \Drupal::service('config.manager')->uninstall('module', $module);
 
-      // Remove any entity schemas belonging to the module.
-
-      $schema = \Drupal::database()->schema();
+      // Notify the entity manager that this module's entity types are being
+      // deleted, so that it can uninstall its schemas or whatever else is
+      // needed.
       foreach ($entity_manager->getDefinitions() as $entity_type) {
         if ($entity_type->getProvider() == $module) {
-          $storage = $entity_manager->getStorage($entity_type->id());
-          if ($storage instanceof EntitySchemaProviderInterface) {
-            foreach ($storage->getSchema() as $table_name => $table_schema) {
-              if ($schema->tableExists($table_name)) {
-                $schema->dropTable($table_name);
-              }
-            }
-          }
+          $entity_manager->onEntityTypeDelete($entity_type);
         }
       }
 
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/contact/tests/modules/contact_storage_test/contact_storage_test.install b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
index bcbe5df..c56d5b5 100644
--- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
+++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
@@ -9,16 +9,15 @@
  * Implements hook_install().
  */
 function contact_storage_test_install() {
-  // ModuleHandler won't create the schema automatically because Message entity
-  // belongs to contact.module.
-  // @todo Remove this when https://www.drupal.org/node/1498720 is in.
   $entity_manager = \Drupal::entityManager();
-  $schema = \Drupal::database()->schema();
   $entity_type = $entity_manager->getDefinition('contact_message');
-  $storage = $entity_manager->getStorage($entity_type->id());
-  foreach ($storage->getSchema() as $table_name => $table_schema) {
-    if (!$schema->tableExists($table_name)) {
-      $schema->createTable($table_name, $table_schema);
-    }
-  }
+
+  // Recreate the original entity type definition, in order to notify the
+  // manager of what changed. The change of storage backend will trigger
+  // schema installation.
+  // @see contact_storage_test_entity_type_alter()
+  $original = clone $entity_type;
+  $original->setStorageClass('Drupal\Core\Entity\ContentEntityNullStorage');
+
+  $entity_manager->onEntityTypeUpdate($entity_type, $original);
 }
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index f6953b3..5931f36 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,10 +334,10 @@ 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);
   }
 
   /**
@@ -406,7 +406,7 @@ 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;
       }
     }
diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php
index 4ef5e10..314dff6 100644
--- a/core/modules/simpletest/src/KernelTestBase.php
+++ b/core/modules/simpletest/src/KernelTestBase.php
@@ -11,10 +11,11 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\EntityTypeListenerInterface;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Site\Settings;
-use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
 use Symfony\Component\DependencyInjection\Parameter;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpFoundation\Request;
@@ -388,24 +389,15 @@ protected function installSchema($module, $tables) {
   protected function installEntitySchema($entity_type_id) {
     /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
     $entity_manager = $this->container->get('entity.manager');
-    /** @var \Drupal\Core\Database\Schema $schema_handler */
-    $schema_handler = $this->container->get('database')->schema();
+    $entity_type = $entity_manager->getDefinition($entity_type_id);
+    $entity_manager->onEntityTypeCreate($entity_type);
 
     $storage = $entity_manager->getStorage($entity_type_id);
-    if ($storage instanceof EntitySchemaProviderInterface) {
-      $schema = $storage->getSchema();
-      foreach ($schema as $table_name => $table_schema) {
-        $schema_handler->createTable($table_name, $table_schema);
-      }
-
+    if ($storage instanceof SqlEntityStorageInterface && $storage instanceof EntityTypeListenerInterface) {
+      $table_mapping = $storage->getTableMapping();
       $this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array(
         '%entity_type' => $entity_type_id,
-        '%tables' => '{' . implode('}, {', array_keys($schema)) . '}',
-      )));
-    }
-    else {
-      throw new \RuntimeException(String::format('Entity type %entity_type does not support automatic schema installation.', array(
-        '%entity-type' => $entity_type_id,
+        '%tables' => '{' . implode('}, {', $table_mapping->getTableNames()) . '}',
       )));
     }
   }
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index 62752ae..7b84a54 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -130,9 +130,14 @@ public function handle($op, Request $request) {
     }
     else {
       switch ($op) {
+        case 'entity_schema':
+          $regions['sidebar_first'] = $this->updateTasksList('entity_schema');
+          $output = $this->entitySchema();
+          break;
+
         case 'selection':
           $regions['sidebar_first'] = $this->updateTasksList('selection');
-          $output = $this->selection();
+          $output = $this->selection($request);
           break;
 
         case 'run':
@@ -162,7 +167,7 @@ public function handle($op, Request $request) {
     if ($output instanceof Response) {
       return $output;
     }
-    $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update');
+    $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal module updates');
 
     return new Response(DefaultHtmlPageRenderer::renderPage($output, $title, 'maintenance', $regions));
   }
@@ -199,22 +204,66 @@ protected function info() {
       '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
     );
 
-    $url = new Url('system.db_update', array('op' => 'selection'));
+    $entity_schema_updates = count(\Drupal::service('entity.schema.manager')->getChangeList());
+    $url = new Url('system.db_update', array('op' => ($entity_schema_updates ? 'entity_schema' : 'selection')));
+    $build['link'] = array(
+      '#type' => 'link',
+      '#title' => $this->t('Continue'),
+      '#attributes' => array('class' => array('button', 'button--primary')),
+    ) + $url->toRenderArray();
+    return $build;
+  }
+
+  /**
+   * Renders a list of available entity schema updates.
+   *
+   * @return array
+   *   A render array.
+   */
+  function entitySchema() {
+    $build = array('#title' => $this->t('Drupal entity schema updates'));
+
+    // Build a summary of the entity schema changes.
+    $summary = \Drupal::service('entity.schema.manager')->getChangeSummary();
+    if ($summary) {
+      $entity_manager = $this->entityManager();
+      foreach ($summary as $entity_type_id => $items) {
+        $definition = $entity_manager->getDefinition($entity_type_id);
+        $build['summary'][$entity_type_id] = array(
+          '#type' => 'details',
+          '#title' => $definition->getLabel(),
+        );
+        $build['summary'][$entity_type_id]['changes'] = array(
+          '#theme' => 'item_list',
+          '#items' => $items,
+        );
+      }
+    }
+    else {
+      $build['summary'] = array('#markup' => $this->t('No entity schema changes available.'));
+    }
+
+    $url = new Url('system.db_update', array('op' => 'selection'), array('query' => array('entity_schema_updates' => (int) !empty($summary))));
     $build['link'] = array(
       '#type' => 'link',
       '#title' => $this->t('Continue'),
       '#attributes' => array('class' => array('button', 'button--primary')),
+      '#weight' => 5,
     ) + $url->toRenderArray();
+
     return $build;
   }
 
   /**
-   * Renders a list of available database updates.
+   * Renders a list of available module updates.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object.
    *
    * @return array
    *   A render array.
    */
-  protected function selection() {
+  protected function selection(Request $request) {
     // Make sure there is no stale theme registry.
     $this->cache->deleteAll();
 
@@ -285,7 +334,8 @@ protected function selection() {
       drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
     }
 
-    if (empty($count)) {
+    $force_updates = (bool) $request->get('entity_schema_updates');
+    if (empty($count) && !$force_updates) {
       drupal_set_message($this->t('No pending updates.'));
       unset($build);
       $build['links'] = array(
@@ -297,21 +347,31 @@ protected function selection() {
       drupal_flush_all_caches();
     }
     else {
-      $build['help'] = array(
-        '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
-        '#weight' => -5,
-      );
-      if ($incompatible_count) {
-        $build['start']['#title'] = $this->formatPlural(
-          $count,
-          '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
-          '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
-          array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
+      if ($count > 0) {
+        $build['help'] = array(
+          '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
+          '#weight' => -5,
         );
+        if ($incompatible_count) {
+          $build['start']['#title'] = $this->formatPlural(
+            $count,
+            '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
+            '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
+            array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
+          );
+        }
+        else {
+          $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
+        }
       }
       else {
-        $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
+        unset($build);
+        $build['help'] = array(
+          '#markup' => '<p>No module update available.</p>',
+          '#weight' => -5,
+        );
       }
+
       $url = new Url('system.db_update', array('op' => 'run'));
       $build['link'] = array(
         '#type' => 'link',
@@ -471,7 +531,8 @@ protected function updateTasksList($active = NULL) {
     $tasks = array(
       'requirements' => $this->t('Verify requirements'),
       'info' => $this->t('Overview'),
-      'selection' => $this->t('Review updates'),
+      'entity_schema' => $this->t('Review entity schema updates'),
+      'selection' => $this->t('Review module updates'),
       'run' => $this->t('Run updates'),
       'results' => $this->t('Review log'),
     );
@@ -501,9 +562,16 @@ protected function triggerBatch(Request $request) {
       $this->state->set('system.maintenance_mode', TRUE);
     }
 
-    $start = $this->getModuleUpdates();
+    // First of all perform entity schema updates, if needed, so that subsequent
+    // updates work with a correct entity schema.
+    $operations = array();
+    if (\Drupal::service('entity.schema.manager')->getChangeList()) {
+      $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema'));
+    }
+
     // 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 +583,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/system.install b/core/modules/system/system.install
index 8e7b534..9b7e901 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -452,6 +452,9 @@ function system_requirements($phase) {
         }
       }
     }
+
+    // Check entity schema status.
+    $requirements['entity_schema'] = \Drupal::service('entity.schema.manager')->getSystemRequirements($phase);
   }
 
   // Verify the update.php access setting
diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
index 51ac5df..5d92f69 100644
--- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Language\Language;
 use Drupal\Tests\UnitTestCase;
@@ -266,21 +267,23 @@ public function providerTestGetRevisionDataTable() {
   }
 
   /**
-   * Tests ContentEntityDatabaseStorage::getSchema().
+   * Tests ContentEntityDatabaseStorage::onEntityTypeCreate().
    *
    * @covers ::__construct()
-   * @covers ::getSchema()
-   * @covers ::schemaHandler()
+   * @covers ::onEntityTypeCreate()
    * @covers ::getTableMapping()
    */
-  public function testGetSchema() {
+  public function testOnEntityTypeCreate() {
     $columns = array(
       'value' => array(
         'type' => 'int',
       ),
     );
 
-    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    $this->fieldDefinitions['id']->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue('id'));
     $this->fieldDefinitions['id']->expects($this->once())
       ->method('getColumns')
       ->will($this->returnValue($columns));
@@ -305,35 +308,60 @@ public function testGetSchema() {
         array('id' => 'id'),
       )));
 
-    $this->entityManager->expects($this->once())
-      ->method('getFieldStorageDefinitions')
-      ->with($this->entityType->id())
-      ->will($this->returnValue($this->fieldDefinitions));
-
     $this->setUpEntityStorage();
 
     $expected = array(
-      'entity_test' => array(
-        'description' => 'The base table for entity_test entities.',
-        'fields' => array(
-          'id' => array(
-            'type' => 'serial',
-            'description' => NULL,
-            'not null' => TRUE,
-          ),
+      'description' => 'The base table for entity_test entities.',
+      'fields' => array(
+        'id' => array(
+          'type' => 'serial',
+          'description' => NULL,
+          'not null' => TRUE,
         ),
-        'primary key' => array('id'),
-        'unique keys' => array(),
-        'indexes' => array(),
-        'foreign keys' => array(),
       ),
+      'primary key' => array('id'),
+      'unique keys' => array(),
+      'indexes' => array(),
+      'foreign keys' => array(),
     );
-    $this->assertEquals($expected, $this->entityStorage->getSchema());
 
-    // Test that repeated calls do not result in repeatedly instantiating
-    // SqlContentEntityStorageSchema as getFieldStorageDefinitions() is only
-    // expected to be called once.
-    $this->assertEquals($expected, $this->entityStorage->getSchema());
+    $schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $schema_handler->expects($this->any())
+      ->method('createTable')
+      ->with($this->equalTo('entity_test'), $this->equalTo($expected));
+
+    $this->connection->expects($this->once())
+      ->method('schema')
+      ->will($this->returnValue($schema_handler));
+
+    $storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
+      ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
+      ->setMethods(array('schemaHandler'))
+      ->getMock();
+
+    $state = $this->getMock('Drupal\Core\State\StateInterface');
+    $schema_manager = $this->getMock('Drupal\Core\Entity\Schema\EntitySchemaManagerInterface');
+    $schema_handler = $this->getMockBuilder('Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $storage, $this->connection))
+      ->setMethods(array('state', 'schemaManager'))
+      ->getMock();
+    $schema_handler
+      ->expects($this->any())
+      ->method('state')
+      ->will($this->returnValue($state));
+    $schema_handler
+      ->expects($this->any())
+      ->method('schemaManager')
+      ->will($this->returnValue($schema_manager));
+
+    $storage
+      ->expects($this->any())
+      ->method('schemaHandler')
+      ->will($this->returnValue($schema_handler));
+
+    $storage->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -397,18 +425,7 @@ public function testGetTableMappingSimple(array $entity_keys) {
   public function testGetTableMappingSimpleWithFields(array $entity_keys) {
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
-
-    $this->entityType->expects($this->any())
-      ->method('getKey')
-      ->will($this->returnValueMap(array(
-        array('id', $entity_keys['id']),
-        array('uuid', $entity_keys['uuid']),
-        array('bundle', $entity_keys['bundle']),
-      )));
-
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
     $this->setUpEntityStorage();
 
     $mapping = $this->entityStorage->getTableMapping();
@@ -533,25 +550,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $field_names = array_merge(
-        $field_names,
-        $revisionable_field_names
-      );
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $field_names = array_merge($field_names, $revisionable_field_names);
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -658,9 +661,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) {
 
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
     $this->entityType->expects($this->exactly(2))
       ->method('isTranslatable')
@@ -840,21 +841,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -1078,9 +1068,56 @@ public function testCreate() {
   }
 
   /**
+   * Returns a set of mock field definitions for the given names.
+   *
+   * @param array $field_names
+   *   An array of field names.
+   * @param array $methods
+   *   (optional) An associative array of mock method return values keyed by
+   *   method name.
+   *
+   * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[]
+   *   An array of mock field definitions.
+   */
+  protected function mockFieldDefinitions(array $field_names, $methods = array()) {
+    $field_definitions = array();
+    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+
+    // Assign common method return values.
+    foreach ($methods as $method => $result) {
+      $definition
+        ->expects($this->any())
+        ->method($method)
+        ->will($this->returnValue($result));
+    }
+
+    // Assign field names to mock definitions.
+    foreach ($field_names as $field_name) {
+      $field_definitions[$field_name] = clone $definition;
+      $field_definitions[$field_name]
+        ->expects($this->any())
+        ->method('getName')
+        ->will($this->returnValue($field_name));
+    }
+
+    return $field_definitions;
+  }
+
+  /**
    * Sets up the content entity database storage.
    */
   protected function setUpEntityStorage() {
+    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->will($this->returnValue($this->entityType));
+
+    $this->entityManager->expects($this->any())
+      ->method('getFieldStorageDefinitions')
+      ->will($this->returnValue($this->fieldDefinitions));
 
     $this->entityManager->expects($this->any())
       ->method('getBaseFieldDefinitions')
@@ -1223,7 +1260,6 @@ public function testLoadMultiplePersistentCacheMiss() {
 
     $entities = $entity_storage->loadMultiple(array($id));
     $this->assertEquals($entity, $entities[$id]);
-
   }
 
   /**
@@ -1239,6 +1275,7 @@ protected function setUpModuleHandlerNoImplementations() {
 
     $this->container->set('module_handler', $this->moduleHandler);
   }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/SqlContentEntityStorageSchemaTest.php
index f1e6a5b..95b075f 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Schema/SqlContentEntityStorageSchemaTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Schema/SqlContentEntityStorageSchemaTest.php
@@ -82,7 +82,7 @@ protected function setUp() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeBaseTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -246,16 +246,6 @@ public function testGetSchemaBase() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setExtraColumns('entity_test', array('default_langcode'));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -381,9 +371,18 @@ public function testGetSchemaBase() {
         ),
       ),
     );
-    $actual = $this->schemaHandler->getSchema();
 
-    $this->assertEquals($expected, $actual);
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setExtraColumns('entity_test', array('default_langcode'));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -391,7 +390,7 @@ public function testGetSchemaBase() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeBaseTable()
    * @covers ::initializeRevisionTable()
    * @covers ::addTableDefaults()
@@ -420,16 +419,6 @@ public function testGetSchemaRevisionable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -483,9 +472,17 @@ public function testGetSchemaRevisionable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
 
-    $this->assertEquals($expected, $actual);
+    $this->schemaHandler->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -493,7 +490,7 @@ public function testGetSchemaRevisionable() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeDataTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -507,7 +504,7 @@ public function testGetSchemaTranslatable() {
       ),
     ));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
 
@@ -519,16 +516,6 @@ public function testGetSchemaTranslatable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -575,9 +562,17 @@ public function testGetSchemaTranslatable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
 
-    $this->assertEquals($expected, $actual);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -585,7 +580,7 @@ public function testGetSchemaTranslatable() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeDataTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -626,18 +621,6 @@ public function testGetSchemaRevisionableTranslatable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -763,26 +746,86 @@ public function testGetSchemaRevisionableTranslatable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
 
-    $this->assertEquals($expected, $actual);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->onEntityTypeCreate($this->entityType);
   }
 
   /**
    * Sets up the schema handler.
    *
-   * This uses the field definitions set in $this->fieldDefinitions.
+   * This uses the field definitions set in $this->storageDefinitions.
+   *
+   * @param array $expected
+   *   (optional) An associative array describing the expected entity schema to
+   *   be created. Defaults to expecting nothing.
    */
-  protected function setUpSchemaHandler() {
-    $this->entityManager->expects($this->once())
+  protected function setUpEntitySchemaHandler(array $expected = array()) {
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->with($this->entityType->id())
+      ->will($this->returnValue($this->entityType));
+
+    $this->entityManager->expects($this->any())
       ->method('getFieldStorageDefinitions')
       ->with($this->entityType->id())
       ->will($this->returnValue($this->storageDefinitions));
-    $this->schemaHandler = new SqlContentEntityStorageSchema(
-      $this->entityManager,
-      $this->entityType,
-      $this->storage
-    );
+
+    $db_schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    if ($expected) {
+      $invocation_count = 0;
+      $expected_table_names = array_keys($expected);
+      $expected_table_schemas = array_values($expected);
+
+      $db_schema_handler->expects($this->any())
+        ->method('createTable')
+        ->with(
+          $this->callback(function($table_name) use (&$invocation_count, $expected_table_names) {
+            return $expected_table_names[$invocation_count] == $table_name;
+          }),
+          $this->callback(function($table_schema) use (&$invocation_count, $expected_table_schemas) {
+            return $expected_table_schemas[$invocation_count] == $table_schema;
+          })
+        )
+        ->will($this->returnCallback(function() use (&$invocation_count) {
+          $invocation_count++;
+        }));
+    }
+
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $connection->expects($this->any())
+      ->method('schema')
+      ->will($this->returnValue($db_schema_handler));
+
+    $state = $this->getMock('Drupal\Core\State\StateInterface');
+    $schema_manager = $this->getMock('Drupal\Core\Entity\Schema\EntitySchemaManagerInterface');
+    $this->schemaHandler = $this->getMockBuilder('Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection))
+      ->setMethods(array('state', 'schemaManager'))
+      ->getMock();
+    $this->schemaHandler
+      ->expects($this->any())
+      ->method('state')
+      ->will($this->returnValue($state));
+    $this->schemaHandler
+      ->expects($this->any())
+      ->method('schemaManager')
+      ->will($this->returnValue($schema_manager));
   }
 
   /**
@@ -795,7 +838,11 @@ protected function setUpSchemaHandler() {
    *   FieldStorageDefinitionInterface::getSchema().
    */
   public function setUpStorageDefinition($field_name, array $schema) {
-    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    // getDescription() is called once for each table.
+    $this->storageDefinitions[$field_name]->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue($field_name));
     // getDescription() is called once for each table.
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getDescription')
@@ -804,7 +851,7 @@ public function setUpStorageDefinition($field_name, array $schema) {
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getSchema')
       ->will($this->returnValue($schema));
-    $this->storageDefinitions[$field_name]->expects($this->once())
+    $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getColumns')
       ->will($this->returnValue($schema['columns']));
   }
diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
new file mode 100644
index 0000000..f685edd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface.
+ */
+
+namespace Drupal\Tests\Core\Field;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines a test interface to mock entity base field definitions.
+ */
+interface TestBaseFieldDefinitionInterface extends FieldDefinitionInterface, FieldStorageDefinitionInterface {
+}
