]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- many-to-many support
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 5 Jan 2014 21:46:01 +0000 (16:46 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 5 Jan 2014 21:46:01 +0000 (16:46 -0500)
- tests
- full documentation, changelog, new in 0.9 announcement

doc/build/changelog/changelog_09.rst
doc/build/changelog/migration_09.rst
doc/build/index.rst
doc/build/requirements.txt
lib/sqlalchemy/ext/automap.py

index 97da9e20c28ac2334adbf82c2115093ff267bafb..9e4bf803bf972cb27e5e55ede58f2edafe4ca205 100644 (file)
 .. changelog::
     :version: 0.9.1
 
+    .. change::
+        :tags: feature, orm, extensions
+
+        A new, **experimental** extension :mod:`sqlalchemy.ext.automap` is added.
+        This extension expands upon the functionality of Declarative as well as
+        the :class:`.DeferredReflection` class to produce a base class which
+        automatically generates mapped classes *and relationships* based on
+        table metadata.
+
+        .. seealso::
+
+            :ref:`feature_automap`
+
+            :ref:`automap_toplevel`
+
     .. change::
         :tags: feature, core
 
index 999bd76020589d3685fae7d812f6829ddc39a70f..29e0042bc7984a280d120874f97e2e5535b85b93 100644 (file)
@@ -9,7 +9,7 @@ What's New in SQLAlchemy 0.9?
     and SQLAlchemy version 0.9, which is expected for release
     in late 2013.
 
-    Document last updated: November 24, 2013
+    Document last updated: January 5, 2014
 
 Introduction
 ============
@@ -933,6 +933,68 @@ complement the :class:`.postgresql.HSTORE` type.
 
 :ticket:`2581`
 
+.. _feature_automap:
+
+Automap Extension
+-----------------
+
+A new extension is added in **0.9.1** known as :mod:`sqlalchemy.ext.automap`.  This is an
+**experimental** extension which expands upon the functionality of Declarative
+as well as the :class:`.DeferredReflection` class.  Essentially, the extension
+provides a base class :class:`.AutomapBase` which automatically generates
+mapped classes and relationships between them based on given table metadata.
+
+The :class:`.MetaData` in use normally might be produced via reflection, but
+there is no requirement that reflection is used.   The most basic usage
+illustrates how :mod:`sqlalchemy.ext.automap` is able to deliver mapped
+classes, including relationships, based on a reflected schema::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy.orm import Session
+    from sqlalchemy import create_engine
+
+    Base = automap_base()
+
+    # engine, suppose it has two tables 'user' and 'address' set up
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    # reflect the tables
+    Base.prepare(engine, reflect=True)
+
+    # mapped classes are now created with names matching that of the table
+    # name.
+    User = Base.classes.user
+    Address = Base.classes.address
+
+    session = Session(engine)
+
+    # rudimentary relationships are produced
+    session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
+    session.commit()
+
+    # collection-based relationships are by default named "<classname>_collection"
+    print (u1.address_collection)
+
+Beyond that, the :class:`.AutomapBase` class is a declarative base, and supports
+all the features that declarative does.  The "automapping" feature can be used
+with an existing, explicitly declared schema to generate relationships and
+missing classes only.  Naming schemes and relationship-production routines
+can be dropped in using callable functions.
+
+It is hoped that the :class:`.AutomapBase` system provides a quick
+and modernized solution to the problem that the very famous
+`SQLSoup <https://sqlsoup.readthedocs.org/en/latest/>`_
+also tries to solve, that of generating a quick and rudimentary object
+model from an existing database on the fly.  By addressing the issue strictly
+at the mapper configuration level, and integrating fully with existing
+Declarative class techniques, :class:`.AutomapBase` seeks to provide
+a well-integrated approach to the issue of expediently auto-generating ad-hoc
+mappings.
+
+.. seealso::
+
+    :ref:`automap_toplevel`
+
 Behavioral Improvements
 =======================
 
index 2a26b206c5dccf319d1eca5ce70932e7ca73cc6c..716a83d0e8b570196056c045c64853d7e31af782 100644 (file)
@@ -39,6 +39,7 @@ of Python objects, proceed first to the tutorial.
   :doc:`Declarative Extension <orm/extensions/declarative>` |
   :doc:`Association Proxy <orm/extensions/associationproxy>` |
   :doc:`Hybrid Attributes <orm/extensions/hybrid>` |
+  :doc:`Automap <orm/extensions/automap>` (**new**) |
   :doc:`Mutable Scalars <orm/extensions/mutable>` |
   :doc:`Ordered List <orm/extensions/orderinglist>`
 
index a5b32f735bbff6fed1647a61c436eeee92f6c639..8b98c8e4571b5e539540063931ab34e6ac27e150 100644 (file)
@@ -1,3 +1,3 @@
 mako
 changelog>=0.3.2
-sphinx-paramlinks>=0.1.9
+sphinx-paramlinks>=0.2.0
index 614afcfc4aba33096d5d0e177d8ee2eef38c0cc2..4106e440f8b446f3319f1bae2f2012c9a07f8de8 100644 (file)
@@ -5,8 +5,8 @@
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
 """Define an extension to the :mod:`sqlalchemy.ext.declarative` system
-which automatically generates mapped classes and attributes from a database
-schema, typically one which is reflected.
+which automatically generates mapped classes and relationships from a database
+schema, typically though not necessarily one which is reflected.
 
 .. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`.
 
@@ -16,105 +16,825 @@ schema, typically one which is reflected.
     **experimental** as of 0.9.1.   Featureset and API stability is
     not guaranteed at this time.
 
-Features:
+It is hoped that the :class:`.AutomapBase` system provides a quick
+and modernized solution to the problem that the very famous
+`SQLSoup <https://sqlsoup.readthedocs.org/en/latest/>`_
+also tries to solve, that of generating a quick and rudimentary object
+model from an existing database on the fly.  By addressing the issue strictly
+at the mapper configuration level, and integrating fully with existing
+Declarative class techniques, :class:`.AutomapBase` seeks to provide
+a well-integrated approach to the issue of expediently auto-generating ad-hoc
+mappings.
 
-* The given :class:`.MetaData` structure may or may not be reflected.
-  :mod:`.automap` isn't dependent on this.
 
-* Classes which are known to be present in the :mod:`.automap` structure
-  can be pre-declared with known attributes and settings.
+Basic Use
+=========
 
-* The system integrates with the featureset of :mod:`.declarative`, including
-  support of mixins, abstract bases, interoperability with non-automapped
-  classes.
+The simplest usage is to reflect an existing database into a new model.
+We create a new :class:`.AutomapBase` class in a similar manner as to how
+we create a declarative base class, using :func:`.automap_base`.
+We then call :meth:`.AutomapBase.prepare` on the resulting base class,
+asking it to reflect the schema and produce mappings::
 
-* The system can build out classes for an entire :class:`.MetaData` structure
-  or for individual :class:`.Table` objects.
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy.orm import Session
+    from sqlalchemy import create_engine
 
-* Relationships between classes are generated based on foreign keys, including
-  that simple many-to-many relationships are also detectable.
+    Base = automap_base()
 
-* Hooks are provided for many key points, including:
+    # engine, suppose it has two tables 'user' and 'address' set up
+    engine = create_engine("sqlite:///mydatabase.db")
 
-    * A function which converts the name of table into a mapped class
+    # reflect the tables
+    Base.prepare(engine, reflect=True)
 
-    * A function which receives a :class:`.Column` object to be mapped and
-      produces the element to be part of the mapping.
+    # mapped classes are now created with names by default
+    # matching that of the table name.
+    User = Base.classes.user
+    Address = Base.classes.address
 
-    * A function which receives two classes which should generate a
-      :func:`.relationship` and produces the actual :func:`.relationship`.
+    session = Session(engine)
+
+    # rudimentary relationships are produced
+    session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
+    session.commit()
+
+    # collection-based relationships are by default named "<classname>_collection"
+    print (u1.address_collection)
+
+Above, calling :meth:`.AutomapBase.prepare` while passing along the
+:paramref:`.AutomapBase.prepare.reflect` parameter indicates that the
+:meth:`.MetaData.reflect` method will be called on this declarative base
+classes' :class:`.MetaData` collection; then, each viable
+:class:`.Table` within the :class:`.MetaData` will get a new mapped class
+generated automatically.  The :class:`.ForeignKeyConstraint` objects which
+link the various tables together will be used to produce new, bidirectional
+:func:`.relationship` objects between classes.   The classes and relationships
+follow along a default naming scheme that we can customize.  At this point,
+our basic mapping consisting of related ``User`` and ``Address`` classes is ready
+to use in the traditional way.
+
+Generating Mappings from an Existing MetaData
+=============================================
+
+We can pass a pre-declared :class:`.MetaData` object to :func:`.automap_base`.
+This object can be constructed in any way, including programmatically, from
+a serialized file, or from itself being reflected using :meth:`.MetaData.reflect`.
+Below we illustrate a combination of reflection and explicit table declaration::
+
+    from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    # produce our own MetaData object
+    metadata = MetaData()
+
+    # we can reflect it ourselves from a database, using options
+    # such as 'only' to limit what tables we look at...
+    metadata.reflect(engine, only=['user', 'address'])
+
+    # ... or just define our own Table objects with it (or combine both)
+    Table('user_order', metadata,
+                    Column('id', Integer, primary_key=True),
+                    Column('user_id', ForeignKey('user.id'))
+                )
+
+    # we can then produce a set of mappings from this MetaData.
+    Base = automap_base(metadata=metadata)
+
+    # calling prepare() just sets up mapped classes and relationships.
+    Base.prepare()
+
+    # mapped classes are ready
+    User, Address, Order = Base.classes.user, Base.classes.address, Base.classes.user_order
+
+Specifying Classes Explcitly
+============================
+
+The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined
+explicitly, in a way similar to that of the :class:`.DeferredReflection` class.
+Classes that extend from :class:`.AutomapBase` act like regular declarative
+classes, but are not immediately mapped after their construction, and are instead
+mapped when we call :meth:`.AutomapBase.prepare`.  The :meth:`.AutomapBase.prepare`
+method will make use of the classes we've established based on the table name
+we use.  If our schema contains tables ``user`` and ``address``, we can define
+one or both of the classes to be used::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import create_engine
+
+    # automap base
+    Base = automap_base()
+
+    # pre-declare User for the 'user' table
+    class User(Base):
+        __tablename__ = 'user'
+
+        # override schema elements like Columns
+        user_name = Column('name', String)
+
+        # override relationships too, if desired.
+        # we must use the same name that automap would use for the relationship,
+        # and also must refer to the class name that automap will generate
+        # for "address"
+        address_collection = relationship("address", collection_class=set)
+
+    # reflect
+    engine = create_engine("sqlite:///mydatabase.db")
+    Base.prepare(engine, reflect=True)
+
+    # we still have Address generated from the tablename "address",
+    # but User is the same as Base.classes.User now
+
+    Address = Base.classes.address
+
+    u1 = session.query(User).first()
+    print (u1.address_collection)
+
+    # the backref is still there:
+    a1 = session.query(Address).first()
+    print (a1.user)
+
+Above, one of the more intricate details is that we illustrated overriding
+one of the :func:`.relationship` objects that automap would have created.
+To do this, we needed to make sure the names match up with what automap
+would normally generate, in that the relationship name would be ``User.address_collection``
+and the name of the class referred to, from automap's perspective, is called
+``address``, even though we are referring to it as ``Address`` within our usage
+of this class.
+
+Overriding Naming Schemes
+=========================
+
+:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and
+relationship names based on a schema, which means it has decision points in how
+these names are determined.  These three decision points are provided using
+functions which can be passed to the :meth:`.AutomapBase.prepare` method, and
+are known as :func:`.classname_for_table`,
+:func:`.name_for_scalar_relationship`,
+and :func:`.name_for_collection_relationship`.  Any or all of these
+functions are provided as in the example below, where we use a "camel case"
+scheme for class names and a "pluralizer" for collection names using the
+`Inflect <https://pypi.python.org/pypi/inflect>`_ package::
+
+    import re
+    import inflect
+
+    def camelize_classname(base, tablename, table):
+        "Produce a 'camelized' class name, e.g. "
+        "'words_and_underscores' -> 'WordsAndUnderscores'"
+
+        return str(tablename[0].upper() + \\
+                re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:]))
+
+    _pluralizer = inflect.engine()
+    def pluralize_collection(base, local_cls, referred_cls, constraint):
+        "Produce an 'uncamelized', 'pluralized' class name, e.g. "
+        "'SomeTerm' -> 'some_terms'"
+
+        referred_name = referred_cls.__name__
+        uncamelized = referred_name[0].lower() + \\
+                        re.sub(r'\W',
+                                lambda m: "_%s" % m.group(0).lower(),
+                                referred_name[1:])
+        pluralized = _pluralizer.plural(uncamelized)
+        return pluralized
+
+    from sqlalchemy.ext.automap import automap_base
+
+    Base = automap_base()
+
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    Base.prepare(engine, reflect=True,
+                classname_for_table=camelize_classname,
+                name_for_collection_relationship=pluralize_collection
+        )
+
+From the above mapping, we would now have classes ``User`` and ``Address``,
+where the collection from ``User`` to ``Address`` is called ``User.addresses``::
+
+    User, Address = Base.classes.User, Base.classes.Address
+
+    u1 = User(addresses=[Address(email="foo@bar.com")])
+
+Relationship Detection
+======================
+
+The vast majority of what automap accomplishes is the generation of
+:func:`.relationship` structures based on foreign keys.  The mechanism
+by which this works for many-to-one and one-to-many relationships is as follows:
+
+1. A given :class:`.Table`, known to be mapped to a particular class,
+   is examined for :class:`.ForeignKeyConstraint` objects.
+
+2. From each :class:`.ForeignKeyConstraint`, the remote :class:`.Table`
+   object present is matched up to the class to which it is to be mapped,
+   if any, else it is skipped.
+
+3. As the :class:`.ForeignKeyConstraint` we are examining correponds to a reference
+   from the immediate mapped class,
+   the relationship will be set up as a many-to-one referring to the referred class;
+   a corresponding one-to-many backref will be created on the referred class referring
+   to this class.
+
+4. The names of the relationships are determined using the
+   :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and
+   :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
+   callable functions.  It is important to note that the default relationship
+   naming derives the name from the **the actual class name**.  If you've
+   given a particular class an explicit name by declaring it, or specified an
+   alternate class naming scheme, that's the name from which the relationship
+   name will be derived.
+
+5. The classes are inspected for an existing mapped property matching these
+   names.  If one is detected on one side, but none on the other side, :class:`.AutomapBase`
+   attempts to create a relationship on the missing side, then uses the
+   :paramref:`.relationship.back_populates` parameter in order to point
+   the new relationship to the other side.
+
+6. In the usual case where no relationship is on either side,
+   :meth:`.AutomapBase.prepare` produces a :func:`.relationship` on the "many-to-one"
+   side and matches it to the other using the :paramref:`.relationship.backref`
+   parameter.
+
+7. Production of the :func:`.relationship` and optionally the :func:`.backref`
+   is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship`
+   function, which can be supplied by the end-user in order to augment
+   the arguments passed to :func:`.relationship` or :func:`.backref` or to
+   make use of custom implementations of these functions.
+
+Custom Relationship Arguments
+-----------------------------
+
+The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used
+to add parameters to relationships.  For most cases, we can make use of the
+existing :func:`.automap.generate_relationship` function to return
+the object, after augmenting the given keyword dictionary with our own
+arguments.
+
+Below is an illustration of how to send
+:paramref:`.relationship.cascade` and
+:paramref:`.relationship.passive_deletes`
+options along to all one-to-many relationships::
+
+    from sqlalchemy.ext.automap import generate_relationship
+
+    def _gen_relationship(base, direction, return_fn,
+                                    attrname, local_cls, referred_cls, **kw):
+        if direction is interfaces.ONETOMANY:
+            kw['cascade'] = 'all, delete-orphan'
+            kw['passive_deletes'] = True
+        # make use of the built-in function to actually return
+        # the result.
+        return generate_relationship(base, direction, return_fn,
+                                        attrname, local_cls, referred_cls, **kw)
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import create_engine
+
+    # automap base
+    Base = automap_base()
+
+    engine = create_engine("sqlite:///mydatabase.db")
+    Base.prepare(engine, reflect=True,
+                generate_relationship=_gen_relationship)
+
+Many-to-Many relationships
+--------------------------
+
+:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g.
+those which contain a ``secondary`` argument.  The process for producing these
+is as follows:
+
+1. A given :class:`.Table` is examined for :class:`.ForeignKeyConstraint` objects,
+   before any mapped class has been assigned to it.
+
+2. If the table contains two and exactly two :class:`.ForeignKeyConstraint`
+   objects, and all columns within this table are members of these two
+   :class:`.ForeignKeyConstraint` objects, the table is assumed to be a
+   "secondary" table, and will **not be mapped directly**.
+
+3. The two (or one, for self-referential) external tables to which the :class:`.Table`
+   refers to are matched to the classes to which they will be mapped, if any.
+
+4. If mapped classes for both sides are located, a many-to-many bi-directional
+   :func:`.relationship` / :func:`.backref` pair is created between the two
+   classes.
+
+5. The override logic for many-to-many works the same as that of one-to-many/
+   many-to-one; the :func:`.generate_relationship` function is called upon
+   to generate the strucures and existing attributes will be maintained.
+
+Using Automap with Explicit Declarations
+========================================
+
+As noted previously, automap has no dependency on reflection, and can make
+use of any collection of :class:`.Table` objects within a :class:`.MetaData`
+collection.  From this, it follows that automap can also be used
+generate missing relationships given an otherwise complete model that fully defines
+table metadata::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import Column, Integer, String, ForeignKey
+
+    Base = automap_base()
+
+    class User(Base):
+        __tablename__ = 'user'
+
+        id = Column(Integer, primary_key=True)
+        name = Column(String)
+
+    class Address(Base):
+        __tablename__ = 'address'
+
+        id = Column(Integer, primary_key=True)
+        email = Column(String)
+        user_id = Column(ForeignKey('user.id'))
+
+    # produce relationships
+    Base.prepare()
+
+    # mapping is complete, with "address_collection" and
+    # "user" relationships
+    a1 = Address(email='u1')
+    a2 = Address(email='u2')
+    u1 = User(address_collection=[a1, a2])
+    assert a1.user is u1
+
+Above, given mostly complete ``User`` and ``Address`` mappings, the
+:class:`.ForeignKey` which we defined on ``Address.user_id`` allowed a
+bidirectional relationship pair ``Address.user`` and ``User.address_collection``
+to be generated on the mapped classes.
+
+Note that when subclassing :class:`.AutomapBase`, the :meth:`.AutomapBase.prepare`
+method is required; if not called, the classes we've declared are in an
+un-mapped state.
 
-    * Functions which produce attribute names; given a scalar column,
-      or a class name for a scalar or collection reference, produce an attribute
-      name.
 
 """
-from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
-from sqlalchemy.ext.declarative.base import _DeferredMapperConfig
-from sqlalchemy.schema import ForeignKeyConstraint
-from sqlalchemy.orm import relationship, backref
-from sqlalchemy import util
+from .declarative import declarative_base as _declarative_base
+from .declarative.base import _DeferredMapperConfig
+from ..sql import and_
+from ..schema import ForeignKeyConstraint
+from ..orm import relationship, backref, interfaces
+from .. import util
+
+
+def classname_for_table(base, tablename, table):
+    """Return the class name that should be used, given the name
+    of a table.
+
+    The default implementation is::
+
+        return str(tablename)
+
+    Alternate implementations can be specified using the
+    :paramref:`.AutomapBase.prepare.classname_for_table`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param tablename: string name of the :class:`.Table`.
 
-def _classname_for_table(table):
-    return str(table.name)
+    :param table: the :class:`.Table` object itself.
 
-def automap_base(**kw):
-    Base = declarative_base(**kw)
+    :return: a string class name.
 
-    class BaseThing(DeferredReflection, Base):
-        __abstract__ = True
+     .. note::
 
-        registry = util.Properties({})
+        In Python 2, the string used for the class name **must** be a non-Unicode
+        object, e.g. a ``str()`` object.  The ``.name`` attribute of
+        :class:`.Table` is typically a Python unicode subclass, so the ``str()``
+        function should be applied to this name, after accounting for any non-ASCII
+        characters.
 
-        @classmethod
-        def prepare(cls, engine):
+    """
+    return str(tablename)
+
+def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
+    """Return the attribute name that should be used to refer from one
+    class to another, for a scalar object reference.
+
+    The default implementation is::
+
+        return referred_cls.__name__.lower()
+
+    Alternate implementations can be specified using the
+    :paramref:`.AutomapBase.prepare.name_for_scalar_relationship`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param local_cls: the class to be mapped on the local side.
+
+    :param referred_cls: the class to be mapped on the referring side.
+
+    :param constraint: the :class:`.ForeignKeyConstraint` that is being
+     inspected to produce this relationship.
+
+    """
+    return referred_cls.__name__.lower()
+
+def name_for_collection_relationship(base, local_cls, referred_cls, constraint):
+    """Return the attribute name that should be used to refer from one
+    class to another, for a collection reference.
+
+    The default implementation is::
+
+        return referred_cls.__name__.lower() + "_collection"
+
+    Alternate implementations
+    can be specified using the :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param local_cls: the class to be mapped on the local side.
+
+    :param referred_cls: the class to be mapped on the referring side.
+
+    :param constraint: the :class:`.ForeignKeyConstraint` that is being
+     inspected to produce this relationship.
+
+    """
+    return referred_cls.__name__.lower() + "_collection"
+
+def generate_relationship(base, direction, return_fn, attrname, local_cls, referred_cls, **kw):
+    """Generate a :func:`.relationship` or :func:`.backref` on behalf of two
+    mapped classes.
+
+    An alternate implementation of this function can be specified using the
+    :paramref:`.AutomapBase.prepare.generate_relationship` parameter.
+
+    The default implementation of this function is as follows::
+
+        if return_fn is backref:
+            return return_fn(attrname, **kw)
+        elif return_fn is relationship:
+            return return_fn(referred_cls, **kw)
+        else:
+            raise TypeError("Unknown relationship function: %s" % return_fn)
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param direction: indicate the "direction" of the relationship; this will
+     be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOONE`.
+
+    :param return_fn: the function that is used by default to create the
+     relationship.  This will be either :func:`.relationship` or :func:`.backref`.
+     The :func:`.backref` function's result will be used to produce a new
+     :func:`.relationship` in a second step, so it is critical that user-defined
+     implementations correctly differentiate between the two functions, if
+     a custom relationship function is being used.
+
+    :attrname: the attribute name to which this relationship is being assigned.
+     If the value of :paramref:`.generate_relationship.return_fn` is the
+     :func:`.backref` function, then this name is the name that is being
+     assigned to the backref.
+
+    :param local_cls: the "local" class to which this relationship or backref
+     will be locally present.
+
+    :param referred_cls: the "referred" class to which the relationship or backref
+     refers to.
+
+    :param \**kw: all additional keyword arguments are passed along to the
+     function.
+
+    :return: a :func:`.relationship` or :func:`.backref` construct, as dictated
+     by the :paramref:`.generate_relationship.return_fn` parameter.
+
+    """
+    if return_fn is backref:
+        return return_fn(attrname, **kw)
+    elif return_fn is relationship:
+        return return_fn(referred_cls, **kw)
+    else:
+        raise TypeError("Unknown relationship function: %s" % return_fn)
+
+class AutomapBase(object):
+    """Base class for an "automap" schema.
+
+    The :class:`.AutomapBase` class can be compared to the "declarative base"
+    class that is produced by the :func:`.declarative.declarative_base`
+    function.  In practice, the :class:`.AutomapBase` class is always used
+    as a mixin along with an actual declarative base.
+
+    A new subclassable :class:`.AutomapBase` is typically instantated
+    using the :func:`.automap_base` function.
+
+    .. seealso::
+
+        :ref:`automap_toplevel`
+
+    """
+    __abstract__ = True
+
+    classes = None
+    """An instance of :class:`.util.Properties` containing classes.
+
+    This object behaves much like the ``.c`` collection on a table.  Classes
+    are present under the name they were given, e.g.::
+
+        Base = automap_base()
+        Base.prepare(engine=some_engine, reflect=True)
+
+        User, Address = Base.classes.User, Base.classes.Address
+
+    """
+
+    @classmethod
+    def prepare(cls,
+                engine=None,
+                reflect=False,
+                classname_for_table=classname_for_table,
+                collection_class=list,
+                name_for_scalar_relationship=name_for_scalar_relationship,
+                name_for_collection_relationship=name_for_collection_relationship,
+                generate_relationship=generate_relationship):
+
+        """Extract mapped classes and relationships from the :class:`.MetaData` and
+        perform mappings.
+
+        :param engine: an :class:`.Engine` or :class:`.Connection` with which
+         to perform schema reflection, if specified.
+         If the :paramref:`.AutomapBase.prepare.reflect` argument is False, this
+         object is not used.
+
+        :param reflect: if True, the :meth:`.MetaData.reflect` method is called
+         on the :class:`.MetaData` associated with this :class:`.AutomapBase`.
+         The :class:`.Engine` passed via :paramref:`.AutomapBase.prepare.engine` will
+         be used to perform the reflection if present; else, the :class:`.MetaData`
+         should already be bound to some engine else the operation will fail.
+
+        :param classname_for_table: callable function which will be used to
+         produce new class names, given a table name.  Defaults to
+         :func:`.classname_for_table`.
+
+        :param name_for_scalar_relationship: callable function which will be used
+         to produce relationship names for scalar relationships.  Defaults to
+         :func:`.name_for_scalar_relationship`.
+
+        :param name_for_collection_relationship: callable function which will be used
+         to produce relationship names for collection-oriented relationships.  Defaults to
+         :func:`.name_for_collection_relationship`.
+
+        :param generate_relationship: callable function which will be used to
+         actually generate :func:`.relationship` and :func:`.backref` constructs.
+         Defaults to :func:`.generate_relationship`.
+
+        :param collection_class: the Python collection class that will be used
+         when a new :func:`.relationship` object is created that represents a
+         collection.  Defaults to ``list``.
+
+        """
+        if reflect:
             cls.metadata.reflect(
                         engine,
                         extend_existing=True,
                         autoload_replace=False
                     )
 
-            table_to_map_config = dict(
-                                    (m.local_table, m)
-                                    for m in _DeferredMapperConfig.classes_for_base(cls)
-                                )
-
-            for table in cls.metadata.tables.values():
-                if table not in table_to_map_config:
-                    mapped_cls = type(
-                        _classname_for_table(table),
-                        (BaseThing, ),
-                        {"__table__": table}
-                    )
-                    map_config = _DeferredMapperConfig.config_for_cls(mapped_cls)
-                    table_to_map_config[table] = map_config
+        table_to_map_config = dict(
+                                (m.local_table, m)
+                                for m in _DeferredMapperConfig.classes_for_base(cls)
+                            )
+
+        many_to_many = []
+
+        for table in cls.metadata.tables.values():
+            lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table)
+            if lcl_m2m is not None:
+                many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table))
+            elif not table.primary_key:
+                continue
+            elif table not in table_to_map_config:
+                mapped_cls = type(
+                    classname_for_table(cls, table.name, table),
+                    (cls, ),
+                    {"__table__": table}
+                )
+                map_config = _DeferredMapperConfig.config_for_cls(mapped_cls)
+                cls.classes[map_config.cls.__name__] = mapped_cls
+                table_to_map_config[table] = map_config
+
+        for map_config in table_to_map_config.values():
+            _relationships_for_fks(cls,
+                            map_config,
+                            table_to_map_config,
+                            collection_class,
+                            name_for_scalar_relationship,
+                            name_for_collection_relationship,
+                            generate_relationship)
+
+        for lcl_m2m, rem_m2m, m2m_const, table in many_to_many:
+            _m2m_relationship(cls, lcl_m2m, rem_m2m, m2m_const, table,
+                            table_to_map_config,
+                            collection_class,
+                            name_for_scalar_relationship,
+                            name_for_collection_relationship,
+                            generate_relationship)
+        for map_config in table_to_map_config.values():
+            map_config.map()
+
+
+    _sa_decl_prepare = True
+    """Indicate that the mapping of classes should be deferred.
+
+    The presence of this attribute name indicates to declarative
+    that the call to mapper() should not occur immediately; instead,
+    information about the table and attributes to be mapped are gathered
+    into an internal structure called _DeferredMapperConfig.  These
+    objects can be collected later using classes_for_base(), additional
+    mapping decisions can be made, and then the map() method will actually
+    apply the mapping.
+
+    The only real reason this deferral of the whole
+    thing is needed is to support primary key columns that aren't reflected
+    yet when the class is declared; everything else can theoretically be
+    added to the mapper later.  However, the _DeferredMapperConfig is a
+    nice interface in any case which exists at that not usually exposed point
+    at which declarative has the class and the Table but hasn't called
+    mapper() yet.
 
-            for map_config in table_to_map_config.values():
-                _relationships_for_fks(map_config, table_to_map_config)
-                cls.registry[map_config.cls.__name__] = map_config.cls
-            super(BaseThing, cls).prepare(engine)
+    """
 
