diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index 10f01c0ccca4264d990a5236cff06d680ec99268..8a793c197b7aaa0d7f0e7de2963caf264a83c802 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -601,6 +601,12 @@ sms = enabled
 ; store its path in the database (default "none").
 url_shortener = none
 
+; Which method to use for generating the short link key. Options:
+; - base62: Base62-encode the database row ID (insecure, but makes very short URLs)
+; - md5: Create a salted MD5 hash of the URL (DEFAULT: more private, but also longer)
+; ...or any hash algorithm supported by PHP's hash() function.
+url_shortener_key_type = md5
+
 ; Which redirect mechanism to use when shortlinks are resolved.
 ; threshold:1000 (default) : If the URL is shorter than the given size, HTTP is used, else HTML.
 ; html                     : HTML meta redirect after 3 seconds with infobox.
diff --git a/module/VuFind/sql/migrations/pgsql/7.0/001-modify-shortlinks.sql b/module/VuFind/sql/migrations/pgsql/7.0/001-modify-shortlinks.sql
new file mode 100644
index 0000000000000000000000000000000000000000..79b6b187f0a90040255872a4a9cd2af0f8badfdb
--- /dev/null
+++ b/module/VuFind/sql/migrations/pgsql/7.0/001-modify-shortlinks.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "shortlinks"
+  ADD COLUMN hash varchar(32);
+CREATE UNIQUE INDEX shortlinks_hash_idx ON shortlinks (hash);
\ No newline at end of file
diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql
index d4e1340ae7be69e65246a1f6b1cc25e4ab99b1c3..5f3ff8cb8c0780ba379cc3306bf4584a929cafab 100644
--- a/module/VuFind/sql/mysql.sql
+++ b/module/VuFind/sql/mysql.sql
@@ -177,8 +177,10 @@ CREATE TABLE `external_session` (
 CREATE TABLE `shortlinks` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `path` mediumtext NOT NULL,
+  `hash` varchar(32),
   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  PRIMARY KEY (`id`)
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `shortlinks_hash_IDX` USING HASH (`hash`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
 /*!40101 SET character_set_client = @saved_cs_client */;
 
diff --git a/module/VuFind/sql/pgsql.sql b/module/VuFind/sql/pgsql.sql
index 39abd1cbc298764d4e2d35c13e6a6ecd528f37b7..e2c222781419537929642663808830f916dabad8 100644
--- a/module/VuFind/sql/pgsql.sql
+++ b/module/VuFind/sql/pgsql.sql
@@ -100,10 +100,11 @@ DROP TABLE IF EXISTS "shortlinks";
 CREATE TABLE shortlinks (
 id SERIAL,
 path text,
+hash varchar(32),
 created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (id)
 );
-
+CREATE UNIQUE INDEX shortlinks_hash_idx ON shortlinks (hash);
 
 -- --------------------------------------------------------
 
diff --git a/module/VuFind/src/VuFind/Controller/UpgradeController.php b/module/VuFind/src/VuFind/Controller/UpgradeController.php
index 4faa73de038bb2201caaf8047b6ce37c3beb01d1..22b47e5843380f6b6707d1b76f917698ac67a294 100644
--- a/module/VuFind/src/VuFind/Controller/UpgradeController.php
+++ b/module/VuFind/src/VuFind/Controller/UpgradeController.php
@@ -38,6 +38,7 @@ use VuFind\Config\Version;
 use VuFind\Config\Writer;
 use VuFind\Cookie\Container as CookieContainer;
 use VuFind\Cookie\CookieManager;
+use VuFind\Crypt\Base62;
 use VuFind\Date\Converter;
 use VuFind\Db\AdapterFactory;
 use VuFind\Exception\RecordMissing as RecordMissingException;
@@ -551,6 +552,9 @@ class UpgradeController extends AbstractBase
                 return $this->redirect()->toRoute('upgrade-fixduplicatetags');
             }
 
+            // fix shortlinks
+            $this->fixshortlinks();
+
             // Clean up the "VuFind" source, if necessary.
             $this->fixVuFindSourceInDatabase();
         } catch (Exception $e) {
@@ -924,4 +928,39 @@ class UpgradeController extends AbstractBase
             = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
         return $this->forwardTo('Upgrade', 'Home');
     }
