diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
index 4586d01..e7f2fff 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
@@ -61,7 +61,7 @@ class Schema extends DatabaseSchema {
    * @return
    *   The index/constraint/pkey identifier
    */
-  protected function ensureIdentifiersLength($identifier) {
+  protected function getIdentifierHash($identifier) {
     $args = func_get_args();
     $info = $this->getPrefixInfo($identifier);
     $args[0] = $info['table'];
@@ -251,11 +251,11 @@ protected function createTableSql($name, $table) {
 
     $sql_keys = [];
     if (isset($table['primary key']) && is_array($table['primary key'])) {
-      $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
+      $sql_keys[] = 'CONSTRAINT ' . $this->getIdentifierHash($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
     }
     if (isset($table['unique keys']) && is_array($table['unique keys'])) {
       foreach ($table['unique keys'] as $key_name => $key) {
-        $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
+        $sql_keys[] = 'CONSTRAINT ' . $this->getIdentifierHash($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
       }
     }
 
@@ -491,7 +491,7 @@ public function renameTable($table, $new_name) {
       // Get the index type by suffix, e.g. idx/key/pkey
       $index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1);
 
-      // If the index is already rewritten by ensureIdentifiersLength() to not
+      // If the index is already rewritten by getIdentifierHash() to not
       // exceed the 63 chars limit of PostgreSQL, we need to take care of that.
       // Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx).
       if (strpos($index->indexname, 'drupal_') !== FALSE) {
@@ -500,12 +500,12 @@ public function renameTable($table, $new_name) {
       }
       else {
         // Make sure to remove the suffix from index names, because
-        // $this->ensureIdentifiersLength() will add the suffix again and thus
+        // $this->getIdentifierHash() will add the suffix again and thus
         // would result in a wrong index name.
         preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches);
         $index_name = $matches[1];
       }
-      $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . '');
+      $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->getIdentifierHash($new_name, $index_name, $index_type) . '');
     }
 
     // Ensure the new table name does not include schema syntax.
@@ -613,7 +613,7 @@ public function fieldSetNoDefault($table, $field) {
 
   public function indexExists($table, $name) {
     // Details http://www.postgresql.org/docs/9.1/interactive/view-pg-indexes.html
-    $index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
+    $index_name = $this->getIdentifierHash($table, $name, 'idx');
     // Remove leading and trailing quotes because the index name is in a WHERE
     // clause and not used as an identifier.
     $index_name = str_replace('"', '', $index_name);
@@ -632,7 +632,7 @@ public function indexExists($table, $name) {
    *   TRUE if the constraint exists, FALSE otherwise.
    */
   public function constraintExists($table, $name) {
-    // ::ensureIdentifiersLength() expects three parameters, although not
+    // ::getIdentifierHash() expects three parameters, although not
     // explicitly stated in its signature, thus we split our constraint name in
     // a proper name and a suffix.
     if ($name == 'pkey') {
@@ -644,7 +644,7 @@ public function constraintExists($table, $name) {
       $suffix = substr($name, $pos + 2);
       $name = substr($name, 0, $pos);
     }
-    $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
+    $constraint_name = $this->getIdentifierHash($table, $name, $suffix);
     // Remove leading and trailing quotes because the index name is in a WHERE
     // clause and not used as an identifier.
     $constraint_name = str_replace('"', '', $constraint_name);
@@ -659,7 +659,7 @@ public function addPrimaryKey($table, $fields) {
       throw new SchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", ['@table' => $table]));
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')');
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->getIdentifierHash($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')');
     $this->resetTableInformation($table);
   }
 
@@ -668,7 +668,7 @@ public function dropPrimaryKey($table) {
       return FALSE;
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey'));
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->getIdentifierHash($table, '', 'pkey'));
     $this->resetTableInformation($table);
     return TRUE;
   }
@@ -681,7 +681,7 @@ public function addUniqueKey($table, $name, $fields) {
       throw new SchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", ['@table' => $table, '@name' => $name]));
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')');
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->getIdentifierHash($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')');
     $this->resetTableInformation($table);
   }
 
@@ -690,7 +690,7 @@ public function dropUniqueKey($table, $name) {
       return FALSE;
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key'));
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->getIdentifierHash($table, $name, 'key'));
     $this->resetTableInformation($table);
     return TRUE;
   }
@@ -715,7 +715,7 @@ public function dropIndex($table, $name) {
       return FALSE;
     }
 
-    $this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx'));
+    $this->connection->query('DROP INDEX ' . $this->getIdentifierHash($table, $name, 'idx'));
     $this->resetTableInformation($table);
     return TRUE;
   }
@@ -830,7 +830,7 @@ public function changeField($table, $field, $field_new, $spec, $new_keys = []) {
   }
 
   protected function _createIndexSql($table, $name, $fields) {
-    $query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} (';
+    $query = 'CREATE INDEX ' . $this->getIdentifierHash($table, $name, 'idx') . ' ON {' . $table . '} (';
     $query .= $this->_createKeySql($fields) . ')';
     return $query;
   }
diff --git a/core/modules/migrate/src/Plugin/migrate/process/MenuLinkParent.php b/core/modules/migrate/src/Plugin/migrate/process/MenuLinkParent.php
index 0cb7c68..7497148 100644
--- a/core/modules/migrate/src/Plugin/migrate/process/MenuLinkParent.php
+++ b/core/modules/migrate/src/Plugin/migrate/process/MenuLinkParent.php
@@ -2,38 +2,68 @@
 
 namespace Drupal\migrate\Plugin\migrate\process;
 
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Menu\MenuLinkManagerInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Url;
+use Drupal\migrate\Plugin\MigratePluginManagerInterface;
 use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\migrate\process\Migration as Lookup;
 use Drupal\migrate\MigrateExecutableInterface;
 use Drupal\migrate\MigrateSkipRowException;
-use Drupal\migrate\Plugin\MigrateProcessInterface;
-use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
 use Drupal\migrate\Row;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * This plugin figures out menu link parent plugin IDs.
+ * Determines the parent of a menu link.
+ *
+ * Available configuration keys:
+ * - 'skip_row': If set, a MigrateSkipRowException is thrown if the parent
+ *   cannot be ascertained. Defaults to TRUE.
+ *
+ * The source is an array of three values:
+ * - parent_id: The numeric ID of the parent menu link, or 0 if the link is at
+ *   the top level of its menu.
+ * - menu_name: The menu name. All links with the same menu name (such as
+ *   'navigation') are part of the same menu.
+ * - parent_link_path: The Drupal path or external URL this link points to.
+ *
+ * Example:
+ *
+ * @code
+ * process:
+ *   parent:
+ *     plugin: menu_link_parent
+ *     source:
+ *       - '20'
+ *       - 'admin'
+ *       - 'admin/structure'
+ * @endcode
+ *
+ * In this example the parent_id of '20' will be looked up and returned from
+ * amongst the already migrated IDs. If it is not found then the
+ * parent_link_path of 'admin/structure' is loaded from the 'admin' menu, the
+ * given menu_name.
+ *
+ * @see \Drupal\migrate\Plugin\MigrateProcessInterface
  *
  * @MigrateProcessPlugin(
  *   id = "menu_link_parent"
  * )
  */
-class MenuLinkParent extends ProcessPluginBase implements ContainerFactoryPluginInterface {
+class MenuLinkParent extends Lookup {
 
   /**
+   * The menu link plugin manager.
+   *
    * @var \Drupal\Core\Menu\MenuLinkManagerInterface
    */
   protected $menuLinkManager;
 
   /**
-   * @var \Drupal\migrate\Plugin\MigrateProcessInterface
-   */
-  protected $migrationPlugin;
-
-  /**
+   * The menu link entity storage handler.
+   *
    * @var \Drupal\Core\Entity\EntityStorageInterface
    */
   protected $menuLinkStorage;
@@ -41,9 +71,21 @@ class MenuLinkParent extends ProcessPluginBase implements ContainerFactoryPlugin
   /**
    * {@inheritdoc}
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateProcessInterface $migration_plugin, MenuLinkManagerInterface $menu_link_manager, EntityStorageInterface $menu_link_storage) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-    $this->migrationPlugin = $migration_plugin;
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MenuLinkManagerInterface $menu_link_manager, EntityStorageInterface $menu_link_storage) {
+    $configuration += [
+      // Skip the row by default if lookup fails.
+      'skip_row' => TRUE,
+      // Find parent menu link IDs already migrated by the current migration.
+      'migration' => $migration->id(),
+    ];
+    parent::__construct(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $migration,
+      $migration_plugin_manager,
+      $process_plugin_manager
+    );
     $this->menuLinkManager = $menu_link_manager;
     $this->menuLinkStorage = $menu_link_storage;
   }
@@ -52,52 +94,79 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
-    $migration_configuration['migration'][] = $migration->id();
     return new static(
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration),
+      $migration,
+      $container->get('plugin.manager.migration'),
+      $container->get('plugin.manager.migrate.process'),
       $container->get('plugin.manager.menu.link'),
-      $container->get('entity.manager')->getStorage('menu_link_content')
+      $container->get('entity_type.manager')->getStorage('menu_link_content')
     );
   }
 
   /**
    * {@inheritdoc}
-   *
-   * Find the parent link GUID.
    */
   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
+
+    // If the parent ID is empty, it's a top-level menu item.
     $parent_id = array_shift($value);
     if (!$parent_id) {
-      // Top level item.
       return '';
     }
+    // Try to look up the parent menu link ID from among the ones that have
+    // already been migrated.
     try {
-      $already_migrated_id = $this
-        ->migrationPlugin
-        ->transform($parent_id, $migrate_executable, $row, $destination_property);
-      if ($already_migrated_id && ($link = $this->menuLinkStorage->load($already_migrated_id))) {
-        return $link->getPluginId();
-      }
+      $parent_id = parent::transform($parent_id, $migrate_executable, $row, $destination_property);
     }
     catch (MigrateSkipRowException $e) {
+      // There are two reasons the exception may have been thrown. Either the
+      // input value was empty -- definitely not the case, since we checked for
+      // that at the top of this method -- or the lookup tried to stub the
+      // parent menu link, but the destination's configuration has no_stub set
+      // to TRUE. In this latter case, we'll just act as if the lookup failed,
+      // and fall back to looking for the parent link using the menu name and
+      // parent link path.
+      //
+      // TODO: Introduce a new exception to be thrown if stubbing fails or is
+      // disallowed, and catch that instead.
+      $parent_id = NULL;
+    }
+    if ($parent_id) {
+      return $this->menuLinkStorage->load($parent_id)->getPluginId();
+    }
 
+    if ($value) {
+    list ($menu_name, $parent_link_path) = $value;
+
+    // If the parent link path is external, Url will be useless because the
+    // link will definitely not correspond to a Drupal route.
+    if (UrlHelper::isExternal($parent_link_path)) {
+      $links = $this->menuLinkStorage->loadByProperties([
+        'menu_name' => $menu_name,
+        'link.uri' => $parent_link_path,
+      ]);
     }
-    if (isset($value[1])) {
-      list($menu_name, $parent_link_path) = $value;
-      $url = Url::fromUserInput("/$parent_link_path");
+    else {
+      $url = Url::fromUserInput('/' . ltrim($parent_link_path, '/'));
       if ($url->isRouted()) {
         $links = $this->menuLinkManager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters(), $menu_name);
-        if (count($links) == 1) {
-          /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
-          $link = reset($links);
-          return $link->getPluginId();
+
         }
       }
+
+      if (!empty($links)) {
+        return reset($links)->getPluginId();
+      }
+    }
+
+    // TODO: Remove this behavior and the skip_row configuration option.
+    if (!empty($this->configuration['skip_row'])) {
+      throw new MigrateSkipRowException();
     }
-    throw new MigrateSkipRowException();
+    return NULL;
   }
 
 }