+def automap_base(declarative_base=None, **kw):
+    """Produce a declarative automap base.
 
-        @classmethod
-        def _sa_decl_prepare(cls, local_table, engine):
-            pass
+    This function produces a new base class that is a product of the
+    :class:`.AutomapBase` class as well a declarative base produced by
+    :func:`.declarative.declarative_base`.
 
-    return BaseThing
+    All parameters other than ``declarative_base`` are keyword arguments
+    that are passed directly to the :func:`.declarative.declarative_base`
+    function.
 
-def _relationships_for_fks(map_config, table_to_map_config):
+    :param declarative_base: an existing class produced by
+     :func:`.declarative.declarative_base`.  When this is passed, the function
+     no longer invokes :func:`.declarative.declarative_base` itself, and all other
+     keyword arguments are ignored.
+
+    :param \**kw: keyword arguments are passed along to
+     :func:`.declarative.declarative_base`.
+
+    """
+    if declarative_base is None:
+        Base = _declarative_base(**kw)
+    else:
+        Base = declarative_base
+
+    return type(
+                Base.__name__,
+                (AutomapBase, Base,),
+                {"__abstract__": True, "classes": util.Properties({})}
+            )
+
+def _is_many_to_many(automap_base, table):
+    fk_constraints = [const for const in table.constraints
+                    if isinstance(const, ForeignKeyConstraint)]
+    if len(fk_constraints) != 2:
+        return None, None, None
+
+    cols = sum(
+                [[fk.parent for fk in fk_constraint.elements]
+                for fk_constraint in fk_constraints], [])
+
+    if set(cols) != set(table.c):
+        return None, None, None
+
+    return (
+        fk_constraints[0].elements[0].column.table,
+        fk_constraints[1].elements[0].column.table,
+        fk_constraints
+    )
+
+def _relationships_for_fks(automap_base, map_config, table_to_map_config,
+                                collection_class,
+                                name_for_scalar_relationship,
+                                name_for_collection_relationship,
+                                generate_relationship):
     local_table = map_config.local_table
     local_cls = map_config.cls
