diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 3b4c4ab..7bf9eab 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1599,6 +1599,10 @@ function install_bootstrap_full() {
  *   The batch definition.
  */
 function install_profile_modules(&$install_state) {
+  // We need to manually trigger the installation of core-provided entity types,
+  // as those will not be handled by the module installer.
+  install_core_entity_type_definitions();
+
   $modules = \Drupal::state()->get('install_profile_modules') ?: array();
   $files = system_rebuild_module_data();
   \Drupal::state()->delete('install_profile_modules');
@@ -1639,6 +1643,18 @@ function install_profile_modules(&$install_state) {
 }
 
 /**
+ * Installs entity type definitions provided by core.
+ */
+function install_core_entity_type_definitions() {
+  $update_manager = \Drupal::entityDefinitionUpdateManager();
+  foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) {
+    if ($entity_type->getProvider() == 'core') {
+      $update_manager->installEntityType($entity_type);
+    }
+  }
+}
+
+/**
  * Installs themes.
  *
  * This does not use a batch, since installing themes is faster than modules and
@@ -1665,12 +1681,6 @@ function install_profile_themes(&$install_state) {
  *   An array of information about the current installation state.
  */
 function install_install_profile(&$install_state) {
-  // Now that all modules are installed, make sure the entity storage and other
-  // handlers are up to date with the current entity and field definitions. For
-  // example, Path module adds a base field to nodes and taxonomy terms after
-  // those modules are already installed.
-  \Drupal::service('entity.definition_update_manager')->applyUpdates();
-
   \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE);
   // Install all available optional config. During installation the module order
   // is determined by dependencies. If there are no dependencies between modules
diff --git a/core/includes/update.inc b/core/includes/update.inc
index 8e9a24c..01c2d52 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -10,7 +10,6 @@
 
 use Drupal\Component\Graph\Graph;
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Utility\Error;
 
 /**
@@ -219,26 +218,6 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
- * Performs entity definition updates, which can trigger schema updates.
- *
- * @param $context
- *   The batch context array.
- */
-function update_entity_definitions(&$context) {
-  try {
-    \Drupal::service('entity.definition_update_manager')->applyUpdates();
-  }
-  catch (EntityStorageException $e) {
-    watchdog_exception('update', $e);
-    $variables = Error::decodeException($e);
-    unset($variables['backtrace']);
-    $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: @message in %function (line %line of %file).', $variables));
-    $context['results']['core']['update_entity_definitions'] = $ret;
-    $context['results']['#abort'][] = 'update_entity_definitions';
-  }
-}
-
-/**
  * Returns a list of all the pending database updates.
  *
  * @return
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index 7621a48..da3e02a 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -687,4 +687,14 @@ public static function destination() {
     return static::getContainer()->get('redirect.destination');
   }
 
+  /**
+   * Returns the entity definition update manager.
+   *
+   * @return \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+   *   The entity definition update manager.
+   */
+  public static function entityDefinitionUpdateManager() {
+    return static::getContainer()->get('entity.definition_update_manager');
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
index 902aab9..c943b95 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
 use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 
@@ -95,13 +96,13 @@ public function getChangeSummary() {
    * {@inheritdoc}
    */
   public function applyUpdates() {
-    $change_list = $this->getChangeList();
-    if ($change_list) {
+    $complete_change_list = $this->getChangeList();
+    if ($complete_change_list) {
       // self::getChangeList() only disables the cache and does not invalidate.
       // In case there are changes, explicitly invalidate caches.
       $this->entityManager->clearCachedDefinitions();
     }
-    foreach ($change_list as $entity_type_id => $change_list) {
+    foreach ($complete_change_list as $entity_type_id => $change_list) {
       // Process entity type definition changes before storage definitions ones
       // this is necessary when you change an entity type from non-revisionable
       // to revisionable and at the same time add revisionable fields to the
@@ -127,42 +128,76 @@ public function applyUpdates() {
   /**
    * {@inheritdoc}
    */
-  public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) {
-    $change_list = $this->getChangeList();
-    if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) {
-      return FALSE;
-    }
-    if ($reset_cached_definitions) {
-      // self::getChangeList() only disables the cache and does not invalidate.
-      // In case there are changes, explicitly invalidate caches.
-      $this->entityManager->clearCachedDefinitions();
-    }
-    $this->doEntityUpdate($op, $entity_type_id);
-    return TRUE;
+  public function getEntityType($entity_type_id) {
+    $entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+    return $entity_type ? clone $entity_type : NULL;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) {
-    $change_list = $this->getChangeList();
-    if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) {
-      return FALSE;
-    }
+  public function installEntityType(EntityTypeInterface $entity_type) {
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onEntityTypeCreate($entity_type);
+  }
 
-    if ($reset_cached_definitions) {
-      // self::getChangeList() only disables the cache and does not invalidate.
-      // In case there are changes, explicitly invalidate caches.
-      $this->entityManager->clearCachedDefinitions();
+  /**
+   * {@inheritdoc}
+   */
+  public function updateEntityType(EntityTypeInterface $entity_type) {
+    $original = $this->getEntityType($entity_type->id());
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onEntityTypeUpdate($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstallEntityType(EntityTypeInterface $entity_type) {
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onEntityTypeDelete($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Pass a mutable field definition interface when we have one. See
+    //   https://www.drupal.org/node/2346329.
+    if ($storage_definition instanceof BaseFieldDefinition) {
+      $storage_definition
+        ->setName($name)
+        ->setTargetEntityTypeId($entity_type_id)
+        ->setProvider($provider)
+        ->setTargetBundle(NULL);
     }
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldStorageDefinition($name, $entity_type_id) {
+    $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+    return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
+  }
 
-    $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
-    $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
-    $storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
-    $original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL;
+  /**
+   * {@inheritdoc}
+   */
+  public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
+    $original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId());
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original);
+  }
 
-    $this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
-    return TRUE;
+  /**
+   * {@inheritdoc}
+   */
+  public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
+    $this->entityManager->clearCachedDefinitions();
+    $this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
index 5946eee..c617af8 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Entity;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
 /**
  * Defines an interface for managing entity definition updates.
  *
@@ -75,6 +77,9 @@ public function getChangeSummary();
   /**
    * Applies all the detected valid changes.
    *
+   * Use this with care, as it will apply updates for any module, which will
+   * lead to unpredictable results.
+   *
    * @throws \Drupal\Core\Entity\EntityStorageException
    *   This exception is thrown if a change cannot be applied without
    *   unacceptable data loss. In such a case, the site administrator needs to
@@ -84,67 +89,84 @@ public function getChangeSummary();
   public function applyUpdates();
 
   /**
-   * Performs a single entity definition update.
-   *
-   * This method should be used from hook_update_N() functions to process
-   * entity definition updates as part of the update function. This is only
-   * necessary if the hook_update_N() implementation relies on the entity
-   * definition update. All remaining entity definition updates will be run
-   * automatically after the hook_update_N() implementations.
+   * Returns an entity type definition ready to be manipulated.
    *
-   * @param string $op
-   *   The operation to perform, either static::DEFINITION_CREATED or
-   *   static::DEFINITION_UPDATED.
    * @param string $entity_type_id
-   *   The entity type to update.
-   * @param bool $reset_cached_definitions
-   *   (optional). Determines whether to clear the Entity Manager's cached
-   *   definitions before applying the update. Defaults to TRUE. Can be used
-   *   to prevent unnecessary cache invalidation when a hook_update_N() makes
-   *   multiple calls to this method.
+   *   The entity type identifier.
    *
-   * @return bool
-   *   TRUE if the entity update is processed, FALSE if not.
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   *   The entity type definition.
+   */
+  public function getEntityType($entity_type_id);
+
+  /**
+   * Installs a new entity type definition.
    *
-   * @throws \Drupal\Core\Entity\EntityStorageException
-   *   This exception is thrown if a change cannot be applied without
-   *   unacceptable data loss. In such a case, the site administrator needs to
-   *   apply some other process, such as a custom update function or a
-   *   migration via the Migrate module.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  public function installEntityType(EntityTypeInterface $entity_type);
+
+  /**
+   * Applies any change performed to the passed entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  public function updateEntityType(EntityTypeInterface $entity_type);
+
+  /**
+   * Uninstalls an entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
    */
-  public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
+  public function uninstallEntityType(EntityTypeInterface $entity_type);
 
   /**
-   * Performs a single field storage definition update.
+   * Returns a field storage definition ready to be manipulated.
+   *
+   * @param string $name
+   *   The field name.
+   * @param string $entity_type_id
+   *   The entity type identifier.
    *
-   * This method should be used from hook_update_N() functions to process field
-   * storage definition updates as part of the update function. This is only
-   * necessary if the hook_update_N() implementation relies on the field storage
-   * definition update. All remaining field storage definition updates will be
-   * run automatically after the hook_update_N() implementations.
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface
+   *   The field storage definition.
+   *
+   * @todo Make this return a mutable storage definition interface when we have
+   *   one. See https://www.drupal.org/node/2346329.
+   */
+  public function getFieldStorageDefinition($name, $entity_type_id);
+
+  /**
+   * Installs a new field storage definition.
    *
-   * @param string $op
-   *   The operation to perform, possible values are static::DEFINITION_CREATED,
-   *   static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
+   * @param string $name
+   *   The field storage definition name.
    * @param string $entity_type_id
-   *   The entity type to update.
-   * @param string $field_name
-   *   The field name to update.
-   * @param bool $reset_cached_definitions
-   *   (optional). Determines whether to clear the Entity Manager's cached
-   *   definitions before applying the update. Defaults to TRUE. Can be used
-   *   to prevent unnecessary cache invalidation when a hook_update_N() makes
-   *   multiple calls to this method.
+   *   The target entity type identifier.
+   * @param string $provider
+   *   The name of the definition provider.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   */
+  public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition);
 
-   * @return bool
-   *   TRUE if the entity update is processed, FALSE if not.
+  /**
+   * Applies any change performed to the passed field storage definition.
    *
-   * @throws \Drupal\Core\Entity\EntityStorageException
-   *   This exception is thrown if a change cannot be applied without
-   *   unacceptable data loss. In such a case, the site administrator needs to
-   *   apply some other process, such as a custom update function or a
-   *   migration via the Migrate module.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   */
+  public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Uninstalls a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
    */
-  public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
+  public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index b701279..472ed20 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -189,9 +189,10 @@ public function getFieldTableName($field_name) {
   public function getColumnNames($field_name) {
     if (!isset($this->columnMapping[$field_name])) {
       $this->columnMapping[$field_name] = array();
-      $storage_definition = $this->fieldStorageDefinitions[$field_name];
-      foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
-        $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name);
+      if (isset($this->fieldStorageDefinitions[$field_name])) {
+        foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
+          $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
+        }
       }
     }
     return $this->columnMapping[$field_name];
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 689655a..259d240 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -10,6 +10,8 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\SchemaException;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\ContentEntityStorageBase;
 use Drupal\Core\Entity\EntityBundleListenerInterface;
@@ -1351,7 +1353,9 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor
    * {@inheritdoc}
    */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
-    $this->getStorageSchema()->onEntityTypeCreate($entity_type);
+    $this->wrapSchemaException(function () use ($entity_type) {
+      $this->getStorageSchema()->onEntityTypeCreate($entity_type);
+    });
   }
 
   /**
@@ -1364,14 +1368,18 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
     // definition.
     $this->initTableLayout();
     // Let the schema handler adapt to possible table layout changes.
-    $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
+    $this->wrapSchemaException(function () use ($entity_type, $original) {
+      $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
+    });
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
-    $this->getStorageSchema()->onEntityTypeDelete($entity_type);
+    $this->wrapSchemaException(function () use ($entity_type) {
+      $this->getStorageSchema()->onEntityTypeDelete($entity_type);
+    });
   }
 
   /**
@@ -1386,14 +1394,18 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $
     if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
       $this->tableMapping = NULL;
     }
-    $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
+    $this->wrapSchemaException(function () use ($storage_definition) {
+      $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
+    });
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
+    $this->wrapSchemaException(function () use ($storage_definition, $original) {
+      $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
+    });
   }
 
   /**
@@ -1421,7 +1433,31 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
     }
 
     // Update the field schema.
-    $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
+    $this->wrapSchemaException(function () use ($storage_definition) {
+      $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
+    });
+  }
+
+  /**
+   * Wraps a database schema exception into an entity storage exception.
+   *
+   * @param callable $callback
+   *   The callback to be executed.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   When a database schema exception is thrown.
+   */
+  protected function wrapSchemaException(callable $callback) {
+    $message = 'Exception thrown while performing a schema update.';
+    try {
+      $callback();
+    }
+    catch (SchemaException $e) {
+      throw new EntityStorageException($message, 0, $e);
+    }
+    catch (DatabaseExceptionWrapper $e) {
+      throw new EntityStorageException($message, 0, $e);
+    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index b92636c..72fe53d 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -1391,6 +1391,20 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
           if ($field_name == $updated_field_name) {
             $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
 
+            // Handle NOT NULL constraints.
+            foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
+              $not_null = !empty($specifier['not null']);
+              $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
+              if ($not_null !== $original_not_null) {
+                if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
+                  throw new EntityStorageException('The "' . $column_name . '" column cannot have NOT NULL constraints as it holds NULL values.');
+                }
+                $column_schema = $original_schema[$table_name]['fields'][$column_name];
+                $column_schema['not null'] = $not_null;
+                $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
+              }
+            }
+
             // Drop original indexes and unique keys.
             if (!empty($original_schema[$table_name]['indexes'])) {
               foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
@@ -1423,6 +1437,26 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
   }
 
   /**
+   * Checks whether a field property has NULL values.
+   *
+   * @param string $table_name
+   *   The name of the table to inspect.
+   * @param string $column_name
+   *   The name of the column holding the field property data.
+   *
+   * @return bool
+   *   TRUE if NULL data is found, FALSE otherwise.
+   */
+  protected function hasNullFieldPropertyData($table_name, $column_name) {
+    $query = $this->database->select($table_name, 't')
+      ->fields('t', [$column_name])
+      ->range(0, 1);
+    $query->isNull('t.' . $column_name);
+    $result = $query->execute()->fetchAssoc();
+    return (bool) $result;
+  }
+
+  /**
    * Gets the schema for a single field definition.
    *
    * Entity types may override this method in order to optimize the generated
@@ -1803,10 +1837,15 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def
     }
 
     if (!$storage_definition->hasCustomStorage()) {
-      $schema = $this->getSchemaFromStorageDefinition($storage_definition);
-      foreach ($this->loadFieldSchemaData($original) as $table => $spec) {
-        if ($spec['fields'] != $schema[$table]['fields']) {
-          return TRUE;
+      $keys = array_flip($this->getColumnSchemaRelevantKeys());
+      $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
+      foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
+        foreach ($table_schema['fields'] as $name => $spec) {
+          $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
+          $stored_spec = array_intersect_key($spec, $keys);
+          if ($definition_spec != $stored_spec) {
+            return TRUE;
+          }
         }
       }
     }
@@ -1814,4 +1853,18 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def
     return FALSE;
   }
 
+  /**
+   * Returns a list of column schema keys affecting data storage.
+   *
+   * When comparing schema definitions, only changes in certain properties
+   * actually affect how data is stored and thus, if applied, may imply data
+   * manipulation.
+   *
+   * @return string[]
+   *   An array of key names.
+   */
+  protected function getColumnSchemaRelevantKeys() {
+    return ['type', 'size', 'length', 'unsigned'];
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 41e698c..3dbc4d0 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -10,9 +10,8 @@
 use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\PreExistingConfigException;
-use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\DrupalKernelInterface;
+use Drupal\Core\Entity\EntityStorageException;
 
 /**
  * Default implementation of the module installer.
@@ -212,14 +211,34 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
           $version = max(max($versions), $version);
         }
 
-        // Notify interested components that this module's entity types are new.
-        // For example, a SQL-based storage handler can use this as an
-        // opportunity to create the necessary database tables.
+        // Notify interested components that this module's entity types and
+        // field storage definitions are new. For example, a SQL-based storage
+        // handler can use this as an opportunity to create the necessary
+        // database tables.
         // @todo Clean this up in https://www.drupal.org/node/2350111.
         $entity_manager = \Drupal::entityManager();
+        $update_manager = \Drupal::entityDefinitionUpdateManager();
         foreach ($entity_manager->getDefinitions() as $entity_type) {
           if ($entity_type->getProvider() == $module) {
-            $entity_manager->onEntityTypeCreate($entity_type);
+            $update_manager->installEntityType($entity_type);
+          }
+          elseif ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
+            // The module being installed may be adding new fields to existing
+            // entity types. Field definitions for any entity type defined by
+            // the module are handled in the if branch.
+            foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
+              if ($storage_definition->getProvider() == $module) {
+                // If the module being installed is also defining a storage key
+                // for the entity type, the entity schema may not exist yet. It
+                // will be created later in that case.
+                try {
+                  $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
+                }
+                catch (EntityStorageException $e) {
+                  watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
+                }
+              }
+            }
           }
         }
 
@@ -362,9 +381,25 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // deleted. For example, a SQL-based storage handler can use this as an
       // opportunity to drop the corresponding database tables.
       // @todo Clean this up in https://www.drupal.org/node/2350111.
+      $update_manager = \Drupal::entityDefinitionUpdateManager();
       foreach ($entity_manager->getDefinitions() as $entity_type) {
         if ($entity_type->getProvider() == $module) {
-          $entity_manager->onEntityTypeDelete($entity_type);
+          $update_manager->uninstallEntityType($entity_type);
+        }
+        elseif ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
+          // The module being installed may be adding new fields to existing
+          // entity types. Field definitions for any entity type defined by
+          // the module are handled in the if branch.
+          $entity_type_id = $entity_type->id();
+          /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
+          $storage = $entity_manager->getStorage($entity_type_id);
+          foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
+            // @todo We need to trigger field purging here.
+            //   See https://www.drupal.org/node/2282119.
+            if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
+              $update_manager->uninstallFieldStorageDefinition($storage_definition);
+            }
+          }
         }
       }
 
diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php
index e30fd87..ff58c97 100644
--- a/core/lib/Drupal/Core/Extension/module.api.php
+++ b/core/lib/Drupal/Core/Extension/module.api.php
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Database\Database;
+use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\UpdateException;
 
@@ -35,8 +36,8 @@
  * - Database schema changes: adding, changing, or removing a database table or
  *   field; moving stored data to different fields or tables; changing the
  *   format of stored data.
- * - Content entity or field changes: these updates are normally handled
- *   automatically by the entity system, but should at least be tested.
+ * - Content entity or field changes: adding, changing, or removing a field
+ *   definition, entity definition, or any of their properties.
  *
  * @section sec_how How to write update code
  * Update code for a module is put into an implementation of hook_update_N(),
@@ -531,6 +532,16 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
  *   ignored, and make sure that the configuration data you are saving matches
  *   the configuration schema at the time when you write the update function
  *   (later updates may change it again to match new schema changes).
+ * - Never assume your field or entity type definitions are the same when the
+ *   update will run as they are when you wrote the update function. Always
+ *   retrieve the correct version via
+ *   \Drupal::entityDefinitionUpdateManager()::getEntityType() or
+ *   \Drupal::entityDefinitionUpdateManager()::getFieldStorageDefinition(). When
+ *   adding a new definition always replicate it in the update function body as
+ *   you would do with a schema definition.
+ * - Never call \Drupal::entityDefinitionUpdateManager()::applyUpdates() in an
+ *   update function, as it will apply updates for any module not only yours,
+ *   which will lead to unpredictable results.
  * - Be careful about API functions and especially CRUD operations that you use
  *   in your update function. If they invoke hooks or use services, they may
  *   not behave as expected, and it may actually not be appropriate to use the
@@ -548,6 +559,9 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
  *   long as you make sure that your update data matches the schema, and you
  *   use the $has_trusted_data argument in the save operation.
  * - Marking a container for rebuild.
+ * - Using the API provided by \Drupal::entityDefinitionUpdateManager() to
+ *   update the entity schema based on changes in entity type or field
+ *   definitions provided by your module.
  *
  * See https://www.drupal.org/node/2535316 for more on writing update functions.
  *
@@ -585,6 +599,8 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
  * @see schemaapi
  * @see hook_update_last_removed()
  * @see update_get_update_list()
+ * @see node_update_8001
+ * @see system_update_8004
  * @see https://www.drupal.org/node/2535316
  */
 function hook_update_N(&$sandbox) {
diff --git a/core/modules/block_content/block_content.install b/core/modules/block_content/block_content.install
new file mode 100644
index 0000000..14bb224
--- /dev/null
+++ b/core/modules/block_content/block_content.install
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the block_content module.
+ */
+
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Add 'revision_translation_affected' field to 'block_content' entities.
+ */
+function block_content_update_8001() {
+  // Install the definition that this field had in
+  // \Drupal\block_content\Entity\BlockContent::baseFieldDefinitions()
+  // at the time that this update function was written. If/when code is
+  // deployed that changes that definition, the corresponding module must
+  // implement an update function that invokes
+  // \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition()
+  // with the new definition.
+  $storage_definition = BaseFieldDefinition::create('boolean')
+    ->setLabel(t('Revision translation affected'))
+    ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
+    ->setReadOnly(TRUE)
+    ->setRevisionable(TRUE)
+    ->setTranslatable(TRUE);
+
+  \Drupal::entityDefinitionUpdateManager()
+    ->installFieldStorageDefinition('revision_translation_affected', 'block_content', 'block_content', $storage_definition);
+}
diff --git a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
index a786a0e..2e4f059 100644
--- a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
+++ b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
@@ -62,7 +62,7 @@ public function updateDefinitions(array $entity_types) {
         foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) {
           /** @var $storage_definition \Drupal\Core\Field\FieldStorageDefinitionInterface */
           if ($storage_definition->getProvider() == 'content_translation') {
-            $this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
+            $this->updateManager->installFieldStorageDefinition($storage_definition->getName(), $entity_type_id, 'content_translation', $storage_definition);
           }
         }
       }
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index a7cb55e..1050d5a 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -5,9 +5,7 @@
  * Install, update and uninstall functions for the node module.
  */
 
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Uuid\Uuid;
-use Drupal\Core\Url;
+use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\user\RoleInterface;
 
 /**
@@ -153,3 +151,73 @@ function node_uninstall() {
   // Delete remaining general module variables.
   \Drupal::state()->delete('node.node_access_needs_rebuild');
 }
+
+/**
+ * Add 'revision_translation_affected' field to 'node' entities.
+ */
+function node_update_8001() {
+  // Install the definition that this field had in
+  // \Drupal\node\Entity\Node::baseFieldDefinitions()
+  // at the time that this update function was written. If/when code is
+  // deployed that changes that definition, the corresponding module must
+  // implement an update function that invokes
+  // \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition()
+  // with the new definition.
+  $storage_definition = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Revision translation affected'))
+      ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
+      ->setReadOnly(TRUE)
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE);
+
+  \Drupal::entityDefinitionUpdateManager()
+    ->installFieldStorageDefinition('revision_translation_affected', 'node', 'node', $storage_definition);
+}
+
+/**
+ * Remove obsolete indexes from the node schema.
+ */
+function node_update_8002() {
+  // The "node__default_langcode" and "node_field__langcode" indexes were
+  // removed from \Drupal\node\NodeStorageSchema in
+  // https://www.drupal.org/node/2261669, but this update function wasn't
+  // added until https://www.drupal.org/node/2542748. Regenerate the related
+  // schemas to ensure they match the currently expected status.
+  $manager = \Drupal::entityDefinitionUpdateManager();
+  // Regenerate entity type indexes, this should drop "node__default_langcode".
+  $manager->updateEntityType($manager->getEntityType('node'));
+  // Regenerate "langcode" indexes, this should drop "node_field__langcode".
+  $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition('langcode', 'node'));
+}
+
+/**
+ * Promote 'status' and 'uid' fields to entity keys.
+ */
+function node_update_8003() {
+  // The 'status' and 'uid' fields were added to the 'entity_keys' annotation
+  // of \Drupal\node\Entity\Node in https://www.drupal.org/node/2498919, but
+  // this update function wasn't added until
+  // https://www.drupal.org/node/2542748. In between, sites could have
+  // performed interim updates, which would have included automated entity
+  // schema updates prior to that being removed (see that issue for details).
+  // Therefore, we check for whether the keys have already been installed.
+  $manager = \Drupal::entityDefinitionUpdateManager();
+  $entity_type = $manager->getEntityType('node');
+  $entity_keys = $entity_type->getKeys();
+  $entity_keys['status'] = 'status';
+  $entity_keys['uid'] = 'uid';
+  $entity_type->set('entity_keys', $entity_keys);
+  $manager->updateEntityType($entity_type);
+
+  // @todo The above should be enough, since that is the only definition that
+  //   changed. But \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema varies
+  //   field schema by whether a field is an entity key, so invoke
+  //   onFieldStorageDefinitionUpdate() with an unmodified
+  //   $field_storage_definition to trigger the necessary changes.
+  //   SqlContentEntityStorageSchema::onEntityTypeUpdate() should be fixed to
+  //   automatically handle this.
+  //   See https://www.drupal.org/node/2554245.
+  foreach (array('status', 'uid') as $field_name) {
+    $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition($field_name, 'node'));
+  }
+}
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index aa46654..28ab72b 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -9,7 +9,6 @@
 
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
 use Drupal\Core\Render\BareHtmlPageRendererInterface;
@@ -62,13 +61,6 @@ class DbUpdateController extends ControllerBase {
   protected $account;
 
   /**
-   * The entity definition update manager.
-   *
-   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
-   */
-  protected $entityDefinitionUpdateManager;
-
-  /**
    * The bare HTML page renderer.
    *
    * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
@@ -97,19 +89,16 @@ class DbUpdateController extends ControllerBase {
    *   The module handler.
    * @param \Drupal\Core\Session\AccountInterface $account
    *   The current user.
-   * @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
-   *   The entity definition update manager.
    * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
    *   The bare HTML page renderer.
    */
-  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, BareHtmlPageRendererInterface $bare_html_page_renderer) {
+  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
     $this->root = $root;
     $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->cache = $cache;
     $this->state = $state;
     $this->moduleHandler = $module_handler;
     $this->account = $account;
-    $this->entityDefinitionUpdateManager = $entity_definition_update_manager;
     $this->bareHtmlPageRenderer = $bare_html_page_renderer;
   }
 
@@ -124,7 +113,6 @@ public static function create(ContainerInterface $container) {
       $container->get('state'),
       $container->get('module_handler'),
       $container->get('current_user'),
-      $container->get('entity.definition_update_manager'),
       $container->get('bare_html_page_renderer')
     );
   }
@@ -325,23 +313,6 @@ protected function selection(Request $request) {
       drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
     }
 
-    // If there are entity definition updates, display their summary.
-    if ($this->entityDefinitionUpdateManager->needsUpdates()) {
-      $entity_build = array();
-      $summary = $this->entityDefinitionUpdateManager->getChangeSummary();
-      foreach ($summary as $entity_type_id => $items) {
-        $entity_update_key = 'entity_type_updates_' . $entity_type_id;
-        $entity_build[$entity_update_key] = array(
-          '#theme' => 'item_list',
-          '#items' => $items,
-          '#title' => $entity_type_id . ' entity type',
-        );
-        $count++;
-      }
-      // Display these above the module updates, since they will be run first.
-      $build['start'] = $entity_build + $build['start'];
-    }
-
     if (empty($count)) {
       drupal_set_message($this->t('No pending updates.'));
       unset($build);
@@ -600,16 +571,6 @@ protected function triggerBatch(Request $request) {
       }
     }
 
-    // Lastly, perform entity definition updates, which will update storage
-    // schema if needed. If module update functions need to work with specific
-    // entity schema they should call the entity update service for the specific
-    // update themselves.
-    // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate()
-    // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate()
-    if ($this->entityDefinitionUpdateManager->needsUpdates()) {
-      $operations[] = array('update_entity_definitions', array());
-    }
-
     $batch['operations'] = $operations;
     $batch += array(
       'title' => $this->t('Updating'),
diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
index e5c8f4b..a9f2b73 100644
--- a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
@@ -126,6 +126,17 @@ protected function modifyBaseField() {
   }
 
   /**
+   * Promotes a field to an entity key.
+   */
+  protected function makeBaseFieldEntityKey() {
+    $entity_type = clone $this->entityManager->getDefinition('entity_test_update');
+    $entity_keys = $entity_type->getKeys();
+    $entity_keys['new_base_field'] = 'new_base_field';
+    $entity_type->set('entity_keys', $entity_keys);
+    $this->state->set('entity_test_update.entity_type', $entity_type);
+  }
+
+  /**
    * Removes the new base field from the 'entity_test_update' entity type.
    */
   protected function removeBaseField() {
diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
index 8ae7fa5..c294714 100644
--- a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -14,10 +14,8 @@
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeEvents;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
-use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\FieldStorageDefinitionEvents;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\entity_test\FieldStorageDefinition;
 
 /**
  * Tests EntityDefinitionUpdateManager functionality.
@@ -607,27 +605,6 @@ public function testEntityTypeSchemaUpdateAndRevisionableBaseFieldCreateWithoutD
   }
 
   /**
-   * Tests ::applyEntityUpdate() and ::applyFieldUpdate().
-   */
-  public function testSingleActionCalls() {
-    // Ensure that the methods return FALSE when called with bogus information.
-    $this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo'), 'Calling applyEntityUpdate() with a non-existent entity returns FALSE.');
-    $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo', 'bar'), 'Calling applyFieldUpdate() with a non-existent entity returns FALSE.');
-    $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'bar'), 'Calling applyFieldUpdate() with a non-existent field returns FALSE.');
-    $this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update'), 'Calling applyEntityUpdate() with an $op that is not applicable to the entity type returns FALSE.');
-    $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_DELETED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() with an $op that is not applicable to the field returns FALSE.');
-
-    // Create a new base field.
-    $this->addRevisionableBaseField();
-    $this->assertTrue($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() correctly returns TRUE.');
-    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
-    // Make the entity type revisionable.
-    $this->updateEntityTypeToRevisionable();
-    $this->assertTrue($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_UPDATED, 'entity_test_update'), 'Calling applyEntityUpdate() correctly returns TRUE.');
-    $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
-  }
-
-  /**
    * Ensures that a new field and index on a shared table are created.
    *
    * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
@@ -671,4 +648,52 @@ public function testCreateIndexUsingEntityStorageSchemaWithData() {
     }
   }
 
+  /**
+   * Tests updating a base field when it has existing data.
+   */
+  public function testBaseFieldEntityKeyUpdateWithExistingData() {
+    // Add the base field and run the update.
+    $this->addBaseField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Save an entity with the base field populated.
+    $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
+
+    // Save an entity with the base field not populated.
+    /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
+    $entity = $this->entityManager->getStorage('entity_test_update')->create();
+    $entity->save();
+
+    // Promote the base field to an entity key. This will trigger the addition
+    // of a NOT NULL constraint.
+    $this->makeBaseFieldEntityKey();
+
+    // Try to apply the update and verify they fail since we have a NULL value.
+    $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail($message);
+    }
+    catch (EntityStorageException $e) {
+      $this->pass($message);
+    }
+
+    // Check that the update is correctly applied when no NULL data is left.
+    $entity->set('new_base_field', $this->randomString());
+    $entity->save();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->pass('The update is correctly performed when no NULL data exists.');
+
+    // Check that the update actually applied a NOT NULL constraint.
+    $entity->set('new_base_field', NULL);
+    $message = 'The NOT NULL constraint was correctly applied.';
+    try {
+      $entity->save();
+      $this->fail($message);
+    }
+    catch (EntityStorageException $e) {
+      $this->pass($message);
+    }
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php
index 0e613f1..3481b55 100644
--- a/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php
+++ b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php
@@ -19,11 +19,6 @@ class SqlContentEntityStorageSchemaIndexTest extends UpdatePathTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['update_order_test'];
-
-  /**
-   * {@inheritdoc}
-   */
   public function setUp() {
     $this->databaseDumpFiles = [
       __DIR__ . '/../../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
@@ -35,9 +30,6 @@ public function setUp() {
    * Tests entity and field schema database updates and execution order.
    */
   public function testIndex() {
-    // Enable the hook implementations in the update_order_test module.
-    \Drupal::state()->set('update_order_test', TRUE);
-
     // The initial Drupal 8 database dump before any updates does not include
     // the entity ID in the entity field data table indices that were added in
     // https://www.drupal.org/node/2261669.
@@ -45,38 +37,12 @@ public function testIndex() {
     $this->assertFalse(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode does not exist prior to running updates.');
     $this->assertFalse(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode does not exist prior to running updates.');
 
-    // Running database updates should automatically update the entity schemata
-    // to add the indices from https://www.drupal.org/node/2261669.
+    // Running database updates should update the entity schemata to add the
+    // indices from https://www.drupal.org/node/2261669.
     $this->runUpdates();
     $this->assertFalse(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode properly removed.');
     $this->assertTrue(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode properly created on the node_field_data table.');
     $this->assertTrue(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode properly created on the user_field_data table.');
-
-    // Ensure that hook_update_N() implementations were in the expected order
-    // relative to the entity and field updates. The expected order is:
-    // 1. Initial Drupal 8.0.0-beta12 installation with no indices.
-    // 2. update_order_test_update_8001() is invoked.
-    // 3. update_order_test_update_8002() is invoked.
-    // 4. update_order_test_update_8002() explicitly applies the updates for
-    //    the update_order_test_field storage. See update_order_test.module.
-    // 5. update_order_test_update_8002() explicitly applies the updates for
-    //    the node entity type indices listed above.
-    // 6. The remaining entity schema updates are applied automatically after
-    //    all update hook implementations have run, which applies the user
-    //    index update.
-   $this->assertTrue(\Drupal::state()->get('update_order_test_update_8001', FALSE), 'Index node__default_langcode still existed during update_order_test_update_8001(), indicating that it ran before the entity type updates.');
-
-    // Node updates were run during update_order_test_update_8002().
-    $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_node__default_langcode', TRUE), 'The node__default_langcode index was removed during update_order_test_update_8002().');
-    $this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_node__id__default_langcode__langcode', FALSE), 'The node__id__default_langcode__langcode index was created during update_order_test_update_8002().');
-
-    // Ensure that the base field created by update_order_test_update_8002() is
-    // created when we expect.
-    $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_update_order_test_before', TRUE), 'The update_order_test field was not been created on Node before update_order_test_update_8002().');
-    $this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_update_order_test_after', FALSE), 'The update_order_test field was created on Node by update_order_test_update_8002().');
-
-    // User update were not run during update_order_test_update_8002().
-    $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_user__id__default_langcode__langcode', TRUE));
   }
 
 }
diff --git a/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php
new file mode 100644
index 0000000..25f97a9
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\Update\UpdateApiEntityDefinitionUpdateTest.
+ */
+
+namespace Drupal\system\Tests\Entity\Update;
+
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\simpletest\WebTestBase;
+use Drupal\system\Tests\Update\DbUpdatesTrait;
+
+/**
+ * Tests performing entity updates through the Update API.
+ *
+ * @group Entity
+ */
+class UpdateApiEntityDefinitionUpdateTest extends WebTestBase {
+
+  use DbUpdatesTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['entity_test'];
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The entity definition update manager.
+   *
+   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+   */
+  protected $updatesManager;
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->entityManager = $this->container->get('entity.manager');
+    $this->updatesManager = $this->container->get('entity.definition_update_manager');
+
+    $admin = $this->drupalCreateUser([], FALSE, TRUE);
+    $this->drupalLogin($admin);
+  }
+
+  /**
+   * Tests that individual updates applied sequentially work as expected.
+   */
+  public function testSingleUpdates() {
+    // Create a test entity.
+    $user_ids = [mt_rand(), mt_rand()];
+    $entity = EntityTest::create(['name' => $this->randomString(),  'user_id' => $user_ids]);
+    $entity->save();
+
+    // Check that only a single value is stored for 'user_id'.
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id->target_id, $user_ids[0]);
+
+    // Make 'user_id' multiple by running updates.
+    $this->enableUpdates('entity_test', 'entity_definition_updates', 8001);
+    $this->runUpdates();
+
+    // Check that data was correctly migrated.
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id->target_id, $user_ids[0]);
+
+    // Store multiple data and check it is correctly stored.
+    $entity->user_id = $user_ids;
+    $entity->save();
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 2);
+    $this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]);
+    $this->assertEqual($entity->user_id[1]->target_id, $user_ids[1]);
+
+    // Make 'user_id' single again by running updates.
+    $this->enableUpdates('entity_test', 'entity_definition_updates', 8002);
+    $this->runUpdates();
+
+    // Check that data was correctly migrated/dropped.
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id->target_id, $user_ids[0]);
+  }
+
+  /**
+   * Tests that multiple updates applied in bulk work as expected.
+   */
+  public function testMultipleUpdates() {
+    // Create a test entity.
+    $user_ids = [mt_rand(), mt_rand()];
+    $entity = EntityTest::create(['name' => $this->randomString(),  'user_id' => $user_ids]);
+    $entity->save();
+
+    // Check that only a single value is stored for 'user_id'.
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id->target_id, $user_ids[0]);
+
+    // Make 'user_id' multiple and then single again by running updates.
+    $this->enableUpdates('entity_test', 'entity_definition_updates', 8002);
+    $this->runUpdates();
+
+    // Check that data was correctly migrated back and forth.
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id->target_id, $user_ids[0]);
+
+    // Check that only a single value is stored for 'user_id' again.
+    $entity->user_id = $user_ids;
+    $entity->save();
+    $entity = $this->reloadEntity($entity);
+    $this->assertEqual(count($entity->user_id), 1);
+    $this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]);
+  }
+
+  /**
+   * Tests that entity updates are correctly reported in the status report page.
+   */
+  function testStatusReport() {
+    // Create a test entity.
+    $entity = EntityTest::create(['name' => $this->randomString(),  'user_id' => mt_rand()]);
+    $entity->save();
+
+    // Check that the status report initially displays no error.
+    $this->drupalGet('admin/reports/status');
+    $this->assertNoRaw('Out of date');
+    $this->assertNoRaw('Mismatch detected');
+
+    // Enable an entity update and check that we have a dedicated status report
+    // item.
+    $this->container->get('state')->set('entity_test.remove_name_field', TRUE);
+    $this->drupalGet('admin/reports/status');
+    $this->assertNoRaw('Out of date');
+    $this->assertRaw('Mismatch detected');
+
+    // Enable a db update and check that now the entity update status report
+    // item is no longer displayed. We assume an update function will fix the
+    // mismatch.
+    $this->enableUpdates('entity_test', 'status_report', 8001);
+    $this->drupalGet('admin/reports/status');
+    $this->assertRaw('Out of date');
+    $this->assertNoRaw('Mismatch detected');
+
+    // Run db updates and check that entity updates were not applied.
+    $this->runUpdates();
+    $this->drupalGet('admin/reports/status');
+    $this->assertNoRaw('Out of date');
+    $this->assertRaw('Mismatch detected');
+
+    // Check that en exception would be triggered when trying to apply them with
+    // existing data.
+    $message = 'Entity updates cannot run if entity data exists.';
+    try {
+      $this->updatesManager->applyUpdates();
+      $this->fail($message);
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass($message);
+    }
+
+    // Check the status report is the same after trying to apply updates.
+    $this->drupalGet('admin/reports/status');
+    $this->assertNoRaw('Out of date');
+    $this->assertRaw('Mismatch detected');
+
+    // Delete entity data, enable a new update, run updates again and check that
+    // entity updates were not applied even when no data exists.
+    $entity->delete();
+    $this->enableUpdates('entity_test', 'status_report', 8002);
+    $this->runUpdates();
+    $this->drupalGet('admin/reports/status');
+    $this->assertNoRaw('Out of date');
+    $this->assertRaw('Mismatch detected');
+  }
+
+  /**
+   * Reloads the specified entity.
+   *
+   * @param \Drupal\entity_test\Entity\EntityTest $entity
+   *   An entity object.
+   *
+   * @return \Drupal\entity_test\Entity\EntityTest
+   *   The reloaded entity object.
+   */
+  protected function reloadEntity(EntityTest $entity) {
+    $this->entityManager->useCaches(FALSE);
+    $this->entityManager->getStorage('entity_test')->resetCache([$entity->id()]);
+    return EntityTest::load($entity->id());
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/DbUpdatesTrait.php b/core/modules/system/src/Tests/Update/DbUpdatesTrait.php
new file mode 100644
index 0000000..e9f0093
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/DbUpdatesTrait.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\DbUpdatesTrait.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+
+/**
+ * Provides methods to conditionally enable db update functions and run updates.
+ */
+trait DbUpdatesTrait {
+
+  use StringTranslationTrait;
+
+  /**
+   * Enables db updates until the specified index.
+   *
+   * @param string $module
+   *   The name of the module defining the update functions.
+   * @param string $group
+   *   A name identifying the group of update functions to enable.
+   * @param $index
+   *   The index of the last update function to run.
+   */
+  protected function enableUpdates($module, $group, $index) {
+    $this->container->get('state')->set($module . '.db_updates.' . $group, $index);
+  }
+
+  /**
+   * Runs DB updates.
+   */
+  protected function runUpdates() {
+    $this->drupalGet(Url::fromRoute('system.db_update'));
+    $this->clickLink($this->t('Continue'));
+    $this->clickLink($this->t('Apply pending updates'));
+  }
+
+  /**
+   * Conditionally load Update API functions for the specified group.
+   *
+   * @param string $module
+   *   The name of the module defining the update functions.
+   * @param string $group
+   *   A name identifying the group of update functions to enable.
+   */
+  public static function includeUpdates($module, $group) {
+    if ($index = \Drupal::state()->get($module . '.db_updates.' . $group)) {
+      module_load_include('inc', $module, 'update/' . $group . '_' . $index);
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
index 6c9b23b..d3d103d 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
@@ -248,6 +248,9 @@ protected function runUpdates() {
       $config = $this->config($name);
       $this->assertConfigSchema($typed_config, $name, $config->get());
     }
+
+    // Ensure that the update hooks updated all entity schema.
+    $this->assertFalse(\Drupal::service('entity.definition_update_manager')->needsUpdates(), 'After all updates ran, entity schema is up to date.');
   }
 
   /**
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index f7c1b39..ce06b7a 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -594,10 +594,16 @@ function system_requirements($phase) {
         }
       }
     }
-    if (!isset($requirements['update']['severity']) && \Drupal::service('entity.definition_update_manager')->needsUpdates()) {
-      $requirements['update']['severity'] = REQUIREMENT_ERROR;
-      $requirements['update']['value'] = t('Out of date');
-      $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
+
+    // Verify that no entity updates are pending after running every DB update.
+    if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) {
+      $requirements['entity_update'] = array(
+        'title' => t('Entity/field definitions'),
+        'value' => t('Mismatch detected'),
+        'severity' => REQUIREMENT_ERROR,
+        // @todo Provide details: https://www.drupal.org/node/2554911
+        'description' => t('Mismatched entity and/or field definitions.'),
+      );
     }
   }
 
@@ -1247,3 +1253,18 @@ function system_update_8003() {
     ]
   );
 }
+
+/**
+ * Add a (id, default_langcode, langcode) composite index to entities.
+ */
+function system_update_8004() {
+  // \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema was changed in
+  // https://www.drupal.org/node/2261669 to include a (id, default_langcode,
+  // langcode) compound index, but this update function wasn't added until
+  // https://www.drupal.org/node/2542748. Regenerate the related schemas to
+  // ensure they match the currently expected status.
+  $manager = \Drupal::entityDefinitionUpdateManager();
+  foreach (array_keys(\Drupal::entityManager()->getDefinitions()) as $entity_type_id) {
+    $manager->updateEntityType($manager->getEntityType($entity_type_id));
+  }
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install
index 1a6d9a2..a9ff423 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.install
+++ b/core/modules/system/tests/modules/entity_test/entity_test.install
@@ -5,6 +5,8 @@
  * Install, update and uninstall functions for the entity_test module.
  */
 
+use Drupal\system\Tests\Update\DbUpdatesTrait;
+
 /**
  * Implements hook_install().
  */
@@ -49,3 +51,6 @@ function entity_test_schema() {
   );
   return $schema;
 }
+
+DbUpdatesTrait::includeUpdates('entity_test', 'entity_definition_updates');
+DbUpdatesTrait::includeUpdates('entity_test', 'status_report');
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 8bddd32..eea78e8 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -12,6 +12,7 @@
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
@@ -119,14 +120,25 @@ function entity_test_entity_base_field_info(EntityTypeInterface $entity_type) {
  * Implements hook_entity_base_field_info_alter().
  */
 function entity_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'entity_test_mulrev' && ($names = \Drupal::state()->get('entity_test.field_definitions.translatable'))) {
+  $state = \Drupal::state();
+  if ($entity_type->id() == 'entity_test_mulrev' && ($names = $state->get('entity_test.field_definitions.translatable'))) {
     foreach ($names as $name => $value) {
       $fields[$name]->setTranslatable($value);
     }
   }
-  if ($entity_type->id() == 'node' && Drupal::state()->get('entity_test.node_remove_status_field')) {
+  if ($entity_type->id() == 'node' && $state->get('entity_test.node_remove_status_field')) {
     unset($fields['status']);
   }
+  if ($entity_type->id() == 'entity_test' && $state->get('entity_test.remove_name_field')) {
+    unset($fields['name']);
+  }
+  // In 8001 we are assuming that a new definition with multiple cardinality has
+  // been deployed.
+  // @todo Remove this if we end up using state definitions at runtime. See
+  //    https://www.drupal.org/node/2554235.
+  if ($entity_type->id() == 'entity_test' && $state->get('entity_test.db_updates.entity_definition_updates') == 8001) {
+    $fields['user_id']->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+  }
 }
 
 /**
diff --git a/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc
new file mode 100644
index 0000000..8c54781
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Defines the 8001 db update for the "entity_definition_updates" group.
+ */
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Makes the 'user_id' field multiple and migrate its data.
+ */
+function entity_test_update_8001() {
+  // To update the field schema we need to have no field data in the storage,
+  // thus we retrieve it, delete it from storage, and write it back to the
+  // storage after updating the schema.
+  $database = \Drupal::database();
+
+  // Retrieve existing field data.
+  $user_ids = $database->select('entity_test', 'et')
+    ->fields('et', ['id', 'user_id'])
+    ->execute()
+    ->fetchAllKeyed();
+
+  // Remove data from the storage.
+  $database->update('entity_test')
+    ->fields(['user_id' => NULL])
+    ->execute();
+
+  // Update definitions and schema.
+  $manager = \Drupal::entityDefinitionUpdateManager();
+  $storage_definition = $manager->getFieldStorageDefinition('user_id', 'entity_test');
+  $storage_definition->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+  $manager->updateFieldStorageDefinition($storage_definition);
+
+  // Restore entity data in the new schema.
+  $insert_query = $database->insert('entity_test__user_id')
+    ->fields(['bundle', 'deleted', 'entity_id', 'revision_id', 'langcode', 'delta', 'user_id_target_id']);
+  foreach ($user_ids as $id => $user_id) {
+    $insert_query->values(['entity_test', 0, $id, $id, 'en', 0, $user_id]);
+  }
+  $insert_query->execute();
+}
diff --git a/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc
new file mode 100644
index 0000000..e304ebd
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Defines the 8002 db update for the "entity_definition_updates" group.
+ */
+
+require_once 'entity_definition_updates_8001.inc';
+
+/**
+ * Makes the 'user_id' field single and migrate its data.
+ */
+function entity_test_update_8002() {
+  // To update the field schema we need to have no field data in the storage,
+  // thus we retrieve it, delete it from storage, and write it back to the
+  // storage after updating the schema.
+  $database = \Drupal::database();
+
+  // Retrieve existing entity data.
+  $query = $database->select('entity_test__user_id', 'et')
+    ->fields('et', ['entity_id', 'user_id_target_id']);
+  $query->condition('et.delta', 0);
+  $user_ids = $query->execute()->fetchAllKeyed();
+
+  // Remove data from the storage.
+  $database->truncate('entity_test__user_id')->execute();
+
+  // Update definitions and schema.
+  $manager = \Drupal::entityDefinitionUpdateManager();
+  $storage_definition = $manager->getFieldStorageDefinition('user_id', 'entity_test');
+  $storage_definition->setCardinality(1);
+  $manager->updateFieldStorageDefinition($storage_definition);
+
+  // Restore entity data in the new schema.
+  foreach ($user_ids as $id => $user_id) {
+    $database->update('entity_test')
+      ->fields(['user_id' => $user_id])
+      ->condition('id', $id)
+      ->execute();
+  }
+}
diff --git a/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc b/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc
new file mode 100644
index 0000000..91eb723
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Defines the 8001 db update for the "status_report" group.
+ */
+
+/**
+ * Test update.
+ */
+function entity_test_update_8001() {
+  // Empty update, we just want to trigger an error in the status report.
+}
diff --git a/core/modules/system/tests/modules/entity_test/update/status_report_8002.inc b/core/modules/system/tests/modules/entity_test/update/status_report_8002.inc
new file mode 100644
index 0000000..7e10218
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/update/status_report_8002.inc
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Defines the 8002 db update for the "status_report" group.
+ */
+
+require_once 'status_report_8001.inc';
+
+/**
+ * Test update.
+ */
+function entity_test_update_8002() {
+  // Empty update, we just want to trigger an update run.
+}
diff --git a/core/profiles/minimal/src/Tests/MinimalTest.php b/core/profiles/minimal/src/Tests/MinimalTest.php
index bfc4e19..becf641 100644
--- a/core/profiles/minimal/src/Tests/MinimalTest.php
+++ b/core/profiles/minimal/src/Tests/MinimalTest.php
@@ -39,5 +39,8 @@ function testMinimal() {
     $this->drupalLogin($this->rootUser);
     $this->drupalGet('update.php/selection');
     $this->assertText('No pending updates.');
+
+    // Ensure that there are no pending entity updates after installation.
+    $this->assertFalse($this->container->get('entity.definition_update_manager')->needsUpdates(), 'After installation, entity schema is up to date.');
   }
 }
diff --git a/core/profiles/standard/src/Tests/StandardTest.php b/core/profiles/standard/src/Tests/StandardTest.php
index ee867d1..e46686d 100644
--- a/core/profiles/standard/src/Tests/StandardTest.php
+++ b/core/profiles/standard/src/Tests/StandardTest.php
@@ -157,6 +157,9 @@ function testStandard() {
     $this->drupalLogin($this->rootUser);
     $this->drupalGet('update.php/selection');
     $this->assertText('No pending updates.');
+
+    // Ensure that there are no pending entity updates after installation.
+    $this->assertFalse($this->container->get('entity.definition_update_manager')->needsUpdates(), 'After installation, entity schema is up to date.');
   }
 
 }