+
+    /**
+     * Generate base62 encoding to migrate old shortlinks
+     *
+     * @throws Exception
+     *
+     * @return void
+     */
+    protected function fixshortlinks()
+    {
+        $shortlinksTable = $this->getTable('shortlinks');
+        $base62 = new Base62();
+
+        try {
+            $results = $shortlinksTable->select(['hash' => null]);
+
+            foreach ($results as $result) {
+                $id = $result['id'];
+                $shortlinksTable->update(
+                    ['hash' => $base62->encode($id)],
+                    ['id' => $id]
+                );
+            }
+
+            $this->session->warnings->append(
+                'Added hash value(s) to ' . count($results) . ' short links.'
+            );
+        } catch (Exception $e) {
+            $this->session->warnings->append(
+                'Could not fix hashes in table shortlinks - maybe column ' .
+                'hash is missing? Exception thrown with ' .
+                'message: ' . $e->getMessage()
+            );
+        }
+    }
 }
diff --git a/module/VuFind/src/VuFind/Crypt/Base62.php b/module/VuFind/src/VuFind/Crypt/Base62.php
new file mode 100644
index 0000000000000000000000000000000000000000..bdf3ce04904036c50945a7aab72c90252d3973b4
--- /dev/null
+++ b/module/VuFind/src/VuFind/Crypt/Base62.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Base62 generator
+ *
+ * Class to encode and decode numbers using base62
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2020.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  VuFind\Crypt
+ * @author   Cornelius Amzar <cornelius.amzar@bsz-bw.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Crypt;
+
+use Exception;
+
+/**
+ * Base62 generator
+ *
+ * Class to encode and decode numbers using base62
+ *
+ * @category VuFind
+ * @package  Crypt
+ * @author   Cornelius Amzar <cornelius.amzar@bsz-bw.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class Base62
+{
+    const BASE62_ALPHABET
+        = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    const BASE62_BASE = 62;
+
+    /**
+     * Common base62 encoding function.
+     * Implemented here so we don't need additional PHP modules like bcmath.
+     *
+     * @param string $base10Number Number to encode
+     *
+     * @return string
+     *
+     * @throws Exception
+     */
+    public function encode($base10Number)
+    {
+        $binaryNumber = intval($base10Number);
+        if ($binaryNumber === 0) {
+            throw new Exception('not a base10 number: "' . $base10Number . '"');
+        }
+
+        $base62Number = '';
+        while ($binaryNumber != 0) {
+            $base62Number = self::BASE62_ALPHABET[$binaryNumber % self::BASE62_BASE]
+                . $base62Number;
+            $binaryNumber = intdiv($binaryNumber, self::BASE62_BASE);
+        }
+
+        return ($base62Number == '') ? '0' : $base62Number;
+    }
+
+    /**
+     * Common base62 decoding function.
+     * Implemented here so we don't need additional PHP modules like bcmath.
+     *
+     * @param string $base62Number Number to decode
+     *
+     * @return int
+     *
+     * @throws Exception
+     */
+    public function decode($base62Number)
+    {
+        $binaryNumber = 0;
+        for ($i = 0; $i < strlen($base62Number); ++$i) {
+            $digit = $base62Number[$i];
+            $strpos = strpos(self::BASE62_ALPHABET, $digit);
+            if ($strpos === false) {
+                throw new Exception('not a base62 digit: "' . $digit . '"');
+            }
+
+            $binaryNumber *= self::BASE62_BASE;
+            $binaryNumber += $strpos;
+        }
+        return $binaryNumber;
+    }
+}
diff --git a/module/VuFind/src/VuFind/UrlShortener/Database.php b/module/VuFind/src/VuFind/UrlShortener/Database.php
index 81c09a93eadcbd86a9b57e00c57b3fde422a98e7..388d9b487204ee96379435e37f2c810d3b3eebd0 100644
--- a/module/VuFind/src/VuFind/UrlShortener/Database.php
+++ b/module/VuFind/src/VuFind/UrlShortener/Database.php
@@ -27,6 +27,7 @@
  */
 namespace VuFind\UrlShortener;
 
+use Exception;
 use VuFind\Db\Table\Shortlinks as ShortlinksTable;
 
 /**
@@ -35,14 +36,18 @@ use VuFind\Db\Table\Shortlinks as ShortlinksTable;
  * @category VuFind
  * @package  UrlShortener
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Cornelius Amzar <cornelius.amzar@bsz-bw.de>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
 class Database implements UrlShortenerInterface
 {
-    const BASE62_ALPHABET
-        = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-    const BASE62_BASE = 62;
+    /**
+     * Hash algorithm to use
+     *
+     * @var string
+     */
+    protected $hashAlgorithm;
 
     /**
      * Base URL of current VuFind site
@@ -58,69 +63,149 @@ class Database implements UrlShortenerInterface
      */
     protected $table;
 
