mappers*. See the next note for this ticket.
[ticket:2526]
+ - [feature] Conflicts between columns on
+ single-inheritance declarative subclasses,
+ with or without using a mixin, can be resolved
+ using a new @declared_attr usage described
+ in the documentation. [ticket:2472]
+
- [feature] *Very limited* support for
inheriting mappers to be GC'ed when the
class itself is deferenced. The mapper
column over that of the superclass, such as querying above
for ``Engineer.id``. Prior to 0.7 this was the reverse.
+.. _declarative_single_table:
+
Single Table Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~
behavior can be disabled by passing an explicit ``exclude_properties``
collection (empty or otherwise) to the ``__mapper_args__``.
+Resolving Column Conflicts
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Note above that the ``primary_language`` and ``golf_swing`` columns
+are "moved up" to be applied to ``Person.__table__``, as a result of their
+declaration on a subclass that has no table of its own. A tricky case
+comes up when two subclasses want to specify *the same* column, as below::
+
+ class Person(Base):
+ __tablename__ = 'people'
+ id = Column(Integer, primary_key=True)
+ discriminator = Column('type', String(50))
+ __mapper_args__ = {'polymorphic_on': discriminator}
+
+ class Engineer(Person):
+ __mapper_args__ = {'polymorphic_identity': 'engineer'}
+ start_date = Column(DateTime)
+
+ class Manager(Person):
+ __mapper_args__ = {'polymorphic_identity': 'manager'}
+ start_date = Column(DateTime)
+
+Above, the ``start_date`` column declared on both ``Engineer`` and ``Manager``
+will result in an error::
+
+ sqlalchemy.exc.ArgumentError: Column 'start_date' on class
+ <class '__main__.Manager'> conflicts with existing
+ column 'people.start_date'
+
+In a situation like this, Declarative can't be sure
+of the intent, especially if the ``start_date`` columns had, for example,
+different types. A situation like this can be resolved by using
+:class:`.declared_attr` to define the :class:`.Column` conditionally, taking
+care to return the **existing column** via the parent ``__table__`` if it already
+exists::
+
+ from sqlalchemy.ext.declarative import declared_attr
+
+ class Person(Base):
+ __tablename__ = 'people'
+ id = Column(Integer, primary_key=True)
+ discriminator = Column('type', String(50))
+ __mapper_args__ = {'polymorphic_on': discriminator}
+
+ class Engineer(Person):
+ __mapper_args__ = {'polymorphic_identity': 'engineer'}
+
+ @declared_attr
+ def start_date(cls):
+ "Start date column, if not present already."
+ return Person.__table__.c.get('start_date', Column(DateTime))
+
+ class Manager(Person):
+ __mapper_args__ = {'polymorphic_identity': 'manager'}
+
+ @declared_attr
+ def start_date(cls):
+ "Start date column, if not present already."
+ return Person.__table__.c.get('start_date', Column(DateTime))
+
+Above, when ``Manager`` is mapped, the ``start_date`` column is
+already present on the ``Person`` class. Declarative lets us return
+that :class:`.Column` as a result in this case, where it knows to skip
+re-assigning the same column. If the mapping is mis-configured such
+that the ``start_date`` column is accidentally re-assigned to a
+different table (such as, if we changed ``Manager`` to be joined
+inheritance without fixing ``start_date``), an error is raised which
+indicates an existing :class:`.Column` is trying to be re-assigned to
+a different owning :class:`.Table`.
+
+.. versionadded:: 0.8 :class:`.declared_attr` can be used on a non-mixin
+ class, and the returned :class:`.Column` or other mapped attribute
+ will be applied to the mapping as any other attribute. Previously,
+ the resulting attribute would be ignored, and also result in a warning
+ being emitted when a subclass was created.
+
+.. versionadded:: 0.8 :class:`.declared_attr`, when used either with a
+ mixin or non-mixin declarative class, can return an existing
+ :class:`.Column` already assigned to the parent :class:`.Table`,
+ to indicate that the re-assignment of the :class:`.Column` should be
+ skipped, however should still be mapped on the target class,
+ in order to resolve duplicate column conflicts.
+
+The same concept can be used with mixin classes (see
+:ref:`declarative_mixins`)::
+
+ class Person(Base):
+ __tablename__ = 'people'
+ id = Column(Integer, primary_key=True)
+ discriminator = Column('type', String(50))
+ __mapper_args__ = {'polymorphic_on': discriminator}
+
+ class HasStartDate(object):
+ @declared_attr
+ def start_date(cls):
+ return cls.__table__.c.get('start_date', Column(DateTime))
+
+ class Engineer(HasStartDate, Person):
+ __mapper_args__ = {'polymorphic_identity': 'engineer'}
+
+ class Manager(HasStartDate, Person):
+ __mapper_args__ = {'polymorphic_identity': 'manager'}
+
+The above mixin checks the local ``__table__`` attribute for the column.
+Because we're using single table inheritance, we're sure that in this case,
+``cls.__table__`` refers to ``People.__table__``. If we were mixing joined-
+and single-table inheritance, we might want our mixin to check more carefully
+if ``cls.__table__`` is really the :class:`.Table` we're looking for.
+
Concrete Table Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~~~
which can't be properly recreated at this level. For columns that
have foreign keys, as well as for the variety of mapper-level constructs
that require destination-explicit context, the
-:func:`~.declared_attr` decorator is provided so that
+:class:`~.declared_attr` decorator is provided so that
patterns common to many classes can be defined as callables::
from sqlalchemy.ext.declarative import declared_attr
the method without the need to copy it.
.. versionchanged:: > 0.6.5
- Rename 0.6.5 ``sqlalchemy.util.classproperty`` into :func:`~.declared_attr`.
+ Rename 0.6.5 ``sqlalchemy.util.classproperty`` into :class:`~.declared_attr`.
-Columns generated by :func:`~.declared_attr` can also be
+Columns generated by :class:`~.declared_attr` can also be
referenced by ``__mapper_args__`` to a limited degree, currently
by ``polymorphic_on`` and ``version_id_col``, by specifying the
classdecorator itself into the dictionary - the declarative extension
__tablename__='test'
id = Column(Integer, primary_key=True)
+
+
Mixing in Relationships
~~~~~~~~~~~~~~~~~~~~~~~
from ...schema import Table, MetaData
from ...orm import synonym as _orm_synonym, mapper,\
- comparable_property
+ comparable_property,\
+ interfaces
from ...orm.util import polymorphic_union, _mapper_or_none
from ... import exc
import weakref
return comparable_property(comparator_factory, fn)
return decorate
-class declared_attr(property):
+class declared_attr(interfaces._MappedAttribute, property):
"""Mark a class-level method as representing the definition of
a mapped property or special declarative member name.
"Can't place __table_args__ on an inherited class "
"with no table."
)
-
# add any columns declared here to the inherited table.
for c in declared_columns:
if c.primary_key:
"class with no table."
)
if c.name in inherited_table.c:
+ if inherited_table.c[c.name] is c:
+ continue
raise exc.ArgumentError(
"Column '%s' on class %s conflicts with "
"existing column '%s'" %
# note here we place the subclass column
# first. See [ticket:1892] for background.
properties[k] = [col] + p.columns
-
result_mapper_args = mapper_args.copy()
result_mapper_args['properties'] = properties
return result_mapper_args
)
-class QueryableAttribute(interfaces._InspectionAttr, interfaces.PropComparator):
+class QueryableAttribute(interfaces._MappedAttribute,
+ interfaces._InspectionAttr,
+ interfaces.PropComparator):
"""Base class for class-bound attributes. """
is_attribute = True
is_attribute = False
is_clause_element = False
-class MapperProperty(_InspectionAttr):
+class _MappedAttribute(object):
+ """Mixin for attributes which should be replaced by mapper-assigned
+ attributes.
+
+ """
+class MapperProperty(_MappedAttribute, _InspectionAttr):
"""Manage the relationship of a ``Mapper`` to a single class
attribute, as well as that attribute as it appears on individual
instances of the class, including attribute instrumentation,
from ..sql import expression, visitors, operators, util as sql_util
from . import instrumentation, attributes, \
exc as orm_exc, events, loading
-from .interfaces import MapperProperty, _InspectionAttr
+from .interfaces import MapperProperty, _InspectionAttr, _MappedAttribute
from .util import _INSTRUMENTOR, _class_to_mapper, \
_state_mapper, class_mapper, \
return result
def _is_userland_descriptor(self, obj):
- if isinstance(obj, (MapperProperty,
- attributes.QueryableAttribute,
+ if isinstance(obj, (_MappedAttribute,
instrumentation.ClassManager,
expression.ColumnElement)):
return False
eq_(sess.query(User).all(), [User(name='u1', address_count=2,
addresses=[Address(email='one'), Address(email='two')])])
- def test_useless_declared_attr_warns_on_subclass(self):
- def go():
- class MyBase(Base):
- __tablename__ = 'foo'
- id = Column(Integer, primary_key=True)
- @declared_attr
- def somecol(cls):
- return Column(Integer)
+ def test_declared_on_base_class(self):
+ class MyBase(Base):
+ __tablename__ = 'foo'
+ id = Column(Integer, primary_key=True)
+ @declared_attr
+ def somecol(cls):
+ return Column(Integer)
- class MyClass(MyBase):
- __tablename__ = 'bar'
- assert_raises_message(
- sa.exc.SAWarning,
- r"Regular \(i.e. not __special__\) attribute 'MyBase.somecol' "
- "uses @declared_attr, but owning class "
- "<class 'test.ext.declarative..*test_basic..*MyBase'> is "
- "mapped - not applying to subclass <class "
- "'test.ext.declarative..*test_basic..*MyClass'>.",
- go
- )
+ class MyClass(MyBase):
+ __tablename__ = 'bar'
+ id = Column(Integer, ForeignKey('foo.id'), primary_key=True)
+
+ # previously, the 'somecol' declared_attr would be ignored
+ # by the mapping and would remain unused. now we take
+ # it as part of MyBase.
+
+ assert 'somecol' in MyBase.__table__.c
+ assert 'somecol' not in MyClass.__table__.c
def test_column(self):
eq_(sess.query(Engineer).filter_by(primary_language='cobol'
).one(), Engineer(name='vlad', primary_language='cobol'))
+ def test_columns_single_inheritance_conflict_resolution(self):
+ """Test that a declared_attr can return the existing column and it will
+ be ignored. this allows conditional columns to be added.
+
+ See [ticket:2472].
+
+ """
+ class Person(Base):
+ __tablename__ = 'person'
+ id = Column(Integer, primary_key=True)
+
+ class Engineer(Person):
+ """single table inheritance"""
+
+ @declared_attr
+ def target_id(cls):
+ return cls.__table__.c.get('target_id',
+ Column(Integer, ForeignKey('other.id'))
+ )
+ @declared_attr
+ def target(cls):
+ return relationship("Other")
+
+ class Manager(Person):
+ """single table inheritance"""
+
+ @declared_attr
+ def target_id(cls):
+ return cls.__table__.c.get('target_id',
+ Column(Integer, ForeignKey('other.id'))
+ )
+ @declared_attr
+ def target(cls):
+ return relationship("Other")
+
+ class Other(Base):
+ __tablename__ = 'other'
+ id = Column(Integer, primary_key=True)
+
+ is_(
+ Engineer.target_id.property.columns[0],
+ Person.__table__.c.target_id
+ )
+ is_(
+ Manager.target_id.property.columns[0],
+ Person.__table__.c.target_id
+ )
+ # do a brief round trip on this
+ Base.metadata.create_all()
+ session = Session()
+ o1, o2 = Other(), Other()
+ session.add_all([
+ Engineer(target=o1),
+ Manager(target=o2),
+ Manager(target=o1)
+ ])
+ session.commit()
+ eq_(session.query(Engineer).first().target, o1)
+
+
def test_joined_from_single(self):
class Company(Base, fixtures.ComparableEntity):
from test.lib.testing import eq_, assert_raises, \
- assert_raises_message
+ assert_raises_message, is_
from sqlalchemy.ext import declarative as decl
import sqlalchemy as sa
from test.lib import testing
from sqlalchemy.ext.declarative import declared_attr
from test.lib import fixtures
+Base = None
+
class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults):
def setup(self):
global Base
assert len(General.bar.prop.columns) == 1
assert Specific.bar.prop is General.bar.prop
+ def test_columns_single_inheritance_conflict_resolution(self):
+ """Test that a declared_attr can return the existing column and it will
+ be ignored. this allows conditional columns to be added.
+
+ See [ticket:2472].
+
+ """
+ class Person(Base):
+ __tablename__ = 'person'
+ id = Column(Integer, primary_key=True)
+
+ class Mixin(object):
+ @declared_attr
+ def target_id(cls):
+ return cls.__table__.c.get('target_id',
+ Column(Integer, ForeignKey('other.id'))
+ )
+
+ @declared_attr
+ def target(cls):
+ return relationship("Other")
+
+ class Engineer(Mixin, Person):
+ """single table inheritance"""
+
+ class Manager(Mixin, Person):
+ """single table inheritance"""
+
+ class Other(Base):
+ __tablename__ = 'other'
+ id = Column(Integer, primary_key=True)
+
+ is_(
+ Engineer.target_id.property.columns[0],
+ Person.__table__.c.target_id
+ )
+ is_(
+ Manager.target_id.property.columns[0],
+ Person.__table__.c.target_id
+ )
+ # do a brief round trip on this
+ Base.metadata.create_all()
+ session = Session()
+ o1, o2 = Other(), Other()
+ session.add_all([
+ Engineer(target=o1),
+ Manager(target=o2),
+ Manager(target=o1)
+ ])
+ session.commit()
+ eq_(session.query(Engineer).first().target, o1)
+
+
def test_columns_joined_table_inheritance(self):
"""Test a column on a mixin with an alternate attribute name,
mapped to a superclass and joined-table inheritance subclass.