From: Mike Bayer Date: Fri, 9 Dec 2011 05:56:12 +0000 (-0500) Subject: - [feature] polymorphic_on now accepts many X-Git-Tag: rel_0_7_4~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d1cc7e7517da26521639fb22826d6e91e51c928e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] polymorphic_on now accepts many new kinds of values: - standalone expressions that aren't otherwise mapped - column_property() objects - string names of any column_property() or attribute name of a mapped Column The docs include an example using the case() construct, which is likely to be a common constructed used here. [ticket:2345] and part of [ticket:2238] --- diff --git a/CHANGES b/CHANGES index 791547e001..83626cf38d 100644 --- a/CHANGES +++ b/CHANGES @@ -6,95 +6,95 @@ CHANGES 0.7.4 ===== - orm - - [bug] Fixed backref behavior when "popping" the - value off of a many-to-one in response to - a removal from a stale one-to-many - the operation - is skipped, since the many-to-one has since - been updated. [ticket:2315] - - - [bug] After some years of not doing this, added - more granularity to the "is X a parent of Y" - functionality, which is used when determining - if the FK on "Y" needs to be "nulled out" as well - as if "Y" should be deleted with delete-orphan - cascade. The test now takes into account the - Python identity of the parent as well its identity - key, to see if the last known parent of Y is - definitely X. If a decision - can't be made, a StaleDataError is raised. The - conditions where this error is raised are fairly - rare, requiring that the previous parent was - garbage collected, and previously - could very well inappropriately update/delete - a record that's since moved onto a new parent, - though there may be some cases where - "silent success" occurred previously that will now - raise in the face of ambiguity. - Expiring "Y" resets the "parent" tracker, meaning - X.remove(Y) could then end up deleting Y even - if X is stale, but this is the same behavior - as before; it's advised to expire X also in that - case. [ticket:2264] - - - [bug] fixed inappropriate evaluation of user-mapped - object in a boolean context within query.get() - [ticket:2310]. Also in 0.6.9. - - - [bug] Added missing comma to PASSIVE_RETURN_NEVER_SET - symbol [ticket:2304] - - - [bug] Cls.column.collate("some collation") now - works. [ticket:1776] Also in 0.6.9 - - - [bug] the value of a composite attribute is now - expired after an insert or update operation, instead - of regenerated in place. This ensures that a - column value which is expired within a flush - will be loaded first, before the composite - is regenerated using that value. [ticket:2309] - - - [bug] The fix in [ticket:2309] also emits the - "refresh" event when the composite value is - loaded on access, even if all column - values were already present, as is appropriate. - This fixes the "mutable" extension which relies - upon the "load" event to ensure the _parents - dictionary is up to date, fixes [ticket:2308]. - Thanks to Scott Torborg for the test case here. - - - [bug] Fixed bug whereby a subclass of a subclass - using concrete inheritance in conjunction with - the new ConcreteBase or AbstractConcreteBase - would fail to apply the subclasses deeper than - one level to the "polymorphic loader" of each - base [ticket:2312] - - - [bug] Fixed bug whereby a subclass of a subclass - using the new AbstractConcreteBase would fail - to acquire the correct "base_mapper" attribute - when the "base" mapper was generated, thereby - causing failures later on. [ticket:2312] - - - [bug] Fixed bug whereby column_property() created - against ORM-level column could be treated as - a distinct entity when producing certain - kinds of joined-inh joins. [ticket:2316] - - - [bug] Fixed the error formatting raised when - a tuple is inadvertently passed to session.query() - [ticket:2297]. Also in 0.6.9. - - - [bug] Calls to query.join() to a single-table - inheritance subclass are now tracked, and - are used to eliminate the additional WHERE.. - IN criterion normally tacked on with single - table inheritance, since the join should - accommodate it. This allows OUTER JOIN - to a single table subclass to produce - the correct results, and overall will produce - fewer WHERE criterion when dealing with - single table inheritance joins. - [ticket:2328] + - [bug] Fixed backref behavior when "popping" the + value off of a many-to-one in response to + a removal from a stale one-to-many - the operation + is skipped, since the many-to-one has since + been updated. [ticket:2315] + + - [bug] After some years of not doing this, added + more granularity to the "is X a parent of Y" + functionality, which is used when determining + if the FK on "Y" needs to be "nulled out" as well + as if "Y" should be deleted with delete-orphan + cascade. The test now takes into account the + Python identity of the parent as well its identity + key, to see if the last known parent of Y is + definitely X. If a decision + can't be made, a StaleDataError is raised. The + conditions where this error is raised are fairly + rare, requiring that the previous parent was + garbage collected, and previously + could very well inappropriately update/delete + a record that's since moved onto a new parent, + though there may be some cases where + "silent success" occurred previously that will now + raise in the face of ambiguity. + Expiring "Y" resets the "parent" tracker, meaning + X.remove(Y) could then end up deleting Y even + if X is stale, but this is the same behavior + as before; it's advised to expire X also in that + case. [ticket:2264] + + - [bug] fixed inappropriate evaluation of user-mapped + object in a boolean context within query.get() + [ticket:2310]. Also in 0.6.9. + + - [bug] Added missing comma to PASSIVE_RETURN_NEVER_SET + symbol [ticket:2304] + + - [bug] Cls.column.collate("some collation") now + works. [ticket:1776] Also in 0.6.9 + + - [bug] the value of a composite attribute is now + expired after an insert or update operation, instead + of regenerated in place. This ensures that a + column value which is expired within a flush + will be loaded first, before the composite + is regenerated using that value. [ticket:2309] + + - [bug] The fix in [ticket:2309] also emits the + "refresh" event when the composite value is + loaded on access, even if all column + values were already present, as is appropriate. + This fixes the "mutable" extension which relies + upon the "load" event to ensure the _parents + dictionary is up to date, fixes [ticket:2308]. + Thanks to Scott Torborg for the test case here. + + - [bug] Fixed bug whereby a subclass of a subclass + using concrete inheritance in conjunction with + the new ConcreteBase or AbstractConcreteBase + would fail to apply the subclasses deeper than + one level to the "polymorphic loader" of each + base [ticket:2312] + + - [bug] Fixed bug whereby a subclass of a subclass + using the new AbstractConcreteBase would fail + to acquire the correct "base_mapper" attribute + when the "base" mapper was generated, thereby + causing failures later on. [ticket:2312] + + - [bug] Fixed bug whereby column_property() created + against ORM-level column could be treated as + a distinct entity when producing certain + kinds of joined-inh joins. [ticket:2316] + + - [bug] Fixed the error formatting raised when + a tuple is inadvertently passed to session.query() + [ticket:2297]. Also in 0.6.9. + + - [bug] Calls to query.join() to a single-table + inheritance subclass are now tracked, and + are used to eliminate the additional WHERE.. + IN criterion normally tacked on with single + table inheritance, since the join should + accommodate it. This allows OUTER JOIN + to a single table subclass to produce + the correct results, and overall will produce + fewer WHERE criterion when dealing with + single table inheritance joins. + [ticket:2328] - [bug] __table_args__ can now be passed as an empty tuple as well as an empty dict. @@ -108,6 +108,20 @@ CHANGES this might be better as an exception but it's not critical either way. [ticket:2325] + - [feature] polymorphic_on now accepts many + new kinds of values: + + - standalone expressions that aren't + otherwise mapped + - column_property() objects + - string names of any column_property() + or attribute name of a mapped Column + + The docs include an example using + the case() construct, which is likely to be + a common constructed used here. + [ticket:2345] and part of [ticket:2238] + - [feature] IdentitySet supports the - operator as the same as difference(), handy when dealing with Session.dirty etc. [ticket:2301] @@ -133,7 +147,7 @@ CHANGES need for a "warning" if a column is attached to a table after it was already used in an expression - the select() construct will now - always produce the correct expression. + always produce the correct expression. There's probably no real-world performance hit here; select() objects are almost always made ad-hoc, and systems that @@ -386,7 +400,7 @@ CHANGES import [ticket:2253] - Reinstated "comparator_factory" argument to - composite(), removed when 0.7 was released. + composite(), removed when 0.7 was released. [ticket:2248] - Fixed bug in query.join() which would occur @@ -415,7 +429,7 @@ CHANGES ArgumentError, rather than UnmappedClassError. [ticket:2196] - - New event hook, MapperEvents.after_configured(). + - 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 @@ -507,7 +521,7 @@ CHANGES long lists of bound parameter sets will be compressed with an informative indicator of the compression taking place. Exception - messages use the same improved formatting. + messages use the same improved formatting. [ticket:2243] - Added optional "sa_pool_key" argument to @@ -598,7 +612,7 @@ CHANGES - The behavior of =/!= when comparing a scalar select to a value will no longer produce IN/NOT IN as of 0.8; this behavior is a little too heavy handed (use in_() if - you want to emit IN) and now emits a deprecation warning. + you want to emit IN) and now emits a deprecation warning. To get the 0.8 behavior immediately and remove the warning, a compiler recipe is given at http://www.sqlalchemy.org/docs/07/dialects/mssql.html#scalar-select-comparisons @@ -759,7 +773,7 @@ CHANGES - Added the same "columns-only" check to mapper.polymorphic_on as used when - receiving user arguments to + receiving user arguments to relationship.order_by, foreign_keys, remote_side, etc. @@ -832,7 +846,7 @@ CHANGES execution. - StatementException wrapping will display the - original exception class in the message. + original exception class in the message. - Failures on connect which raise dbapi.Error will forward the error to dialect.is_disconnect() @@ -844,7 +858,7 @@ CHANGES - sqlite - SQLite dialect no longer strips quotes off of reflected default value, allowing - a round trip CREATE TABLE to work. + a round trip CREATE TABLE to work. This is consistent with other dialects that also maintain the exact form of the default. [ticket:2189] diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst index 5f9a28671a..8d73ceecab 100644 --- a/doc/build/orm/inheritance.rst +++ b/doc/build/orm/inheritance.rst @@ -134,6 +134,8 @@ And that's it. Querying against ``Employee`` will return a combination of ``employees.type`` column with ``engineer``, ``manager``, or ``employee``, as appropriate. +.. _with_polymorphic: + Basic Control of Which Tables are Queried ++++++++++++++++++++++++++++++++++++++++++ diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index b37e813518..118b52f046 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -120,6 +120,8 @@ with the desired key:: 'name': user_table.c.user_name, }) +.. _column_prefix: + Naming All Columns with a Prefix -------------------------------- @@ -188,6 +190,8 @@ See examples of this usage at :ref:`mapper_sql_expressions`. .. autofunction:: column_property +.. _include_exclude_cols: + Mapping a Subset of Table Columns --------------------------------- diff --git a/doc/build/orm/relationships.rst b/doc/build/orm/relationships.rst index 337f9c73f1..f4c43fbc51 100644 --- a/doc/build/orm/relationships.rst +++ b/doc/build/orm/relationships.rst @@ -1171,6 +1171,7 @@ that ``Widget.widget_id`` remains an "autoincrementing" column we specify :func:`.relationship` we must limit those columns considered as part of the foreign key for the purposes of joining and cross-population. +.. _passive_updates: Mutable Primary Keys / Update Cascades --------------------------------------- diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index afb64d4918..f1092d557b 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -787,12 +787,62 @@ def deferred(*columns, **kwargs): def mapper(class_, local_table=None, *args, **params): """Return a new :class:`~.Mapper` object. - - :param class\_: The class to be mapped. - - :param local_table: The table to which the class is mapped, or None if - this mapper inherits from another mapper using concrete table - inheritance. + + This function is typically used behind the scenes + via the Declarative extension. When using Declarative, + many of the usual :func:`.mapper` arguments are handled + by the Declarative extension itself, including ``class_``, + ``local_table``, ``properties``, and ``inherits``. + Other options are passed to :func:`.mapper` using + the ``__mapper_args__`` class variable:: + + class MyClass(Base): + __tablename__ = 'my_table' + id = Column(Integer, primary_key=True) + type = Column(String(50)) + alt = Column("some_alt", Integer) + + __mapper_args__ = { + 'polymorphic_on' : type + } + + + Explicit use of :func:`.mapper` + is often referred to as *classical mapping*. The above + declarative example is equivalent in classical form to:: + + my_table = Table("my_table", metadata, + Column('id', Integer, primary_key=True), + Column('type', String(50)), + Column("some_alt", Integer) + ) + + class MyClass(object): + pass + + mapper(MyClass, my_table, + polymorphic_on=my_table.c.type, + properties={ + 'alt':my_table.c.some_alt + }) + + See also: + + :ref:`classical_mapping` - discussion of direct usage of + :func:`.mapper` + + :param class\_: The class to be mapped. When using Declarative, + this argument is automatically passed as the declared class + itself. + + :param local_table: The :class:`.Table` or other selectable + to which the class is mapped. May be ``None`` if + this mapper inherits from another mapper using single-table + inheritance. When using Declarative, this argument is + automatically passed by the extension, based on what + is configured via the ``__table__`` argument or via the :class:`.Table` + produced as a result of the ``__tablename__`` and :class:`.Column` + arguments present. :param always_refresh: If True, all query operations for this mapped class will overwrite all data within object instances that already @@ -812,31 +862,31 @@ def mapper(class_, local_table=None, *args, **params): particular primary key value. A "partial primary key" can occur if one has mapped to an OUTER JOIN, for example. - :param batch: Indicates that save operations of multiple entities - can be batched together for efficiency. setting to False indicates + :param batch: Defaults to ``True``, indicating that save operations + of multiple entities can be batched together for efficiency. + Setting to False indicates that an instance will be fully saved before saving the next - instance, which includes inserting/updating all table rows - corresponding to the entity as well as calling all - :class:`.MapperExtension` methods corresponding to the save - operation. - - :param column_prefix: A string which will be prepended to the `key` - name of all :class:`.Column` objects when creating - column-based properties from the - given :class:`.Table`. Does not affect explicitly specified - column-based properties + instance. This is used in the extremely rare case that a + :class:`.MapperEvents` listener requires being called + in between individual row persistence operations. + + :param column_prefix: A string which will be prepended + to the mapped attribute name when :class:`.Column` + objects are automatically assigned as attributes to the + mapped class. Does not affect explicitly specified + column-based properties. + + See the section :ref:`column_prefix` for an example. :param concrete: If True, indicates this mapper should use concrete table inheritance with its parent mapper. + + See the section :ref:`concrete_inheritance` for an example. :param exclude_properties: A list or set of string column names to - be excluded from mapping. As of SQLAlchemy 0.6.4, this collection - may also include :class:`.Column` objects. Columns named or present - in this list will not be automatically mapped. Note that neither - this option nor include_properties will allow one to circumvent plan - Python inheritance - if mapped class ``B`` inherits from mapped - class ``A``, no combination of includes or excludes will allow ``B`` - to have fewer properties than its superclass, ``A``. + be excluded from mapping. + + See :ref:`include_exclude_cols` for an example. :param extension: A :class:`.MapperExtension` instance or list of :class:`.MapperExtension` @@ -844,97 +894,183 @@ def mapper(class_, local_table=None, *args, **params): :class:`.Mapper`. **Deprecated.** Please see :class:`.MapperEvents`. :param include_properties: An inclusive list or set of string column - names to map. As of SQLAlchemy 0.6.4, this collection may also - include :class:`.Column` objects in order to disambiguate between - same-named columns in a selectable (such as a - :func:`~.expression.join()`). If this list is not ``None``, columns - present in the mapped table but not named or present in this list - will not be automatically mapped. See also "exclude_properties". - - :param inherits: Another :class:`.Mapper` for which - this :class:`.Mapper` will have an inheritance - relationship with. - + names to map. + + See :ref:`include_exclude_cols` for an example. + + :param inherits: A mapped class or the corresponding :class:`.Mapper` + of one indicating a superclass to which this :class:`.Mapper` + should *inherit* from. The mapped class here must be a subclass of the + other mapper's class. When using Declarative, this argument + is passed automatically as a result of the natural class + hierarchy of the declared classes. + + See also: + + :ref:`inheritance_toplevel` + :param inherit_condition: For joined table inheritance, a SQL - expression (constructed - :class:`.ClauseElement`) which will + expression which will define how the two tables are joined; defaults to a natural join between the two tables. - :param inherit_foreign_keys: When inherit_condition is used and the - condition contains no ForeignKey columns, specify the "foreign" - columns of the join condition in this list. else leave as None. + :param inherit_foreign_keys: When ``inherit_condition`` is used and the + columns present are missing a :class:`.ForeignKey` configuration, + this parameter can be used to specify which columns are "foreign". + In most cases can be left as ``None``. - :param non_primary: Construct a :class:`.Mapper` that will define only - the selection of instances, not their persistence. Any number of - non_primary mappers may be created for a particular class. + :param non_primary: Specify that this :class:`.Mapper` is in addition + to the "primary" mapper, that is, the one used for persistence. + The :class:`.Mapper` created here may be used for ad-hoc + mapping of the class to an alternate selectable, for loading + only. + + The ``non_primary`` feature is rarely needed with modern + usage. :param order_by: A single :class:`.Column` or list of :class:`.Column` objects for which selection operations should use as the default - ordering for entities. Defaults to the OID/ROWID of the table if - any, or the first primary key column of the table. + ordering for entities. By default mappers have no pre-defined + ordering. - :param passive_updates: Indicates UPDATE behavior of foreign keys - when a primary key changes on a joined-table inheritance or other - joined table mapping. + :param passive_updates: Indicates UPDATE behavior of foreign key + columns when a primary key column changes on a joined-table inheritance + mapping. Defaults to ``True``. When True, it is assumed that ON UPDATE CASCADE is configured on the foreign key in the database, and that the database will handle - propagation of an UPDATE from a source column to dependent rows. - Note that with databases which enforce referential integrity (i.e. - PostgreSQL, MySQL with InnoDB tables), ON UPDATE CASCADE is - required for this operation. The relationship() will update the - value of the attribute on related items which are locally present - in the session during a flush. + propagation of an UPDATE from a source column to dependent columns + on joined-table rows. When False, it is assumed that the database does not enforce referential integrity and will not be issuing its own CASCADE - operation for an update. The relationship() will issue the - appropriate UPDATE statements to the database in response to the - change of a referenced key, and items locally present in the - session during a flush will also be refreshed. - - This flag should probably be set to False if primary key changes - are expected and the database in use doesn't support CASCADE (i.e. - SQLite, MySQL MyISAM tables). - - Also see the passive_updates flag on :func:`relationship()`. - - A future SQLAlchemy release will provide a "detect" feature for - this flag. - - :param polymorphic_on: Used with mappers in an inheritance - relationship, a :class:`.Column` which will identify the class/mapper - combination to be used with a particular row. Requires the - ``polymorphic_identity`` value to be set for all mappers in the - inheritance hierarchy. The column specified by ``polymorphic_on`` - is usually a column that resides directly within the base mapper's - mapped table; alternatively, it may be a column that is only - present within the portion of the ``with_polymorphic`` - argument. - - :param polymorphic_identity: A value which will be stored in the - Column denoted by polymorphic_on, corresponding to the class - identity of this mapper. + operation for an update. The :class:`.Mapper` here will + emit an UPDATE statement for the dependent columns during a + primary key change. + + See also: + + :ref:`passive_updates` - description of a similar feature as + used with :func:`.relationship` + + :param polymorphic_on: Specifies the column, attribute, or + SQL expression used to determine the target class for an + incoming row, when inheriting classes are present. + + This value is commonly a :class:`.Column` object that's + present in the mapped :class:`.Table`:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + + __mapper_args__ = { + "polymorphic_on":discriminator, + "polymorphic_identity":"employee" + } + + As of SQLAlchemy 0.7.4, it may also be specified + as a SQL expression, as in this example where we + use the :func:`.case` construct to provide a conditional + approach:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + + __mapper_args__ = { + "polymorphic_on":case([ + (discriminator == "EN", "engineer"), + (discriminator == "MA", "manager"), + ], else_="employee"), + "polymorphic_identity":"employee" + } + + Also as of 0.7.4, it may also refer to any attribute + configured with :func:`.column_property`, or to the + string name of one:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + employee_type = column_property( + case([ + (discriminator == "EN", "engineer"), + (discriminator == "MA", "manager"), + ], else_="employee") + ) + + __mapper_args__ = { + "polymorphic_on":employee_type, + "polymorphic_identity":"employee" + } + + When setting ``polymorphic_on`` to reference an + attribute or expression that's not present in the + locally mapped :class:`.Table`, yet the value + of the discriminator should be persisted to the database, + the value of the + discriminator is not automatically set on new + instances; this must be handled by the user, + either through manual means or via event listeners. + A typical approach to establishing such a listener + looks like:: + + from sqlalchemy import event + from sqlalchemy.orm import object_mapper + + @event.listens_for(Employee, "init", propagate=True) + def set_identity(instance, *arg, **kw): + mapper = object_mapper(instance) + instance.discriminator = mapper.polymorphic_identity + + Where above, we assign the value of ``polymorphic_identity`` + for the mapped class to the ``discriminator`` attribute, + thus persisting the value to the ``discriminator`` column + in the database. + + See also: + + :ref:`inheritance_toplevel` + + :param polymorphic_identity: Specifies the value which + identifies this particular class as returned by the + column expression referred to by the ``polymorphic_on`` + setting. As rows are received, the value corresponding + to the ``polymorphic_on`` column expression is compared + to this value, indicating which subclass should + be used for the newly reconstructed object. :param properties: A dictionary mapping the string names of object - attributes to ``MapperProperty`` instances, which define the - persistence behavior of that attribute. Note that the columns in - the mapped table are automatically converted into - ``ColumnProperty`` instances based on the ``key`` property of each - :class:`.Column` (although they can be overridden using this dictionary). + attributes to :class:`.MapperProperty` instances, which define the + persistence behavior of that attribute. Note that :class:`.Column` + objects present in + the mapped :class:`.Table` are automatically placed into + ``ColumnProperty`` instances upon mapping, unless overridden. + When using Declarative, this argument is passed automatically, + based on all those :class:`.MapperProperty` instances declared + in the declared class body. :param primary_key: A list of :class:`.Column` objects which define the primary key to be used against this mapper's selectable unit. This is normally simply the primary key of the ``local_table``, but can be overridden here. - :param version_id_col: A :class:`.Column` which must have an integer type + :param version_id_col: A :class:`.Column` that will be used to keep a running version id of mapped entities - in the database. this is used during save operations to ensure that + in the database. This is used during save operations to ensure that no other thread or process has updated the instance during the - lifetime of the entity, else a :class:`.StaleDataError` exception is - thrown. + lifetime of the entity, else a :class:`~sqlalchemy.orm.exc.StaleDataError` + exception is + thrown. By default the column must be of :class:`.Integer` type, + unless ``version_id_generator`` specifies a new generation + algorithm. :param version_id_generator: A callable which defines the algorithm used to generate new version ids. Defaults to an integer @@ -943,10 +1079,15 @@ def mapper(class_, local_table=None, *args, **params): import uuid - mapper(Cls, table, - version_id_col=table.c.version_uuid, - version_id_generator=lambda version:uuid.uuid4().hex - ) + class MyClass(Base): + __tablename__ = 'mytable' + id = Column(Integer, primary_key=True) + version_uuid = Column(String(32)) + + __mapper_args__ = { + 'version_id_col':version_uuid, + 'version_id_generator':lambda version:uuid.uuid4().hex + } The callable receives the current version identifier as its single argument. @@ -959,14 +1100,16 @@ def mapper(class_, local_table=None, *args, **params): ``'*'`` may be used to indicate all descending classes should be loaded immediately. The second tuple argument indicates a selectable that will be used to query for multiple - classes. Normally, it is left as None, in which case this mapper - will form an outer join from the base mapper's table to that of - all desired sub-mappers. When specified, it provides the - selectable to be used for polymorphic loading. When - with_polymorphic includes mappers which load from a "concrete" - inheriting table, the argument is required, since it - usually requires more complex UNION queries. - + classes. + + See also: + + :ref:`concrete_inheritance` - typically uses ``with_polymorphic`` + to specify a UNION statement to select from. + + :ref:`with_polymorphic` - usage example of the related + :meth:`.Query.with_polymorphic` method + """ return Mapper(class_, local_table, *args, **params) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 3a363e7313..f8e20c969e 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -128,9 +128,7 @@ class Mapper(object): self.batch = batch self.eager_defaults = eager_defaults self.column_prefix = column_prefix - self.polymorphic_on = expression._only_column_elements_or_none( - polymorphic_on, - "polymorphic_on") + self.polymorphic_on = polymorphic_on self._dependency_processors = [] self.validators = util.immutabledict() self.passive_updates = passive_updates @@ -882,34 +880,67 @@ class Mapper(object): if self.polymorphic_on is not None: setter = True - if self.polymorphic_on not in self._columntoproperty: + if isinstance(self.polymorphic_on, basestring): + try: + self.polymorphic_on = self._props[self.polymorphic_on] + except KeyError: + raise sa_exc.ArgumentError( + "Can't determine polymorphic_on " + "value '%s' - no attribute is " + "mapped to this name." % self.polymorphic_on) + + if self.polymorphic_on in self._columntoproperty: + prop = self._columntoproperty[self.polymorphic_on] + polymorphic_key = prop.key + self.polymorphic_on = prop.columns[0] + elif isinstance(self.polymorphic_on, MapperProperty): + if not isinstance(self.polymorphic_on, properties.ColumnProperty): + raise sa_exc.ArgumentError( + "Only direct column-mapped " + "property or SQL expression " + "can be passed for polymorphic_on") + prop = self.polymorphic_on + self.polymorphic_on = prop.columns[0] + polymorphic_key = prop.key + elif not expression.is_column(self.polymorphic_on): + raise sa_exc.ArgumentError( + "Only direct column-mapped " + "property or SQL expression " + "can be passed for polymorphic_on" + ) + else: col = self.mapped_table.corresponding_column(self.polymorphic_on) if col is None: setter = False instrument = False col = self.polymorphic_on - if self.with_polymorphic is None \ - or self.with_polymorphic[1].corresponding_column(col) \ - is None: - raise sa_exc.InvalidRequestError("Could not map polymorphic_on column " - "'%s' to the mapped table - polymorphic " - "loads will not function properly" - % col.description) + if isinstance(col, schema.Column) and ( + self.with_polymorphic is None or \ + self.with_polymorphic[1].corresponding_column(col) is None + ): + raise sa_exc.InvalidRequestError( + "Could not map polymorphic_on column " + "'%s' to the mapped table - polymorphic " + "loads will not function properly" + % col.description) else: instrument = True - if self._should_exclude(col.key, col.key, False, col): - raise sa_exc.InvalidRequestError( + key = getattr(col, 'key', None) + if key: + if self._should_exclude(col.key, col.key, False, col): + raise sa_exc.InvalidRequestError( "Cannot exclude or override the discriminator column %r" % col.key) + else: + col = col.label(None) + key = col.key self._configure_property( - col.key, + key, properties.ColumnProperty(col, _instrument=instrument), init=init, setparent=True) - polymorphic_key = col.key - else: - polymorphic_key = self._columntoproperty[self.polymorphic_on].key + polymorphic_key = key if setter: def _set_polymorphic_identity(state): @@ -1045,7 +1076,7 @@ class Mapper(object): prop.columns[0] is self.polymorphic_on) self.columns[key] = col - for col in prop.columns: + for col in prop.columns + prop._orig_columns: for col in col.proxy_set: self._columntoproperty[col] = prop diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 330a851083..8794cd3dad 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -61,6 +61,7 @@ class ColumnProperty(StrategizedProperty): :param extension: """ + self._orig_columns = [expression._labeled(c) for c in columns] self.columns = [expression._labeled(_orm_deannotate(c)) for c in columns] self.group = kwargs.pop('group', None) diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py index 7b991a618e..c9aa5fc9be 100644 --- a/test/orm/inheritance/test_basic.py +++ b/test/orm/inheritance/test_basic.py @@ -1,7 +1,7 @@ import warnings from test.lib.testing import eq_, assert_raises, assert_raises_message from sqlalchemy import * -from sqlalchemy import exc as sa_exc, util +from sqlalchemy import exc as sa_exc, util, event from sqlalchemy.orm import * from sqlalchemy.orm import exc as orm_exc, attributes from test.lib.assertsql import AllOf, CompiledSQL @@ -86,62 +86,189 @@ class PolymorphicOnNotLocalTest(fixtures.MappedTest): Column('y', String(10)), Column('xid', ForeignKey('t1.id'))) - def test_non_col_polymorphic_on(self): - class InterfaceBase(object): + @classmethod + def setup_classes(cls): + class Parent(cls.Comparable): + pass + class Child(Parent): pass + def test_non_col_polymorphic_on(self): + Parent = self.classes.Parent + t2 = self.tables.t2 assert_raises_message( sa_exc.ArgumentError, - "Column-based expression object expected " - "for argument 'polymorphic_on'; got: " - "'im not a column', type", + "Can't determine polymorphic_on " + "value 'im not a column' - no " + "attribute is mapped to this name.", mapper, - InterfaceBase, t2, polymorphic_on="im not a column" + Parent, t2, polymorphic_on="im not a column" ) - def test_bad_polymorphic_on(self): + def test_polymorphic_on_non_expr_prop(self): t2, t1 = self.tables.t2, self.tables.t1 + Parent = self.classes.Parent - class InterfaceBase(object): - pass + t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias() + def go(): + interface_m = mapper(Parent, t2, + polymorphic_on=lambda:"hi", + polymorphic_identity=0) + assert_raises_message( + sa_exc.ArgumentError, + "Only direct column-mapped property or " + "SQL expression can be passed for polymorphic_on", + go + ) + + def test_polymorphic_on_not_present_col(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent = self.classes.Parent t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias() def go(): - interface_m = mapper(InterfaceBase, t2, + t1t2_join_2 = select([t1.c.q], from_obj=[t1.join(t2)]).alias() + interface_m = mapper(Parent, t2, polymorphic_on=t1t2_join.c.x, + with_polymorphic=('*', t1t2_join_2), polymorphic_identity=0) - assert_raises_message( sa_exc.InvalidRequestError, "Could not map polymorphic_on column 'x' to the mapped table - " "polymorphic loads will not function properly", go ) - clear_mappers() + def test_polymorphic_on_only_in_with_poly(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent = self.classes.Parent + t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias() # if its in the with_polymorphic, then its OK - interface_m = mapper(InterfaceBase, t2, + mapper(Parent, t2, polymorphic_on=t1t2_join.c.x, with_polymorphic=('*', t1t2_join), polymorphic_identity=0) - configure_mappers() - clear_mappers() + def test_polymorpic_on_not_in_with_poly(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent = self.classes.Parent + + t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias() # if with_polymorphic, but its not present, not OK def go(): t1t2_join_2 = select([t1.c.q], from_obj=[t1.join(t2)]).alias() - interface_m = mapper(InterfaceBase, t2, + interface_m = mapper(Parent, t2, polymorphic_on=t1t2_join.c.x, with_polymorphic=('*', t1t2_join_2), polymorphic_identity=0) assert_raises_message( sa_exc.InvalidRequestError, - "Could not map polymorphic_on column 'x' to the mapped table - " + "Could not map polymorphic_on column 'x' " + "to the mapped table - " "polymorphic loads will not function properly", go ) + def test_polymorphic_on_expr_explicit_map(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent, Child = self.classes.Parent, self.classes.Child + expr = case([ + (t1.c.x=="p", "parent"), + (t1.c.x=="c", "child"), + ],else_ = t1.c.x) + mapper(Parent, t1, properties={ + "discriminator":column_property(expr) + }, polymorphic_identity="parent", + polymorphic_on=expr) + mapper(Child, t2, inherits=Parent, + polymorphic_identity="child") + + self._roundtrip() + + def test_polymorphic_on_expr_implicit_map(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent, Child = self.classes.Parent, self.classes.Child + expr = case([ + (t1.c.x=="p", "parent"), + (t1.c.x=="c", "child"), + ],else_ = t1.c.x).label("foo") + mapper(Parent, t1, polymorphic_identity="parent", + polymorphic_on=expr) + mapper(Child, t2, inherits=Parent, polymorphic_identity="child") + + self._roundtrip() + + def test_polymorphic_on_column_prop(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent, Child = self.classes.Parent, self.classes.Child + expr = case([ + (t1.c.x=="p", "parent"), + (t1.c.x=="c", "child"), + ],else_ = t1.c.x) + cprop = column_property(expr) + mapper(Parent, t1, properties={ + "discriminator":cprop + }, polymorphic_identity="parent", + polymorphic_on=cprop) + mapper(Child, t2, inherits=Parent, + polymorphic_identity="child") + + self._roundtrip() + + def test_polymorphic_on_column_str_prop(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent, Child = self.classes.Parent, self.classes.Child + expr = case([ + (t1.c.x=="p", "parent"), + (t1.c.x=="c", "child"), + ],else_ = t1.c.x) + cprop = column_property(expr) + mapper(Parent, t1, properties={ + "discriminator":cprop + }, polymorphic_identity="parent", + polymorphic_on="discriminator") + mapper(Child, t2, inherits=Parent, + polymorphic_identity="child") + + self._roundtrip() + + def test_polymorphic_on_synonym(self): + t2, t1 = self.tables.t2, self.tables.t1 + Parent, Child = self.classes.Parent, self.classes.Child + cprop = column_property(t1.c.x) + assert_raises_message( + sa_exc.ArgumentError, + "Only direct column-mapped property or " + "SQL expression can be passed for polymorphic_on", + mapper, Parent, t1, properties={ + "discriminator":cprop, + "discrim_syn":synonym(cprop) + }, polymorphic_identity="parent", + polymorphic_on="discrim_syn") + + def _roundtrip(self, set_event=True): + Parent, Child = self.classes.Parent, self.classes.Child + + if set_event: + @event.listens_for(Parent, "init", propagate=True) + def set_identity(instance, *arg, **kw): + instance.x = object_mapper(instance).polymorphic_identity + + s = Session(testing.db) + s.add_all([ + Parent(q="p1"), + Child(q="c1", y="c1"), + Parent(q="p2"), + ]) + s.commit() + s.close() + + eq_( + [type(t) for t in s.query(Parent).order_by(Parent.id)], + [Parent, Child, Parent] + ) + class FalseDiscriminatorTest(fixtures.MappedTest): @classmethod