+    /**
+     * HMacKey from config
+     *
+     * @var string
+     */
+    protected $salt;
+
+    /**
+     * When using a hash algorithm other than base62, the preferred number of
+     * characters to use from the hash in the URL (more may be used for
+     * disambiguation when necessary).
+     *
+     * @var int
+     */
+    protected $preferredHashLength = 9;
+
+    /**
+     * The maximum allowed hash length (tied to the width of the database hash
+     * column); if we can't generate a unique hash under this length, something
+     * has gone very wrong.
+     *
+     * @var int
+     */
+    protected $maxHashLength = 32;
+
     /**
      * Constructor
      *
-     * @param string          $baseUrl Base URL of current VuFind site
-     * @param ShortlinksTable $table   Shortlinks database table
+     * @param string          $baseUrl       Base URL of current VuFind site
+     * @param ShortlinksTable $table         Shortlinks database table
+     * @param string          $salt          HMacKey from config
+     * @param string          $hashAlgorithm Hash algorithm to use
      */
-    public function __construct(string $baseUrl, ShortlinksTable $table)
-    {
+    public function __construct(
+        string $baseUrl,
+        ShortlinksTable $table,
+        string $salt,
+        string $hashAlgorithm = 'md5'
+    ) {
         $this->baseUrl = $baseUrl;
         $this->table = $table;
+        $this->salt = $salt;
+        $this->hashAlgorithm = $hashAlgorithm;
     }
 
     /**
-     * Common base62 encoding function.
-     * Implemented here so we don't need additional PHP modules like bcmath.
+     * Generate a short hash using the base62 algorithm (and write a row to the
+     * database).
      *
-     * @param string $base10Number Number to encode
+     * @param string $path Path to store in database
      *
      * @return string
+     */
+    protected function getBase62Hash(string $path): string
+    {
+        $this->table->insert(['path' => $path]);
+        $id = $this->table->getLastInsertValue();
+        $row = $this->table->select(['id' => $id])->current();
+        $b62 = new \VuFind\Crypt\Base62();
+        $row->hash = $b62->encode($id);
+        $row->save();
+        return $row->hash;
+    }
+
+    /**
+     * Support method for getGenericHash(): do the work of picking a short version
+     * of the hash and writing to the database as needed.
+     *
+     * @param string $path   Path to store in database
+     * @param string $hash   Hash of $path (generated in getGenericHash)
+     * @param int    $length Minimum number of characters from hash to use for
+     * lookups (may be increased to enforce uniqueness)
      *
-     * @throws \Exception
+     * @return string
      */
-    protected function base62Encode($base10Number)
+    protected function saveAndShortenHash($path, $hash, $length)
     {
-        $binaryNumber = intval($base10Number);
-        if ($binaryNumber === 0) {
-            throw new \Exception('not a base10 number: "' . $base10Number . '"');
+        // Validate hash length:
+        if ($length > $this->maxHashLength) {
+            throw new \Exception(
+                'Could not generate unique hash under ' . $this->maxHashLength
+                . ' characters in length.'
+            );
         }
+        $shorthash = str_pad(substr($hash, 0, $length), $length, '_');
+        $results = $this->table->select(['hash' => $shorthash]);
 
-        $base62Number = '';
-        while ($binaryNumber != 0) {
-            $base62Number = self::BASE62_ALPHABET[$binaryNumber % self::BASE62_BASE]
-                . $base62Number;
-            $binaryNumber = intdiv($binaryNumber, self::BASE62_BASE);
+        // Brand new hash? Create row and return:
+        if ($results->count() == 0) {
+            $this->table->insert(['path' => $path, 'hash' => $shorthash]);
+            return $shorthash;
         }
 
-        return ($base62Number == '') ? '0' : $base62Number;
+        // If we got this far, the hash already exists; let's check if it matches
+        // the path...
+        if ($results->current()['path'] === $path) {
+            return $shorthash;
+        }
+
+        // If we got here, we have encountered an unexpected hash collision. Let's
+        // disambiguate by making it one character longer:
+        return $this->saveAndShortenHash($path, $hash, $length + 1);
     }
 
     /**
-     * Common base62 decoding function.
-     * Implemented here so we don't need additional PHP modules like bcmath.
+     * Generate a short hash using the configured algorithm (and write a row to the
+     * database if the link is new).
      *
-     * @param string $base62Number Number to decode
+     * @param string $path Path to store in database
      *
-     * @return int
+     * @return string
+     */
+    protected function getGenericHash(string $path): string
+    {
+        $hash = hash($this->hashAlgorithm, $path . $this->salt);
+        // Generate short hash within a transaction to avoid odd timing-related
+        // problems:
+        $connection = $this->table->getAdapter()->getDriver()->getConnection();
+        $connection->beginTransaction();
+        $shortHash = $this
+            ->saveAndShortenHash($path, $hash, $this->preferredHashLength);
+        $connection->commit();
+        return $shortHash;
+    }
+
+    /**
+     * Given a URL, create a database entry (if necessary) and return the hash
+     * value for inclusion in the short URL.
      *
-     * @throws \Exception
+     * @param string $url URL
+     *
+     * @return string
      */
-    protected function base62Decode($base62Number)
+    protected function getShortHash(string $url): string
     {
-        $binaryNumber = 0;
-        for ($i = 0; $i < strlen($base62Number); ++$i) {
-            $digit = $base62Number[$i];
-            $strpos = strpos(self::BASE62_ALPHABET, $digit);
-            if ($strpos === false) {
-                throw new \Exception('not a base62 digit: "' . $digit . '"');
-            }
-
-            $binaryNumber *= self::BASE62_BASE;
-            $binaryNumber += $strpos;
-        }
-        return $binaryNumber;
+        $path = str_replace($this->baseUrl, '', $url);
+
+        // We need to handle things differently depending on whether we're
+        // using the legacy base62 algorithm, or a different hash mechanism.
+        $shorthash = $this->hashAlgorithm === 'base62'
+            ? $this->getBase62Hash($path) : $this->getGenericHash($path);
+
+        return $shorthash;
     }
 
     /**
@@ -132,26 +217,23 @@ class Database implements UrlShortenerInterface
      */
     public function shorten($url)
     {
-        $path = str_replace($this->baseUrl, '', $url);
-        $this->table->insert(['path' => $path]);
-        $id = $this->table->getLastInsertValue();
-
-        $shortUrl = $this->baseUrl . '/short/' . $this->base62Encode($id);
-        return $shortUrl;
+        return $this->baseUrl . '/short/' . $this->getShortHash($url);
     }
 
     /**
      * Resolve URL from Database via id.
      *
-     * @param string $id ID to resolve
+     * @param string $input hash
      *
      * @return string
+     *
+     * @throws Exception
      */
-    public function resolve($id)
+    public function resolve($input)
     {
-        $results = $this->table->select(['id' => $this->base62Decode($id)]);
+        $results = $this->table->select(['hash' => $input]);
         if ($results->count() !== 1) {
-            throw new \Exception('Shortlink could not be resolved: ' . $id);
+            throw new Exception('Shortlink could not be resolved: ' . $input);
         }
 
         return $this->baseUrl . $results->current()['path'];
diff --git a/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php b/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php
index 2790b129e76c066f02dd65549eac02a2c4b63ac3..817d525ca44c0e602c2b122fa2883ae5b62d2391 100644
--- a/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php
+++ b/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php
@@ -27,6 +27,7 @@
  */
 namespace VuFind\UrlShortener;
 
+use Exception;
 use Interop\Container\ContainerInterface;
 
 /**
@@ -53,13 +54,20 @@ class DatabaseFactory
         array $options = null
     ) {
         if (!empty($options)) {
-            throw new \Exception('Unexpected options passed to factory.');
+            throw new Exception('Unexpected options passed to factory.');
         }
         $router = $container->get('HttpRouter');
         $baseUrl = $container->get('ViewRenderer')->plugin('serverurl')
             ->__invoke($router->assemble([], ['name' => 'home']));
         $table = $container->get(\VuFind\Db\Table\PluginManager::class)
             ->get('shortlinks');
-        return new $requestedName(rtrim($baseUrl, '/'), $table);
+        $config = $container->get(\VuFind\Config\PluginManager::class)
+            ->get('config');
+        $salt = $config->Security->HMACkey ?? '';
+        if (empty($salt)) {
+            throw new Exception('HMACkey missing from configuration.');
+        }
+        $hashType = $config->Mail->url_shortener_key_type ?? 'md5';
+        return new $requestedName(rtrim($baseUrl, '/'), $table, $salt, $hashType);
     }
 }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/Base62Test.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/Base62Test.php
new file mode 100644
index 0000000000000000000000000000000000000000..a5bc57540eb355c4c85c86ffd29f25f723bf0d26
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/Base62Test.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Base62 Test Class
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2020.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindTest\Crypt;
+
+use VuFind\Crypt\Base62;
+
+/**
+ * Base62 Test Class
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class Base62Test extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Test encoding.
+     *
+     * @param string $input    Input
+     * @param string $expected Expected output
+     *
+     * @dataProvider exampleProvider
+     *
+     * @return void
+     */
+    public function testEncode($input, $expected)
+    {
+        $base62 = new Base62();
+        $this->assertEquals($expected, $base62->encode($input));
+    }
+
+    /**
+     * Test decoding.
+     *
+     * @param string $expected Expected output
+     * @param string $input    Input
+     *
+     * @dataProvider exampleProvider
+     *
+     * @return void
+     */
+    public function testDecode($expected, $input)
+    {
+        $base62 = new Base62();
+        $this->assertEquals($expected, $base62->decode($input));
+    }
+
+    /**
+     * Data provider for tests.
+     *
+     * @return array
+     */
+    public function exampleProvider()
+    {
+        // format: base 10 number, base 62 number
+        return [
+            ['2', '2'],
+            ['6234', '1cY'],
+            ['1437846', '6234'],
+        ];
+    }
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php
index 144b5089bbb111d1539b14c1b02a6b500766fe80..48079e59320be94a0dd8b221cc38ec3f7353053c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php
@@ -27,7 +27,14 @@
  */
 namespace VuFindTest\UrlShortener;
 
+use Exception;
+use PHPUnit\Framework\TestCase;
+use VuFind\Db\Table\Shortlinks;
 use VuFind\UrlShortener\Database;
+use Zend\Db\Adapter\Adapter;
+use Zend\Db\Adapter\Driver\ConnectionInterface;
+use Zend\Db\Adapter\Driver\DriverInterface;
+use Zend\Db\ResultSet;
 
 /**
  * "Database" URL shortener test.
@@ -35,33 +42,34 @@ use VuFind\UrlShortener\Database;
  * @category VuFind
  * @package  Tests
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Cornelius Amzar <cornelius.amzar@bsz-bw.de>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
  */
-class DatabaseTest extends \PHPUnit\Framework\TestCase
+class DatabaseTest extends TestCase
 {
     /**
      * Get the object to test.
      *
-     * @param object $table Database table object/mock
+     * @param  object $table Database table object/mock
      *
      * @return Database
      */
     public function getShortener($table)
     {
-        return new Database('http://foo', $table);
+        return new Database('http://foo', $table, 'RAnD0mVuFindSa!t');
     }
 
     /**
      * Get the mock table object.
      *
-     * @param array $methods Methods to mock.
+     * @param  array $methods Methods to mock.
      *
      * @return object
      */
     public function getMockTable($methods)
     {
-        return $this->getMockBuilder(\VuFind\Db\Table\Shortlinks::class)
+        return $this->getMockBuilder(Shortlinks::class)
             ->disableOriginalConstructor()
             ->setMethods($methods)
             ->getMock();
@@ -71,62 +79,129 @@ class DatabaseTest extends \PHPUnit\Framework\TestCase
      * Test that the shortener works correctly under "happy path."
      *
      * @return void
+     *
+     * @throws Exception
      */
     public function testShortener()
     {
-        $table = $this->getMockTable(['insert', 'getLastInsertValue']);
+        $connection = $this->getMockBuilder(ConnectionInterface::class)
+            ->setMethods(
+                [
+                    'beginTransaction', 'commit', 'connect', 'getResource',
+                    'isConnected', 'getCurrentSchema', 'disconnect', 'rollback',
+                    'execute', 'getLastGeneratedValue'
+                ]
+            )->disableOriginalConstructor()
+            ->getMock();
+        $connection->expects($this->once())->method('beginTransaction');
+        $connection->expects($this->once())->method('commit');
+        $driver = $this->getMockBuilder(DriverInterface::class)
+            ->setMethods(
+                [
+                    'getConnection', 'getDatabasePlatformName', 'checkEnvironment',
+                    'createStatement', 'createResult', 'getPrepareType',
+                    'formatParameterName', 'getLastGeneratedValue'
+                ]
+            )->disableOriginalConstructor()
+            ->getMock();
+        $driver->expects($this->once())->method('getConnection')
+            ->will($this->returnValue($connection));
+        $adapter = $this->getMockBuilder(Adapter::class)
+            ->setMethods(['getDriver'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $adapter->expects($this->once())->method('getDriver')
+            ->will($this->returnValue($driver));
+        $table = $this->getMockTable(['insert', 'select', 'getAdapter']);
         $table->expects($this->once())->method('insert')
-            ->with($this->equalTo(['path' => '/bar']));
-        $table->expects($this->once())->method('getLastInsertValue')
-            ->will($this->returnValue('10'));
+            ->with($this->equalTo(['path' => '/bar', 'hash' => 'a1e7812e2']));
+        $table->expects($this->once())->method('getAdapter')
+            ->will($this->returnValue($adapter));
+        $mockResults = $this->getMockBuilder(ResultSet::class)
+            ->setMethods(['count', 'current'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $mockResults->expects($this->once())->method('count')
+            ->will($this->returnValue(0));
+        $table->expects($this->once())->method('select')
+            ->with($this->equalTo(['hash' => 'a1e7812e2']))
+            ->will($this->returnValue($mockResults));
         $db = $this->getShortener($table);
-        $this->assertEquals('http://foo/short/A', $db->shorten('http://foo/bar'));
+        $this->assertEquals('http://foo/short/a1e7812e2', $db->shorten('http://foo/bar'));
     }
 
     /**
      * Test that resolve is supported.
      *
      * @return void
+     *
+     * @throws Exception
      */
     public function testResolution()
     {
         $table = $this->getMockTable(['select']);
-        $mockResults = $this->getMockBuilder(\Zend\Db\ResultSet::class)
+        $mockResults = $this->getMockBuilder(ResultSet::class)
             ->setMethods(['count', 'current'])
             ->disableOriginalConstructor()
             ->getMock();
         $mockResults->expects($this->once())->method('count')
             ->will($this->returnValue(1));
         $mockResults->expects($this->once())->method('current')
-            ->will($this->returnValue(['path' => '/bar']));
+            ->will($this->returnValue(['path' => '/bar', 'hash' => '8ef580184']));
         $table->expects($this->once())->method('select')
-            ->with($this->equalTo(['id' => 10]))
+            ->with($this->equalTo(['hash' => '8ef580184']))
             ->will($this->returnValue($mockResults));
         $db = $this->getShortener($table);
-        $this->assertEquals('http://foo/bar', $db->resolve('A'));
+        $this->assertEquals('http://foo/bar', $db->resolve('8ef580184'));
     }
 
     /**
      * Test that resolve errors correctly when given bad input
      *
      * @return void
+     *
+     * @throws Exception
      */
     public function testResolutionOfBadInput()
     {
-        $this->expectException(\Exception::class);
-        $this->expectExceptionMessage('Shortlink could not be resolved: B');
+        $this->expectExceptionMessage('Shortlink could not be resolved: abcd12?');
 
         $table = $this->getMockTable(['select']);
-        $mockResults = $this->getMockBuilder(\Zend\Db\ResultSet::class)
+        $mockResults = $this->getMockBuilder(ResultSet::class)
             ->setMethods(['count'])
             ->disableOriginalConstructor()
             ->getMock();
         $mockResults->expects($this->once())->method('count')
             ->will($this->returnValue(0));
         $table->expects($this->once())->method('select')
-            ->with($this->equalTo(['id' => 11]))
+            ->with($this->equalTo(['hash' => 'abcd12?']))
             ->will($this->returnValue($mockResults));
         $db = $this->getShortener($table);
-        $db->resolve('B');
+        $db->resolve('abcd12?');
+    }
+
+    /**
+     * Test that resolve errors correctly when given bad input
+     *
+     * @return void
+     *
+     * @throws Exception
+     */
+    public function testResolutionOfOldIds()
+    {
+        $table = $this->getMockTable(['select']);
+        $mockResults = $this->getMockBuilder(ResultSet::class)
+            ->setMethods(['count', 'current'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $mockResults->expects($this->once())->method('count')
+            ->will($this->returnValue(1));
+        $mockResults->expects($this->once())->method('current')
+            ->will($this->returnValue(['path' => '/bar', 'hash' => 'A']));
+        $table->expects($this->once())->method('select')
+            ->with($this->equalTo(['hash' => 'A']))
+            ->will($this->returnValue($mockResults));
+        $db = $this->getShortener($table);
+        $this->assertEquals('http://foo/bar', $db->resolve('A'));
     }
 }