diff --git a/pokedex/db/load.py b/pokedex/db/load.py
index a13cc3f..079696f 100644
--- a/pokedex/db/load.py
+++ b/pokedex/db/load.py
@@ -17,7 +17,7 @@ from pokedex.defaults import get_default_csv_dir
 from pokedex.db.dependencies import find_dependent_tables
 from pokedex.db.oracle import rewrite_long_table_names
 
-from sqlalchemy import and_
+from sqlalchemy import and_, true
 from sqlalchemy.sql import exists
 
 
@@ -381,6 +381,7 @@ def load(session, tables=[], directory=None, drop_tables=False, verbose=False, s
         session.query(VGPMM).delete()
 
         q = session.query(t.VersionGroup.id, t.PokemonMoveMethod.id)
+        q = q.filter(true())  # Suppress cartesian product warning
         q = q.filter(exists().where(and_(
                 t.PokemonMove.pokemon_move_method_id == t.PokemonMoveMethod.id,
                 t.PokemonMove.version_group_id == t.VersionGroup.id)))
diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py
index fcfcd47..211df50 100644
--- a/pokedex/db/multilang.py
+++ b/pokedex/db/multilang.py
@@ -10,6 +10,18 @@ from sqlalchemy.types import Integer
 
 from pokedex.db import markdown
 
+# Decide which method to use for the default value of the parameter _default_language_id
+_MULTILANG_SESSION_USE_EVENT = False
+try:
+    from sqlalchemy.orm import SessionEvents
+except ImportError:
+    pass
+else:
+    if hasattr(SessionEvents, 'do_orm_execute'):
+        # SQLAlchemy 1.4+
+        from sqlalchemy import event
+        _MULTILANG_SESSION_USE_EVENT = True
+
 class LocalAssociationProxy(AssociationProxy, ColumnOperators):
     """An association proxy for names in the default language
 
@@ -168,11 +180,12 @@ def create_translation_table(_table_name, foreign_class, relation_name,
         primaryjoin=and_(
             Translations.foreign_id == foreign_class.id,
             Translations.local_language_id == bindparam('_default_language_id',
-                value='dummy', type_=Integer, required=True),
+                value='dummy', type_=Integer),
         ),
         foreign_keys=[Translations.foreign_id, Translations.local_language_id],
         uselist=False,
         lazy=relation_lazy,
+        viewonly=True,
     ))
 
     # Add per-column proxies to the original class
@@ -206,14 +219,16 @@ def create_translation_table(_table_name, foreign_class, relation_name,
     # Done
     return Translations
 
-class MultilangQuery(Query):
-    def _execute_and_instances(self, *args, **kwargs):
-        # Set _default_language_id param if it hasn't been set by the time the query is executed.
-        # XXX This is really hacky and we should figure out a cleaner method.
-        if '_default_language_id' not in self._params or self._params['_default_language_id'] == 'dummy':
-            self._params = self._params.copy()
-            self._params['_default_language_id'] = self.session.default_language_id
-        return super(MultilangQuery, self)._execute_and_instances(*args, **kwargs)
+if not _MULTILANG_SESSION_USE_EVENT:
+    # SQLAlchemy 1.4 no longer supports Query._execute_and_instances
+    class MultilangQuery(Query):
+        def _execute_and_instances(self, *args, **kwargs):
+            # Set _default_language_id param if it hasn't been set by the time the query is executed.
+            # XXX This is really hacky and we should figure out a cleaner method.
+            if '_default_language_id' not in self._params or self._params['_default_language_id'] == 'dummy':
+                self._params = self._params.copy()
+                self._params['_default_language_id'] = self.session.default_language_id
+            return super(MultilangQuery, self)._execute_and_instances(*args, **kwargs)
 
 class MultilangSession(Session):
     """A tiny Session subclass that adds support for a default language.
@@ -232,10 +247,19 @@ class MultilangSession(Session):
 
         self.markdown_extension = markdown_extension_class(self)
 
-        kwargs.setdefault('query_cls', MultilangQuery)
+        if not _MULTILANG_SESSION_USE_EVENT:
+            kwargs.setdefault('query_cls', MultilangQuery)
 
         super(MultilangSession, self).__init__(*args, **kwargs)
 
+if _MULTILANG_SESSION_USE_EVENT:
+    @event.listens_for(MultilangSession, 'do_orm_execute')
+    def receive_do_orm_execute(state):
+        # Set _default_language_id param if it hasn't been set by the time the query is executed.
+        # The same hack as above, but for SQLAlchemy 1.4+
+        if state.is_select and state.parameters.get('_default_language_id', 'dummy') == 'dummy':
+            return state.invoke_statement(params={'_default_language_id': state.session.default_language_id})
+
 class MultilangScopedSession(ScopedSession):
     """Dispatches language selection to the attached Session."""
 
diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py
index c37f22d..e30d54b 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables.py
@@ -30,6 +30,7 @@ from functools import partial
 import six
 
 from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, UniqueConstraint
+from sqlalchemy import __version__ as sqla_version
 from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.hybrid import hybrid_property
@@ -39,6 +40,12 @@ from sqlalchemy.types import Boolean, Enum, Integer, SmallInteger, Unicode, Unic
 
 from pokedex.db import markdown, multilang
 
+relationship = partial(relationship, viewonly=True)
+if (1, 3, 17) <= tuple(int(x) for x in sqla_version.split(".")) < (1, 4):
+    # `sync_backref` was introduced in 1.3.17
+    # Since 1.4 it defaults to False if `viewonly` is True
+    relationship = partial(relationship, sync_backref=False)
+
 class TableSuperclass(object):
     """Superclass for declarative tables, to give them some generic niceties
     like stringification.
@@ -2475,7 +2482,8 @@ Experience.growth_rate = relationship(GrowthRate,
 Generation.versions = relationship(Version,
     secondary=VersionGroup.__table__,
     innerjoin=True)
-Generation.main_region = relationship(Region, innerjoin=True)
+Generation.main_region = relationship(Region, innerjoin=True,
+    backref=backref('generation', uselist=False))
 
 
 GrowthRate.max_experience_obj = relationship(Experience,
@@ -2497,14 +2505,13 @@ Item.flavor_text = relationship(ItemFlavorText,
 Item.fling_effect = relationship(ItemFlingEffect,
     backref='items')
 Item.machines = relationship(Machine,
-    order_by=Machine.version_group_id.asc())
+    order_by=Machine.version_group_id.asc(),
+    backref='item')
 Item.category = relationship(ItemCategory,
     innerjoin=True,
     backref=backref('items', order_by=Item.identifier.asc()))
 Item.pocket = association_proxy('category', 'pocket')
 
-ItemCategory.pocket = relationship(ItemPocket, innerjoin=True)
-
 ItemFlavorText.version_group = relationship(VersionGroup,
     innerjoin=True, lazy='joined')
 ItemFlavorText.language = relationship(Language,
@@ -2518,7 +2525,8 @@ ItemGameIndex.generation = relationship(Generation,
 
 ItemPocket.categories = relationship(ItemCategory,
     innerjoin=True,
-    order_by=ItemCategory.identifier.asc())
+    order_by=ItemCategory.identifier.asc(),
+    backref=backref('pocket', innerjoin=True))
 
 
 Location.region = relationship(Region,
@@ -2542,11 +2550,6 @@ LocationGameIndex.generation = relationship(Generation,
     innerjoin=True, lazy='joined')
 
 
-Machine.item = relationship(Item)
-Machine.version_group = relationship(VersionGroup,
-    innerjoin=True, lazy='joined')
-
-
 Move.changelog = relationship(MoveChangelog,
     order_by=MoveChangelog.changed_in_version_group_id.desc(),
     backref=backref('move', innerjoin=True, lazy='joined'))
@@ -2886,7 +2889,6 @@ PokemonSpecies.conquest_evolution = relationship(ConquestPokemonEvolution,
 PokemonSpeciesFlavorText.version = relationship(Version, innerjoin=True, lazy='joined')
 PokemonSpeciesFlavorText.language = relationship(Language, innerjoin=True, lazy='joined')
 
-Region.generation = relationship(Generation, uselist=False)
 Region.version_group_regions = relationship(VersionGroupRegion,
     order_by=VersionGroupRegion.version_group_id.asc(),
     backref='region')
@@ -2950,7 +2952,8 @@ VersionGroup.pokemon_move_methods = relationship(PokemonMoveMethod,
     backref="version_groups")
 VersionGroup.machines = relationship(Machine,
     innerjoin=True,
-    order_by=Machine.machine_number)
+    order_by=Machine.machine_number,
+    backref=backref('version_group', innerjoin=True, lazy='joined'))
 
 
 VersionGroupPokemonMoveMethod.version_group = relationship(VersionGroup,
diff --git a/setup.py b/setup.py
index 16ac667..cb10936 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ setup(
         'pokedex': ['data/csv/*.csv']
     },
     install_requires=[
-        'SQLAlchemy>=1.0,<1.4',
+        'SQLAlchemy>=1.0,<2.0',
         'whoosh>=2.5,<2.7',
         'markdown>=2.4.1,<=2.6.11',
         'construct==2.5.3',