From: Mike Bayer Date: Sat, 20 Jul 2013 02:56:34 +0000 (-0400) Subject: - Improved the examples in ``examples/generic_associations``, including X-Git-Tag: rel_0_8_3~85 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=37fc3e3bf8a481e00a54fdc83b0a927dffbba8a7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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. 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. --- diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index a626bb31aa..47cd32c389 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -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 diff --git a/doc/build/orm/extensions/declarative.rst b/doc/build/orm/extensions/declarative.rst index 35895e8df7..671e0e05b8 100644 --- a/doc/build/orm/extensions/declarative.rst +++ b/doc/build/orm/extensions/declarative.rst @@ -10,6 +10,8 @@ API Reference .. autofunction:: declarative_base +.. autofunction:: as_declarative + .. autoclass:: declared_attr .. autofunction:: sqlalchemy.ext.declarative.api._declarative_constructor diff --git a/examples/generic_associations/__init__.py b/examples/generic_associations/__init__.py index b6cb240887..2121e8b603 100644 --- a/examples/generic_associations/__init__.py +++ b/examples/generic_associations/__init__.py @@ -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 `_. +. """ \ No newline at end of file diff --git a/examples/generic_associations/discriminator_on_association.py b/examples/generic_associations/discriminator_on_association.py index 3c170d5c88..48e3a5a561 100644 --- a/examples/generic_associations/discriminator_on_association.py +++ b/examples/generic_associations/discriminator_on_association.py @@ -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 index 0000000000..b92a28f602 --- /dev/null +++ b/examples/generic_associations/generic_fk.py @@ -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 diff --git a/examples/generic_associations/table_per_association.py b/examples/generic_associations/table_per_association.py index e1ff2be5bb..5124abda37 100644 --- a/examples/generic_associations/table_per_association.py +++ b/examples/generic_associations/table_per_association.py @@ -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. diff --git a/examples/generic_associations/table_per_related.py b/examples/generic_associations/table_per_related.py index 693908189f..c12074831f 100644 --- a/examples/generic_associations/table_per_related.py +++ b/examples/generic_associations/table_per_related.py @@ -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 diff --git a/lib/sqlalchemy/ext/declarative/__init__.py b/lib/sqlalchemy/ext/declarative/__init__.py index f8c685da0b..b92adf02b6 100644 --- a/lib/sqlalchemy/ext/declarative/__init__.py +++ b/lib/sqlalchemy/ext/declarative/__init__.py @@ -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', diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index 2f222f6829..9cbe322673 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -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.