diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-09-10 16:54:23 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-09-10 16:54:23 -0400 |
commit | bb62d80217e584acf3f899804bb66b19f71205e2 (patch) | |
tree | 72fb88a12bcf660be5408f5acd64518eb3381dbf /lib/sqlalchemy | |
parent | e61a4438493c812990382ec5f1fc46016b319a4c (diff) | |
download | sqlalchemy-bb62d80217e584acf3f899804bb66b19f71205e2.tar.gz |
- New event hook, MapperEvents.after_configured().
Called after a configure() step has completed and
mappers were in fact affected. Theoretically this
event is called once per application, unless new mappings
are constructed after existing ones have been used
already.
- New declarative features:
- __declare_last__() method, establishes an event
listener for the class method that will be called
when mappers are completed with the final "configure"
step.
- __abstract__ flag. The class will not be mapped
at all when this flag is present on the class.
- New helper classes ConcreteBase, AbstractConcreteBase.
Allow concrete mappings using declarative which automatically
set up the "polymorphic_union" when the "configure"
mapper step is invoked.
- The mapper itself has semi-private methods that allow
the "with_polymorphic" selectable to be assigned
to the mapper after it has already been configured.
[ticket:2239]
Diffstat (limited to 'lib/sqlalchemy')
-rwxr-xr-x | lib/sqlalchemy/ext/declarative.py | 211 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/events.py | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 191 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 13 |
4 files changed, 352 insertions, 78 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 3b260a797..200b0d6c6 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -459,10 +459,52 @@ before the class is built:: __table__ = managers __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True} -There is a recipe which allows the above pattern to be executed -using the declarative form, via a special base class that defers -the creation of the mapper. That recipe is available at -`DeclarativeAbstractConcreteBase <http://www.sqlalchemy.org/trac/wiki/UsageRecipes/DeclarativeAbstractConcreteBase>`_ +Using the Concrete Helpers +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +New helper classes released in 0.7.3 provides a simpler pattern for concrete inheritance. +With these objects, the ``__declare_last__`` helper is used to configure the "polymorphic" +loader for the mapper after all subclasses have been declared. + +A basic abstract example of the :class:`.AbstractConcreteBase` class:: + + from sqlalchemy.ext.declarative import AbstractConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + +To have a concrete ``employee`` table, use :class:`.ConcreteBase` instead:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + +Either ``Employee`` base can be used in the normal fashion:: + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + class Engineer(Employee): + __tablename__ = 'engineer' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + engineer_info = Column(String(40)) + __mapper_args__ = {'polymorphic_identity':'engineer', + 'concrete':True} + .. _declarative_mixins: @@ -825,6 +867,44 @@ it as part of ``__table_args__``:: __tablename__ = 'atable' c = Column(Integer,primary_key=True) +Special Directives +================== + +``__declare_last__()`` +~~~~~~~~~~~~~~~~~~~~~~ + +The ``__declare_last__()`` hook, introduced in 0.7.3, allows definition of +a class level function that is automatically called by the :meth:`.MapperEvents.after_configured` +event, which occurs after mappings are assumed to be completed and the 'configure' step +has finished:: + + class MyClass(Base): + @classmethod + def __declare_last__(cls): + "" + # do something with mappings + + +``__abstract__`` +~~~~~~~~~~~~~~~~~~~ + +``__abstract__`` is introduced in 0.7.3 and causes declarative to skip the production +of a table or mapper for the class entirely. A class can be added within a hierarchy +in the same way as mixin (see :ref:`declarative_mixins`), allowing subclasses to extend +just from the special class:: + + class SomeAbstractBase(Base): + __abstract__ = True + + def some_helpful_method(self): + "" + + @declared_attr + def __mapper_args__(cls): + return {"helpful mapper arguments":True} + + class MyMappedClass(SomeAbstractBase): + "" Class Constructor ================= @@ -862,6 +942,8 @@ from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, Comp from sqlalchemy.orm.util import _is_mapped_class from sqlalchemy import util, exc from sqlalchemy.sql import util as sql_util, expression +from sqlalchemy import event +from sqlalchemy.orm.util import polymorphic_union, _mapper_or_none __all__ = 'declarative_base', 'synonym_for', \ @@ -906,6 +988,18 @@ def _as_declarative(cls, classname, dict_): declarative_props = (declared_attr, util.classproperty) for base in cls.__mro__: + _is_declarative_inherits = hasattr(base, '_decl_class_registry') + + if '__declare_last__' in base.__dict__: + @event.listens_for(mapper, "after_configured") + def go(): + cls.__declare_last__() + if '__abstract__' in base.__dict__: + if (base is cls or + (base in cls.__bases__ and not _is_declarative_inherits) + ): + return + class_mapped = _is_mapped_class(base) if class_mapped: parent_columns = base.__table__.c.keys() @@ -1156,8 +1250,8 @@ class DeclarativeMeta(type): def __init__(cls, classname, bases, dict_): if '_decl_class_registry' in cls.__dict__: return type.__init__(cls, classname, bases, dict_) - - _as_declarative(cls, classname, cls.__dict__) + else: + _as_declarative(cls, classname, cls.__dict__) return type.__init__(cls, classname, bases, dict_) def __setattr__(cls, key, value): @@ -1454,3 +1548,108 @@ def _undefer_column_name(key, column): column.key = key if column.name is None: column.name = key + +class ConcreteBase(object): + """A helper class for 'concrete' declarative mappings. + + :class:`.ConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :func:`.MapperEvents.after_configured` event. + + :class:`.ConcreteBase` produces a mapped + table for the class itself. Compare to :class:`.AbstractConcreteBase`, + which does not. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + @classmethod + def _create_polymorphic_union(cls, mappers): + return polymorphic_union(dict( + (mapper.polymorphic_identity, mapper.local_table) + for mapper in mappers + ), 'type', 'pjoin') + + @classmethod + def __declare_last__(cls): + m = cls.__mapper__ + if m.with_polymorphic: + return + mappers = [ sm for sm in [ + _mapper_or_none(klass) + for klass in cls.__subclasses__() + ] if sm is not None] + [m] + pjoin = cls._create_polymorphic_union(mappers) + m._set_with_polymorphic(("*",pjoin)) + m._set_polymorphic_on(pjoin.c.type) + +class AbstractConcreteBase(ConcreteBase): + """A helper class for 'concrete' declarative mappings. + + :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :func:`.MapperEvents.after_configured` event. + + :class:`.AbstractConcreteBase` does not produce a mapped + table for the class itself. Compare to :class:`.ConcreteBase`, + which does. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + __abstract__ = True + + @classmethod + def __declare_last__(cls): + if hasattr(cls, '__mapper__'): + return + table = cls._create_polymorphic_union( + m for m in [ + _mapper_or_none(klass) + for klass in cls.__subclasses__() + ] if m is not None + ) + cls.__mapper__ = m = mapper(cls, table, polymorphic_on=table.c.type) + for scls in cls.__subclasses__(): + sm = _mapper_or_none(scls) + if sm.concrete and cls in scls.__bases__: + sm._set_concrete_base(m) diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 714cc9245..d551ecf7a 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -404,6 +404,21 @@ class MapperEvents(event.Events): """ # TODO: need coverage for this event + def after_configured(self): + """Called after a series of mappers have been configured. + + This corresponds to the :func:`.orm.configure_mappers` call, which + note is usually called automatically as mappings are first + used. + + Theoretically this event is called once per + application, but is actually called any time new mappers + have been affected by a :func:`.orm.configure_mappers` call. If new mappings + are constructed after existing ones have already been used, + this event can be called again. + + """ + def translate_row(self, mapper, context, row): """Perform pre-processing on the given result row and return a new row instance. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 5a2ca6ed4..b1a6b1a33 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -151,17 +151,7 @@ class Mapper(object): self.allow_partial_pks = allow_partial_pks - if with_polymorphic == '*': - self.with_polymorphic = ('*', None) - elif isinstance(with_polymorphic, (tuple, list)): - if isinstance(with_polymorphic[0], (basestring, tuple, list)): - self.with_polymorphic = with_polymorphic - else: - self.with_polymorphic = (with_polymorphic, None) - elif with_polymorphic is not None: - raise sa_exc.ArgumentError("Invalid setting for with_polymorphic") - else: - self.with_polymorphic = None + self._set_with_polymorphic(with_polymorphic) if isinstance(self.local_table, expression._SelectBase): raise sa_exc.InvalidRequestError( @@ -225,10 +215,10 @@ class Mapper(object): local_table = None """The :class:`.Selectable` which this :class:`.Mapper` manages. - + Typically is an instance of :class:`.Table` or :class:`.Alias`. May also be ``None``. - + The "local" table is the selectable that the :class:`.Mapper` is directly responsible for managing from an attribute access and flush perspective. For @@ -239,15 +229,15 @@ class Mapper(object): single-table inheriting mapper, local_table will be ``None``. See also :attr:`~.Mapper.mapped_table`. - + """ mapped_table = None """The :class:`.Selectable` to which this :class:`.Mapper` is mapped. - + Typically an instance of :class:`.Table`, :class:`.Join`, or - :class:`.Alias`. - + :class:`.Alias`. + The "mapped" table is the selectable that the mapper selects from during queries. For non-inheriting mappers, the mapped table is the same as the "local" table. @@ -255,99 +245,99 @@ class Mapper(object): full :class:`.Join` representing full rows for this particular subclass. For single-table inheritance mappers, mapped_table references the base table. - + See also :attr:`~.Mapper.local_table`. - + """ inherits = None """References the :class:`.Mapper` which this :class:`.Mapper` inherits from, if any. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ configured = None """Represent ``True`` if this :class:`.Mapper` has been configured. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + See also :func:`.configure_mappers`. - + """ concrete = None """Represent ``True`` if this :class:`.Mapper` is a concrete inheritance mapper. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ tables = None """An iterable containing the collection of :class:`.Table` objects which this :class:`.Mapper` is aware of. - + If the mapper is mapped to a :class:`.Join`, or an :class:`.Alias` representing a :class:`.Select`, the individual :class:`.Table` objects that comprise the full construct will be represented here. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ primary_key = None """An iterable containing the collection of :class:`.Column` objects which comprise the 'primary key' of the mapped table, from the perspective of this :class:`.Mapper`. - + This list is against the selectable in :attr:`~.Mapper.mapped_table`. In the case of inheriting mappers, some columns may be managed by a superclass mapper. For example, in the case of a :class:`.Join`, the primary key is determined by all of the primary key columns across all tables referenced by the :class:`.Join`. - + The list is also not necessarily the same as the primary key column collection associated with the underlying tables; the :class:`.Mapper` features a ``primary_key`` argument that can override what the :class:`.Mapper` considers as primary key columns. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ class_ = None """The Python class which this :class:`.Mapper` maps. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ class_manager = None """The :class:`.ClassManager` which maintains event listeners and class-bound descriptors for this :class:`.Mapper`. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. """ single = None """Represent ``True`` if this :class:`.Mapper` is a single table - inheritance mapper. - + inheritance mapper. + :attr:`~.Mapper.local_table` will be ``None`` if this flag is set. - - This is a *read only* attribute determined during mapper construction. + + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ non_primary = None @@ -355,35 +345,35 @@ class Mapper(object): mapper, e.g. a mapper that is used only to selet rows but not for persistence management. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ polymorphic_on = None """The :class:`.Column` specified as the ``polymorphic_on`` column for this :class:`.Mapper`, within an inheritance scenario. - + This attribute may also be of other types besides :class:`.Column` in a future SQLAlchemy release. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ polymorphic_map = None """A mapping of "polymorphic identity" identifiers mapped to :class:`.Mapper` instances, within an inheritance scenario. - + The identifiers can be of any type which is comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. - + An inheritance chain of mappers will all reference the same polymorphic map object. The object is used to correlate incoming result rows to target mappers. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. """ @@ -391,32 +381,32 @@ class Mapper(object): polymorphic_identity = None """Represent an identifier which is matched against the :attr:`~.Mapper.polymorphic_on` column during result row loading. - + Used only with inheritance, this object can be of any type which is comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. - - This is a *read only* attribute determined during mapper construction. + + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ base_mapper = None """The base-most :class:`.Mapper` in an inheritance chain. - + In a non-inheriting scenario, this attribute will always be this :class:`.Mapper`. In an inheritance scenario, it references the :class:`.Mapper` which is parent to all other :class:`.Mapper` objects in the inheritance chain. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ columns = None """A collection of :class:`.Column` or other scalar expression objects maintained by this :class:`.Mapper`. - + The collection behaves the same as that of the ``c`` attribute on any :class:`.Table` object, except that only those columns included in this mapping are present, and are keyed based on the attribute name @@ -424,9 +414,9 @@ class Mapper(object): :class:`.Column` itself. Additionally, scalar expressions mapped by :func:`.column_property` are also present here. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ validators = None @@ -530,16 +520,6 @@ class Mapper(object): if self.polymorphic_identity is not None: self.polymorphic_map[self.polymorphic_identity] = self - if self.polymorphic_on is None: - for mapper in self.iterate_to_root(): - # try to set up polymorphic on using - # correesponding_column(); else leave - # as None - if mapper.polymorphic_on is not None: - self.polymorphic_on = \ - self.mapped_table.corresponding_column( - mapper.polymorphic_on) - break else: self._all_tables = set() self.base_mapper = self @@ -553,6 +533,59 @@ class Mapper(object): "Mapper '%s' does not have a mapped_table specified." % self) + def _set_with_polymorphic(self, with_polymorphic): + if with_polymorphic == '*': + self.with_polymorphic = ('*', None) + elif isinstance(with_polymorphic, (tuple, list)): + if isinstance(with_polymorphic[0], (basestring, tuple, list)): + self.with_polymorphic = with_polymorphic + else: + self.with_polymorphic = (with_polymorphic, None) + elif with_polymorphic is not None: + raise sa_exc.ArgumentError("Invalid setting for with_polymorphic") + else: + self.with_polymorphic = None + + if isinstance(self.local_table, expression._SelectBase): + raise sa_exc.InvalidRequestError( + "When mapping against a select() construct, map against " + "an alias() of the construct instead." + "This because several databases don't allow a " + "SELECT from a subquery that does not have an alias." + ) + + if self.with_polymorphic and \ + isinstance(self.with_polymorphic[1], + expression._SelectBase): + self.with_polymorphic = (self.with_polymorphic[0], + self.with_polymorphic[1].alias()) + if self.configured: + self._expire_memoizations() + + def _set_concrete_base(self, mapper): + """Set the given :class:`.Mapper` as the 'inherits' for this :class:`.Mapper`, + assuming this :class:`.Mapper` is concrete and does not already have + an inherits.""" + + assert self.concrete + assert not self.inherits + assert isinstance(mapper, Mapper) + self.inherits = mapper + self.inherits.polymorphic_map.update(self.polymorphic_map) + self.polymorphic_map = self.inherits.polymorphic_map + for mapper in self.iterate_to_root(): + if mapper.polymorphic_on is not None: + mapper._requires_row_aliasing = True + self.batch = self.inherits.batch + self.base_mapper = self.inherits.base_mapper + self.inherits._inheriting_mappers.add(self) + self.passive_updates = self.inherits.passive_updates + self._all_tables = self.inherits._all_tables + + def _set_polymorphic_on(self, polymorphic_on): + self.polymorphic_on = polymorphic_on + self._configure_polymorphic_setter(True) + def _configure_legacy_instrument_class(self): if self.inherits: @@ -818,7 +851,7 @@ class Mapper(object): init=False, setparent=True) - def _configure_polymorphic_setter(self): + def _configure_polymorphic_setter(self, init=False): """Configure an attribute on the mapper representing the 'polymorphic_on' column, if applicable, and not already generated by _configure_properties (which is typical). @@ -834,6 +867,17 @@ class Mapper(object): # but we need it for the base mapper setter = False + if self.polymorphic_on is None: + for mapper in self.iterate_to_root(): + # try to set up polymorphic on using + # correesponding_column(); else leave + # as None + if mapper.polymorphic_on is not None: + self.polymorphic_on = \ + self.mapped_table.corresponding_column( + mapper.polymorphic_on) + break + if self.polymorphic_on is not None: setter = True @@ -861,7 +905,7 @@ class Mapper(object): self._configure_property( col.key, properties.ColumnProperty(col, _instrument=instrument), - init=False, setparent=True) + init=init, setparent=True) polymorphic_key = col.key else: polymorphic_key = self._columntoproperty[self.polymorphic_on].key @@ -2714,6 +2758,7 @@ def configure_mappers(): if not _new_mappers: return + _call_configured = None _COMPILE_MUTEX.acquire() try: global _already_compiling @@ -2744,6 +2789,7 @@ def configure_mappers(): mapper._post_configure_properties() mapper._expire_memoizations() mapper.dispatch.mapper_configured(mapper, mapper.class_) + _call_configured = mapper except: exc = sys.exc_info()[1] if not hasattr(exc, '_configure_failed'): @@ -2755,7 +2801,8 @@ def configure_mappers(): _already_compiling = False finally: _COMPILE_MUTEX.release() - + if _call_configured is not None: + _call_configured.dispatch.after_configured() def reconstructor(fn): """Decorate a method as the 'reconstructor' hook. diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 3dc1f8676..d778bef3d 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -601,6 +601,9 @@ def has_identity(object): return state.has_identity def _is_mapped_class(cls): + """Return True if the given object is a mapped class, + :class:`.Mapper`, or :class:`.AliasedClass`.""" + if isinstance(cls, (AliasedClass, mapperlib.Mapper)): return True if isinstance(cls, expression.ClauseElement): @@ -610,6 +613,16 @@ def _is_mapped_class(cls): return manager and _INSTRUMENTOR in manager.info return False +def _mapper_or_none(cls): + """Return the :class:`.Mapper` for the given class or None if the + class is not mapped.""" + + manager = attributes.manager_of_class(cls) + if manager is not None and _INSTRUMENTOR in manager.info: + return manager.info[_INSTRUMENTOR] + else: + return None + def instance_str(instance): """Return a string describing an instance.""" |