+
     for constraint in local_table.constraints:
         if isinstance(constraint, ForeignKeyConstraint):
             fks = constraint.elements
             referred_table = fks[0].column.table
-            referred_cls = table_to_map_config[referred_table].cls
+            referred_cfg = table_to_map_config.get(referred_table, None)
+            if referred_cfg is None:
+                continue
+            referred_cls = referred_cfg.cls
+
+            relationship_name = name_for_scalar_relationship(
+                                        automap_base,
+                                        local_cls,
+                                        referred_cls, constraint)
+            backref_name = name_for_collection_relationship(
+                                        automap_base,
+                                        referred_cls,
+                                        local_cls,
+                                        constraint
+                                    )
 
-            map_config.properties[referred_cls.__name__.lower()] = \
-                relationship(referred_cls,
+            create_backref = backref_name not in referred_cfg.properties
+
+            if relationship_name not in map_config.properties:
+                if create_backref:
+                    backref_obj = generate_relationship(automap_base,
+                                        interfaces.ONETOMANY, backref,
+                                        backref_name, referred_cls, local_cls,
+                                        collection_class=collection_class)
+                else:
+                    backref_obj = None
+                map_config.properties[relationship_name] = \
+                    generate_relationship(automap_base,
+                        interfaces.MANYTOONE,
+                        relationship,
+                        relationship_name,
+                        local_cls, referred_cls,
+                        foreign_keys=[fk.parent for fk in constraint.elements],
+                        backref=backref_obj,
+                        remote_side=[fk.column for fk in constraint.elements]
+                    )
+                if not create_backref:
+                    referred_cfg.properties[backref_name].back_populates = relationship_name
+            elif create_backref:
+                referred_cfg.properties[backref_name] = \
+                    generate_relationship(automap_base,
+                        interfaces.ONETOMANY,
+                        relationship,
+                        backref_name,
+                        referred_cls, local_cls,
                         foreign_keys=[fk.parent for fk in constraint.elements],
-                        backref=backref(
-                                local_cls.__name__.lower() + "_collection",
+                        back_populates=relationship_name,
+                        collection_class=collection_class)
+                map_config.properties[relationship_name].back_populates = backref_name
+
+def _m2m_relationship(automap_base, lcl_m2m, rem_m2m, m2m_const, table,
+                            table_to_map_config,
+                            collection_class,
+                            name_for_scalar_relationship,
+                            name_for_collection_relationship,
+                            generate_relationship):
+
+    map_config = table_to_map_config.get(lcl_m2m, None)
+    referred_cfg = table_to_map_config.get(rem_m2m, None)
+    if map_config is None or referred_cfg is None:
+        return
+
+    local_cls = map_config.cls
+    referred_cls = referred_cfg.cls
+
+    relationship_name = name_for_collection_relationship(
+                                automap_base,
+                                local_cls,
+                                referred_cls, m2m_const[0])
+    backref_name = name_for_collection_relationship(
+                                automap_base,
+                                referred_cls,
+                                local_cls,
+                                m2m_const[1]
                             )
-                        )
 
+    create_backref = backref_name not in referred_cfg.properties
+
+    if relationship_name not in map_config.properties:
+        if create_backref:
+            backref_obj = generate_relationship(automap_base,
+                            interfaces.MANYTOMANY,
+                            backref,
+                            backref_name,
+                            referred_cls, local_cls,
+                            collection_class=collection_class
+                            )
+        else:
+            backref_obj = None
+        map_config.properties[relationship_name] = \
+            generate_relationship(automap_base,
+                interfaces.MANYTOMANY,
+                relationship,
+                relationship_name,
+                local_cls, referred_cls,
+                secondary=table,
+                primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements),
+                secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements),
+                backref=backref_obj,
+                collection_class=collection_class
+                )
+        if not create_backref:
+            referred_cfg.properties[backref_name].back_populates = relationship_name
+    elif create_backref:
+        referred_cfg.properties[backref_name] = \
+            generate_relationship(automap_base,
+                interfaces.MANYTOMANY,
+                relationship,
+                backref_name,
+                referred_cls, local_cls,
+                secondary=table,
+                primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements),
+                secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements),
+                back_populates=relationship_name,
+                collection_class=collection_class)
+        map_config.properties[relationship_name].back_populates = backref_name