.. 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
.. autofunction:: declarative_base
+.. autofunction:: as_declarative
+
.. autoclass:: declared_attr
.. autofunction:: sqlalchemy.ext.declarative.api._declarative_constructor
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
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.
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
"""
__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.
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")
"""
@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):
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
--- /dev/null
+"""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
"""
-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.
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
-Base = declarative_base(cls=Base)
class Address(Base):
"""The Address class.
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.
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
"%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)
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
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',
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:
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.