]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Improved the examples in ``examples/generic_associations``, including
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 20 Jul 2013 02:56:34 +0000 (22:56 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 20 Jul 2013 02:57:12 +0000 (22:57 -0400)
that ``discriminator_on_association.py`` makes use of single table
inheritance do the work with the "discriminator".  Also
added a true "generic foreign key" example, which works similarly
to other popular frameworks in that it uses an open-ended integer
to point to any other table, foregoing traditional referential
integrity.  While we don't recommend this pattern, information wants
to be free.  Also in 0.8.3.

- Added a convenience class decorator :func:`.as_declarative`, is
a wrapper for :func:`.declarative_base` which allows an existing base
class to be applied using a nifty class-decorated approach.  Also
in 0.8.3.

doc/build/changelog/changelog_08.rst
doc/build/orm/extensions/declarative.rst
examples/generic_associations/__init__.py
examples/generic_associations/discriminator_on_association.py
examples/generic_associations/generic_fk.py [new file with mode: 0644]
examples/generic_associations/table_per_association.py
examples/generic_associations/table_per_related.py
lib/sqlalchemy/ext/declarative/__init__.py
lib/sqlalchemy/ext/declarative/api.py

index a626bb31aa61cde2ded2cec3e68f27ab3c69b8ce..47cd32c389ddf9270df9f50be94b68c0ea933af7 100644 (file)
@@ -6,6 +6,25 @@
 .. changelog::
     :version: 0.8.3
 
+    .. change::
+        :tags: feature, examples
+
+        Improved the examples in ``examples/generic_associations``, including
+        that ``discriminator_on_association.py`` makes use of single table
+        inheritance do the work with the "discriminator".  Also
+        added a true "generic foreign key" example, which works similarly
+        to other popular frameworks in that it uses an open-ended integer
+        to point to any other table, foregoing traditional referential
+        integrity.  While we don't recommend this pattern, information wants
+        to be free.
+
+    .. change::
+        :tags: feature, orm, declarative
+
+        Added a convenience class decorator :func:`.as_declarative`, is
+        a wrapper for :func:`.declarative_base` which allows an existing base
+        class to be applied using a nifty class-decorated approach.
+
     .. change::
         :tags: bug, orm
         :tickets: 2786
index 35895e8df74650058d87577d98077553ad8d2344..671e0e05b8992fc411a5afaa235f7d2d5b8cbb00 100644 (file)
@@ -10,6 +10,8 @@ API Reference
 
 .. autofunction:: declarative_base
 
+.. autofunction:: as_declarative
+
 .. autoclass:: declared_attr
 
 .. autofunction:: sqlalchemy.ext.declarative.api._declarative_constructor
index b6cb2408875ab4473808e8a0beb273c79ec77074..2121e8b603530a836590e9d2ef29faf1d4457cb5 100644 (file)
@@ -16,10 +16,12 @@ The configurations include:
   table per association.
 * ``discriminator_on_association.py`` - shared collection table and shared
   association table, including a discriminator column.
+* ``generic_fk.py`` - imitates the approach taken by popular frameworks such
+  as Django and Ruby on Rails to create a so-called "generic foreign key".
 
-The ``discriminator_on_association.py`` script in particular is a modernized
-version of the "polymorphic associations" example present in older versions of
-SQLAlchemy, originally from the blog post at
-http://techspot.zzzeek.org/2007/05/29/polymorphic-associations-with-sqlalchemy/.
+The ``discriminator_on_association.py`` and ``generic_fk.py`` scripts
+are modernized versions of recipes presented in the 2007 blog post
+`Polymorphic Associations with SQLAlchemy <http://techspot.zzzeek.org/2007/05/29/polymorphic-associations-with-sqlalchemy/>`_.
+.
 
 """
\ No newline at end of file
index 3c170d5c889cddb25de3c27d3f5109911f59e104..48e3a5a56181183fec9c4dfeb3b6294ce739daa7 100644 (file)
@@ -3,26 +3,29 @@
 The HasAddresses mixin will provide a relationship
 to the fixed Address table based on a fixed association table.
 
-The association table will also contain a "discriminator"
+The association table contains a "discriminator"
 which determines what type of parent object associates to the
-Address row.
+Address row.  SQLAlchemy's single-table-inheritance feature is used
+to target different association types.
 
 This is a "polymorphic association".   Even though a "discriminator"
 that refers to a particular table is present, the extra association
 table is used so that traditional foreign key constraints may be used.
 
-This configuration has the advantage that a fixed set of tables
-are used, with no extra-table-per-parent needed.   The individual
-Address record can also locate its parent with no need to scan
-amongst many tables.
+This configuration attempts to simulate a so-called "generic foreign key"
+as closely as possible without actually foregoing the use of real
+foreign keys.   Unlike table-per-related and table-per-association,
+it uses a fixed number of tables to serve any number of potential parent
+objects, but is also slightly more complex.
 
 """
-from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.ext.declarative import as_declarative, declared_attr
 from sqlalchemy import create_engine, Integer, Column, \
-                    String, ForeignKey, Table
+                    String, ForeignKey
 from sqlalchemy.orm import Session, relationship, backref
 from sqlalchemy.ext.associationproxy import association_proxy
 
+@as_declarative()
 class Base(object):
     """Base class which provides automated table name
     and surrogate primary key column.
@@ -32,7 +35,6 @@ class Base(object):
     def __tablename__(cls):
         return cls.__name__.lower()
     id = Column(Integer, primary_key=True)
-Base = declarative_base(cls=Base)
 
 class AddressAssociation(Base):
     """Associates a collection of Address objects
@@ -41,22 +43,10 @@ class AddressAssociation(Base):
     """
     __tablename__ = "address_association"
 
-    @classmethod
-    def creator(cls, discriminator):
-        """Provide a 'creator' function to use with
-        the association proxy."""
-
-        return lambda addresses:AddressAssociation(
-                                addresses=addresses,
-                                discriminator=discriminator)
-
     discriminator = Column(String)
     """Refers to the type of parent."""
 
-    @property
-    def parent(self):
-        """Return the parent object."""
-        return getattr(self, "%s_parent" % self.discriminator)
+    __mapper_args__ = {"polymorphic_on": discriminator}
 
 class Address(Base):
     """The Address class.
@@ -65,15 +55,11 @@ class Address(Base):
     single table.
 
     """
-    association_id = Column(Integer,
-                        ForeignKey("address_association.id")
-                    )
+    association_id = Column(Integer, ForeignKey("address_association.id"))
     street = Column(String)
     city = Column(String)
     zip = Column(String)
-    association = relationship(
-                    "AddressAssociation",
-                    backref="addresses")
+    association = relationship("AddressAssociation", backref="addresses")
 
     parent = association_proxy("association", "parent")
 
@@ -89,19 +75,29 @@ class HasAddresses(object):
     """
     @declared_attr
     def address_association_id(cls):
-        return Column(Integer,
-                                ForeignKey("address_association.id"))
+        return Column(Integer, ForeignKey("address_association.id"))
 
     @declared_attr
     def address_association(cls):
-        discriminator = cls.__name__.lower()
-        cls.addresses= association_proxy(
+        name = cls.__name__
+        discriminator = name.lower()
+
+        assoc_cls = type(
+                        "%sAddressAssociation" % name,
+                        (AddressAssociation, ),
+                        dict(
+                            __mapper_args__={
+                                "polymorphic_identity": discriminator
+                            }
+                        )
+                    )
+
+        cls.addresses = association_proxy(
                     "address_association", "addresses",
-                    creator=AddressAssociation.creator(discriminator)
+                    creator=lambda addresses: assoc_cls(addresses=addresses)
                 )
-        return relationship("AddressAssociation",
-                    backref=backref("%s_parent" % discriminator,
-                                        uselist=False))
+        return relationship(assoc_cls,
+                    backref=backref("parent", uselist=False))
 
 
 class Customer(HasAddresses, Base):
@@ -145,4 +141,4 @@ session.commit()
 for customer in session.query(Customer):
     for address in customer.addresses:
         print address
-        print address.parent
\ No newline at end of file
+        print address.parent
diff --git a/examples/generic_associations/generic_fk.py b/examples/generic_associations/generic_fk.py
new file mode 100644 (file)
index 0000000..b92a28f
--- /dev/null
@@ -0,0 +1,141 @@
+"""generic_fk.py
+
+This example will emulate key aspects of the system used by popular
+frameworks such as Django, ROR, etc.
+
+It approaches the issue by bypassing standard referential integrity
+practices, and producing a so-called "generic foreign key", which means
+a database column that is not constrained to refer to any particular table.
+In-application logic is used to determine which table is referenced.
+
+This approach is not in line with SQLAlchemy's usual style, as foregoing
+foreign key integrity means that the tables can easily contain invalid
+references and also have no ability to use in-database cascade functionality.
+
+However, due to the popularity of these systems, as well as that it uses
+the fewest number of tables (which doesn't really offer any "advantage",
+though seems to be comforting to many) this recipe remains in
+high demand, so in the interests of having an easy StackOverflow answer
+queued up, here it is.   The author recommends "table_per_related"
+or "table_per_association" instead of this approach.
+
+.. versionadded:: 0.8.3
+
+"""
+from sqlalchemy.ext.declarative import as_declarative, declared_attr
+from sqlalchemy import create_engine, Integer, Column, \
+                    String, and_
+from sqlalchemy.orm import Session, relationship, foreign, remote, backref
+from sqlalchemy import event
+
+
+@as_declarative()
+class Base(object):
+    """Base class which provides automated table name
+    and surrogate primary key column.
+
+    """
+    @declared_attr
+    def __tablename__(cls):
+        return cls.__name__.lower()
+    id = Column(Integer, primary_key=True)
+
+class Address(Base):
+    """The Address class.
+
+    This represents all address records in a
+    single table.
+
+    """
+    street = Column(String)
+    city = Column(String)
+    zip = Column(String)
+
+    discriminator = Column(String)
+    """Refers to the type of parent."""
+
+    parent_id = Column(Integer)
+    """Refers to the primary key of the parent.
+
+    This could refer to any table.
+    """
+
+    @property
+    def parent(self):
+        """Provides in-Python access to the "parent" by choosing
+        the appropriate relationship.
+
+        """
+        return getattr(self, "parent_%s" % self.discriminator)
+
+    def __repr__(self):
+        return "%s(street=%r, city=%r, zip=%r)" % \
+            (self.__class__.__name__, self.street,
+            self.city, self.zip)
+
+class HasAddresses(object):
+    """HasAddresses mixin, creates a relationship to
+    the address_association table for each parent.
+
+    """
+
+@event.listens_for(HasAddresses, "mapper_configured", propagate=True)
+def setup_listener(mapper, class_):
+    name = class_.__name__
+    discriminator = name.lower()
+    class_.addresses = relationship(Address,
+                        primaryjoin=and_(
+                                        class_.id == foreign(remote(Address.parent_id)),
+                                        Address.discriminator == discriminator
+                                    ),
+                        backref=backref(
+                                "parent_%s" % discriminator,
+                                primaryjoin=remote(class_.id) == foreign(Address.parent_id)
+                                )
+                        )
+    @event.listens_for(class_.addresses, "append")
+    def append_address(target, value, initiator):
+        value.discriminator = discriminator
+
+class Customer(HasAddresses, Base):
+    name = Column(String)
+
+class Supplier(HasAddresses, Base):
+    company_name = Column(String)
+
+engine = create_engine('sqlite://', echo=True)
+Base.metadata.create_all(engine)
+
+session = Session(engine)
+
+session.add_all([
+    Customer(
+        name='customer 1',
+        addresses=[
+            Address(
+                    street='123 anywhere street',
+                    city="New York",
+                    zip="10110"),
+            Address(
+                    street='40 main street',
+                    city="San Francisco",
+                    zip="95732")
+        ]
+    ),
+    Supplier(
+        company_name="Ace Hammers",
+        addresses=[
+            Address(
+                    street='2569 west elm',
+                    city="Detroit",
+                    zip="56785")
+        ]
+    ),
+])
+
+session.commit()
+
+for customer in session.query(Customer):
+    for address in customer.addresses:
+        print(address)
+        print(address.parent)
\ No newline at end of file
index e1ff2be5bb73fd565f48c1d2e271bf88011fdd03..5124abda370541ca0c1c685b8d1b859b5491efac 100644 (file)
@@ -12,11 +12,12 @@ has no dependency on the system.
 
 
 """
-from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.ext.declarative import as_declarative, declared_attr
 from sqlalchemy import create_engine, Integer, Column, \
                     String, ForeignKey, Table
 from sqlalchemy.orm import Session, relationship
 
+@as_declarative()
 class Base(object):
     """Base class which provides automated table name
     and surrogate primary key column.
@@ -26,7 +27,6 @@ class Base(object):
     def __tablename__(cls):
         return cls.__name__.lower()
     id = Column(Integer, primary_key=True)
-Base = declarative_base(cls=Base)
 
 class Address(Base):
     """The Address class.
index 693908189f9efd71967effcaea3eeb7b7fa71d42..c12074831f423682c9eb4f824f114511a9361c97 100644 (file)
@@ -9,11 +9,19 @@ size for one type of parent will have no impact on other types
 of parent.   Navigation between parent and "Address" is simple,
 direct, and bidirectional.
 
+This recipe is the most efficient (speed wise and storage wise)
+and simple of all of them.
+
+The creation of many related tables may seem at first like an issue
+but there really isn't any - the management and targeting of these tables
+is completely automated.
+
 """
-from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.ext.declarative import as_declarative, declared_attr
 from sqlalchemy import create_engine, Integer, Column, String, ForeignKey
 from sqlalchemy.orm import Session, relationship
 
+@as_declarative()
 class Base(object):
     """Base class which provides automated table name
     and surrogate primary key column.
@@ -23,7 +31,6 @@ class Base(object):
     def __tablename__(cls):
         return cls.__name__.lower()
     id = Column(Integer, primary_key=True)
-Base = declarative_base(cls=Base)
 
 class Address(object):
     """Define columns that will be present in each
@@ -54,11 +61,11 @@ class HasAddresses(object):
             "%sAddress" % cls.__name__,
             (Address, Base,),
             dict(
-                __tablename__ = "%s_address" %
+                __tablename__="%s_address" %
                             cls.__tablename__,
-                parent_id = Column(Integer,
-                    ForeignKey("%s.id" % cls.__tablename__)),
-                parent = relationship(cls)
+                parent_id=Column(Integer,
+                            ForeignKey("%s.id" % cls.__tablename__)),
+                parent=relationship(cls)
             )
         )
         return relationship(cls.Address)
@@ -104,4 +111,4 @@ session.commit()
 for customer in session.query(Customer):
     for address in customer.addresses:
         print address
-        print address.parent
\ No newline at end of file
+        print address.parent
index f8c685da0b926f7abb4c5c20cb2589b116e37e77..b92adf02b62e40bf7cdae584c46ec0fdbde96f42 100644 (file)
@@ -1254,7 +1254,7 @@ Mapped instances then make usage of
 from .api import declarative_base, synonym_for, comparable_using, \
     instrument_declarative, ConcreteBase, AbstractConcreteBase, \
     DeclarativeMeta, DeferredReflection, has_inherited_table,\
-    declared_attr
+    declared_attr, as_declarative
 
 
 __all__ = ['declarative_base', 'synonym_for', 'has_inherited_table',
index 2f222f6829b127ea5f6027efb81a50d0778c9816..9cbe322673b62f0667722ad05a7f6f49674f4f87 100644 (file)
@@ -218,6 +218,10 @@ def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
       compatible callable to use as the meta type of the generated
       declarative base class.
 
+    .. seealso::
+
+        :func:`.as_declarative`
+
     """
     lcl_metadata = metadata or MetaData()
     if bind:
@@ -237,6 +241,42 @@ def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
 
     return metaclass(name, bases, class_dict)
 
+def as_declarative(**kw):
+    """
+    Class decorator for :func:`.declarative_base`.
+
+    Provides a syntactical shortcut to the ``cls`` argument
+    sent to :func:`.declarative_base`, allowing the base class
+    to be converted in-place to a "declarative" base::
+
+    from sqlalchemy.ext.declarative import as_declarative
+
+    @as_declarative()
+    class Base(object)
+        @declared_attr
+        def __tablename__(cls):
+            return cls.__name__.lower()
+        id = Column(Integer, primary_key=True)
+
+    class MyMappedClass(Base):
+        # ...
+
+    All keyword arguments passed to :func:`.as_declarative` are passed
+    along to :func:`.declarative_base`.
+
+    .. versionadded:: 0.8.3
+
+    .. seealso::
+
+        :func:`.declarative_base`
+
+    """
+    def decorate(cls):
+        kw['cls'] = cls
+        kw['name'] = cls.__name__
+        return declarative_base(**kw)
+
+    return decorate
 
 class ConcreteBase(object):
     """A helper class for 'concrete' declarative mappings.