summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-04-23 22:17:25 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-04-23 22:17:25 -0400
commitcb3913a186a01d9425e0ba97de89aa6d7d64ab96 (patch)
tree7616eade5c09cb0b6e833d8e881f795f4ea965f5
parent841ea194bd7cf239323ee21320210fd6dc5c551d (diff)
downloadsqlalchemy-cb3913a186a01d9425e0ba97de89aa6d7d64ab96.tar.gz
- [feature] New standalone function with_polymorphic()
provides the functionality of query.with_polymorphic() in a standalone form. It can be applied to any entity within a query, including as the target of a join in place of the "of_type()" modifier. [ticket:2333] - redo a large portion of the inheritance docs in terms of declarative, new with_polymorphic() function - upgrade examples/inheritance/polymorph, rename to "joined"
-rw-r--r--CHANGES7
-rw-r--r--doc/build/core/types.rst6
-rw-r--r--doc/build/orm/inheritance.rst530
-rw-r--r--examples/inheritance/joined.py135
-rw-r--r--examples/inheritance/polymorph.py125
-rw-r--r--lib/sqlalchemy/orm/__init__.py1
-rw-r--r--lib/sqlalchemy/orm/mapper.py3
-rw-r--r--lib/sqlalchemy/orm/query.py153
-rw-r--r--lib/sqlalchemy/orm/util.py142
-rw-r--r--test/orm/inheritance/test_with_poly.py113
10 files changed, 744 insertions, 471 deletions
diff --git a/CHANGES b/CHANGES
index 4c89bc0d9..0287e8766 100644
--- a/CHANGES
+++ b/CHANGES
@@ -42,6 +42,13 @@ those which apply to an 0.7 release are noted.
approach can be upgraded to this new
approach. [ticket:1401]
+ - [feature] New standalone function with_polymorphic()
+ provides the functionality of query.with_polymorphic()
+ in a standalone form. It can be applied to any
+ entity within a query, including as the target
+ of a join in place of the "of_type()" modifier.
+ [ticket:2333]
+
- [feature] Added new core function "inspect()",
which serves as a generic gateway to
introspection into mappers, objects,
diff --git a/doc/build/core/types.rst b/doc/build/core/types.rst
index b6144a438..7d64e6b4c 100644
--- a/doc/build/core/types.rst
+++ b/doc/build/core/types.rst
@@ -468,12 +468,6 @@ Base Type API
:inherited-members:
:show-inheritance:
-.. autoclass:: MutableType
- :members:
- :undoc-members:
- :inherited-members:
- :show-inheritance:
-
.. autoclass:: Concatenable
:members:
:undoc-members:
diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst
index 4713f3289..fbddc6579 100644
--- a/doc/build/orm/inheritance.rst
+++ b/doc/build/orm/inheritance.rst
@@ -3,137 +3,110 @@
Mapping Class Inheritance Hierarchies
======================================
-SQLAlchemy supports three forms of inheritance: *single table inheritance*,
-where several types of classes are stored in one table, *concrete table
-inheritance*, where each type of class is stored in its own table, and *joined
-table inheritance*, where the parent/child classes are stored in their own
-tables that are joined together in a select. Whereas support for single and
-joined table inheritance is strong, concrete table inheritance is a less
-common scenario with some particular problems so is not quite as flexible.
+SQLAlchemy supports three forms of inheritance: **single table inheritance**,
+where several types of classes are represented by a single table, **concrete table
+inheritance**, where each type of class is represented by independent tables,
+and **joined
+table inheritance**, where the class hierarchy is broken up
+among dependent tables, each class represented by its own table that only
+includes those attributes local to that class.
+
+The most common forms of inheritance are single and joined table, while
+concrete inheritance presents more configurational challenges.
When mappers are configured in an inheritance relationship, SQLAlchemy has the
ability to load elements "polymorphically", meaning that a single query can
return objects of multiple types.
-.. note::
-
- This section currently uses classical mappings to illustrate inheritance
- configurations, and will soon be updated to standardize on Declarative.
- Until then, please refer to :ref:`declarative_inheritance` for information on
- how common inheritance mappings are constructed declaratively.
-
-For the following sections, assume this class relationship:
-
-.. sourcecode:: python+sql
-
- class Employee(object):
- def __init__(self, name):
- self.name = name
- def __repr__(self):
- return self.__class__.__name__ + " " + self.name
-
- class Manager(Employee):
- def __init__(self, name, manager_data):
- self.name = name
- self.manager_data = manager_data
- def __repr__(self):
- return (
- self.__class__.__name__ + " " +
- self.name + " " + self.manager_data
- )
-
- class Engineer(Employee):
- def __init__(self, name, engineer_info):
- self.name = name
- self.engineer_info = engineer_info
- def __repr__(self):
- return (
- self.__class__.__name__ + " " +
- self.name + " " + self.engineer_info
- )
-
Joined Table Inheritance
-------------------------
In joined table inheritance, each class along a particular classes' list of
parents is represented by a unique table. The total set of attributes for a
particular instance is represented as a join along all tables in its
-inheritance path. Here, we first define a table to represent the ``Employee``
-class. This table will contain a primary key column (or columns), and a column
+inheritance path. Here, we first define the ``Employee`` class.
+This table will contain a primary key column (or columns), and a column
for each attribute that's represented by ``Employee``. In this case it's just
``name``::
- employees = Table('employees', metadata,
- Column('employee_id', Integer, primary_key=True),
- Column('name', String(50)),
- Column('type', String(30), nullable=False)
- )
+ class Employee(Base):
+ __tablename__ = 'employee'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+ type = Column(String(50))
-The table also has a column called ``type``. It is strongly advised in both
-single- and joined- table inheritance scenarios that the root table contains a
-column whose sole purpose is that of the **discriminator**; it stores a value
-which indicates the type of object represented within the row. The column may
-be of any desired datatype. While there are some "tricks" to work around the
-requirement that there be a discriminator column, they are more complicated to
-configure when one wishes to load polymorphically.
+ __mapper_args__ = {
+ 'polymorphic_identity':'employee',
+ 'polymorphic_on':type
+ }
-Next we define individual tables for each of ``Engineer`` and ``Manager``,
-which contain columns that represent the attributes unique to the subclass
+The mapped table also has a column called ``type``. The purpose of
+this column is to act as the **discriminator**, and stores a value
+which indicates the type of object represented within the row. The column may
+be of any datatype, though string and integer are the most common.
+
+The discriminator column is only needed if polymorphic loading is
+desired, as is usually the case. It is not strictly necessary that
+it be present directly on the base mapped table, and can instead be defined on a
+derived select statement that's used when the class is queried;
+however, this is a much more sophisticated configuration scenario.
+
+The mapping receives additional arguments via the ``__mapper_args__``
+dictionary. Here the ``type`` column is explicitly stated as the
+discriminator column, and the **polymorphic identity** of ``employee``
+is also given; this is the value that will be
+stored in the polymorphic discriminator column for instances of this
+class.
+
+We next define ``Engineer`` and ``Manager`` subclasses of ``Employee``.
+Each contains columns that represent the attributes unique to the subclass
they represent. Each table also must contain a primary key column (or
-columns), and in most cases a foreign key reference to the parent table. It is
-standard practice that the same column is used for both of these roles, and
-that the column is also named the same as that of the parent table. However
-this is optional in SQLAlchemy; separate columns may be used for primary key
-and parent-relationship, the column may be named differently than that of the
-parent, and even a custom join condition can be specified between parent and
-child tables instead of using a foreign key::
-
- engineers = Table('engineers', metadata,
- Column('employee_id', Integer,
- ForeignKey('employees.employee_id'),
- primary_key=True),
- Column('engineer_info', String(50)),
- )
-
- managers = Table('managers', metadata,
- Column('employee_id', Integer,
- ForeignKey('employees.employee_id'),
- primary_key=True),
- Column('manager_data', String(50)),
- )
+columns), and in most cases a foreign key reference to the parent table::
-One natural effect of the joined table inheritance configuration is that the
-identity of any mapped object can be determined entirely from the base table.
-This has obvious advantages, so SQLAlchemy always considers the primary key
-columns of a joined inheritance class to be those of the base table only,
-unless otherwise manually configured. In other words, the ``employee_id``
-column of both the ``engineers`` and ``managers`` table is not used to locate
-the ``Engineer`` or ``Manager`` object itself - only the value in
-``employees.employee_id`` is considered, and the primary key in this case is
-non-composite. ``engineers.employee_id`` and ``managers.employee_id`` are
-still of course critical to the proper operation of the pattern overall as
-they are used to locate the joined row, once the parent row has been
-determined, either through a distinct SELECT statement or all at once within a
-JOIN.
-
-We then configure mappers as usual, except we use some additional arguments to
-indicate the inheritance relationship, the polymorphic discriminator column,
-and the **polymorphic identity** of each class; this is the value that will be
-stored in the polymorphic discriminator column.
-
-.. sourcecode:: python+sql
-
- mapper(Employee, employees, polymorphic_on=employees.c.type,
- polymorphic_identity='employee')
- mapper(Engineer, engineers, inherits=Employee,
- polymorphic_identity='engineer')
- mapper(Manager, managers, inherits=Employee,
- polymorphic_identity='manager')
-
-And that's it. Querying against ``Employee`` will return a combination of
+ class Engineer(Employee):
+ __tablename__ = 'engineer'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ engineer_name = Column(String(30))
+
+ __mapper_args__ = {
+ 'polymorphic_identity':'engineer',
+ }
+
+ class Manager(Person):
+ __tablename__ = 'manager'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ manager_name = Column(String(30))
+
+ __mapper_args__ = {
+ 'polymorphic_identity':'manager',
+ }
+
+It is standard practice that the same column is used for both the role
+of primary key as well as foreign key to the parent table,
+and that the column is also named the same as that of the parent table.
+However, both of these practices are optional. Separate columns may be used for
+primary key and parent-relationship, the column may be named differently than
+that of the parent, and even a custom join condition can be specified between
+parent and child tables instead of using a foreign key.
+
+.. topic:: Joined inheritance primary keys
+
+ One natural effect of the joined table inheritance configuration is that the
+ identity of any mapped object can be determined entirely from the base table.
+ This has obvious advantages, so SQLAlchemy always considers the primary key
+ columns of a joined inheritance class to be those of the base table only.
+ In other words, the ``id``
+ columns of both the ``engineer`` and ``manager`` tables are not used to locate
+ ``Engineer`` or ``Manager`` objects - only the value in
+ ``employee.id`` is considered. ``engineer.id`` and ``manager.id`` are
+ still of course critical to the proper operation of the pattern overall as
+ they are used to locate the joined row, once the parent row has been
+ determined within a statement.
+
+With the joined inheritance mapping complete, querying against ``Employee`` will return a combination of
``Employee``, ``Engineer`` and ``Manager`` objects. Newly saved ``Engineer``,
``Manager``, and ``Employee`` objects will automatically populate the
-``employees.type`` column with ``engineer``, ``manager``, or ``employee``, as
+``employee.type`` column with ``engineer``, ``manager``, or ``employee``, as
appropriate.
.. _with_polymorphic:
@@ -141,28 +114,27 @@ appropriate.
Basic Control of Which Tables are Queried
++++++++++++++++++++++++++++++++++++++++++
-The :func:`~sqlalchemy.orm.query.Query.with_polymorphic` method of
-:class:`~sqlalchemy.orm.query.Query` affects the specific subclass tables
-which the Query selects from. Normally, a query such as this:
-
-.. sourcecode:: python+sql
+The :func:`.orm.with_polymorphic` function and the
+:func:`~sqlalchemy.orm.query.Query.with_polymorphic` method of
+:class:`~sqlalchemy.orm.query.Query` affects the specific tables
+which the :class:`.Query` selects from. Normally, a query such as this::
session.query(Employee).all()
-...selects only from the ``employees`` table. When loading fresh from the
+...selects only from the ``employee`` table. When loading fresh from the
database, our joined-table setup will query from the parent table only, using
SQL such as this:
.. sourcecode:: python+sql
{opensql}
- SELECT employees.employee_id AS employees_employee_id,
- employees.name AS employees_name, employees.type AS employees_type
- FROM employees
+ SELECT employee.id AS employee_id,
+ employee.name AS employee_name, employee.type AS employee_type
+ FROM employee
[]
As attributes are requested from those ``Employee`` objects which are
-represented in either the ``engineers`` or ``managers`` child tables, a second
+represented in either the ``engineer`` or ``manager`` child tables, a second
load is issued for the columns in that related row, if the data was not
already loaded. So above, after accessing the objects you'd see further SQL
issued along the lines of:
@@ -170,127 +142,174 @@ issued along the lines of:
.. sourcecode:: python+sql
{opensql}
- SELECT managers.employee_id AS managers_employee_id,
- managers.manager_data AS managers_manager_data
- FROM managers
- WHERE ? = managers.employee_id
+ SELECT manager.id AS manager_id,
+ manager.manager_data AS manager_manager_data
+ FROM manager
+ WHERE ? = manager.id
[5]
- SELECT engineers.employee_id AS engineers_employee_id,
- engineers.engineer_info AS engineers_engineer_info
- FROM engineers
- WHERE ? = engineers.employee_id
+ SELECT engineer.id AS engineer_id,
+ engineer.engineer_info AS engineer_engineer_info
+ FROM engineer
+ WHERE ? = engineer.id
[2]
This behavior works well when issuing searches for small numbers of items,
such as when using :meth:`.Query.get`, since the full range of joined tables are not
pulled in to the SQL statement unnecessarily. But when querying a larger span
of rows which are known to be of many types, you may want to actively join to
-some or all of the joined tables. The ``with_polymorphic`` feature of
-:class:`~sqlalchemy.orm.query.Query` and ``mapper`` provides this.
+some or all of the joined tables. The ``with_polymorphic`` feature
+provides this.
Telling our query to polymorphically load ``Engineer`` and ``Manager``
-objects:
+objects, we can use the :func:`.orm.with_polymorphic` function
+to create a new aliased class which represents a select of the base
+table combined with outer joins to each of the inheriting tables::
-.. sourcecode:: python+sql
+ from sqlalchemy.orm import with_polymorphic
+
+ eng_plus_manager = with_polymorphic(Employee, [Engineer, Manager])
- query = session.query(Employee).with_polymorphic([Engineer, Manager])
+ query = session.query(eng_plus_manager)
-produces a query which joins the ``employees`` table to both the ``engineers`` and ``managers`` tables like the following:
+The above produces a query which joins the ``employee`` table to both the
+``engineer`` and ``manager`` tables like the following:
.. sourcecode:: python+sql
query.all()
{opensql}
- SELECT employees.employee_id AS employees_employee_id,
- engineers.employee_id AS engineers_employee_id,
- managers.employee_id AS managers_employee_id,
- employees.name AS employees_name,
- employees.type AS employees_type,
- engineers.engineer_info AS engineers_engineer_info,
- managers.manager_data AS managers_manager_data
- FROM employees
- LEFT OUTER JOIN engineers
- ON employees.employee_id = engineers.employee_id
- LEFT OUTER JOIN managers
- ON employees.employee_id = managers.employee_id
+ SELECT employee.id AS employee_id,
+ engineer.id AS engineer_id,
+ manager.id AS manager_id,
+ employee.name AS employee_name,
+ employee.type AS employee_type,
+ engineer.engineer_info AS engineer_engineer_info,
+ manager.manager_data AS manager_manager_data
+ FROM employee
+ LEFT OUTER JOIN engineer
+ ON employee.id = engineer.id
+ LEFT OUTER JOIN manager
+ ON employee.id = manager.id
[]
-:func:`~sqlalchemy.orm.query.Query.with_polymorphic` accepts a single class or
+The entity returned by :func:`.orm.with_polymorphic` is an :class:`.AliasedClass`
+object, which can be used in a :class:`.Query` like any other alias, including
+named attributes for those attributes on the ``Employee`` class. In our
+example, ``eng_plus_manager`` becomes the entity that we use to refer to the
+three-way outer join above. It also includes namespaces for each class named
+in the list of classes, so that attributes specific to those subclasses can be
+called upon as well. The following example illustrates calling upon attributes
+specific to ``Engineer`` as well as ``Manager`` in terms of ``eng_plus_manager``::
+
+ eng_plus_manager = with_polymorphic(Employee, [Engineer, Manager])
+ query = session.query(eng_plus_manager).filter(
+ or_(
+ eng_plus_manager.Engineer.engineer_info=='x',
+ eng_plus_manager.Manager.manager_data=='y'
+ )
+ )
+
+:func:`.orm.with_polymorphic` accepts a single class or
mapper, a list of classes/mappers, or the string ``'*'`` to indicate all
subclasses:
.. sourcecode:: python+sql
- # join to the engineers table
- query.with_polymorphic(Engineer)
+ # join to the engineer table
+ entity = with_polymorphic(Employee, Engineer)
- # join to the engineers and managers tables
- query.with_polymorphic([Engineer, Manager])
+ # join to the engineer and manager tables
+ entity = with_polymorphic(Employee, [Engineer, Manager])
# join to all subclass tables
- query.with_polymorphic('*')
+ entity = query.with_polymorphic(Employee, '*')
+
+ # use with Query
+ session.query(entity).all()
It also accepts a second argument ``selectable`` which replaces the automatic
join creation and instead selects directly from the selectable given. This
feature is normally used with "concrete" inheritance, described later, but can
be used with any kind of inheritance setup in the case that specialized SQL
-should be used to load polymorphically:
-
-.. sourcecode:: python+sql
+should be used to load polymorphically::
# custom selectable
- query.with_polymorphic(
+ employee = Employee.__table__
+ manager = Manager.__table__
+ engineer = Engineer.__table__
+ entity = with_polymorphic(
+ Employee,
[Engineer, Manager],
- employees.outerjoin(managers).outerjoin(engineers)
+ employee.outerjoin(manager).outerjoin(engineer)
)
-:func:`~sqlalchemy.orm.query.Query.with_polymorphic` is also needed
-when you wish to add filter criteria that are specific to one or more
-subclasses; it makes the subclasses' columns available to the WHERE clause:
+ # use with Query
+ session.query(entity).all()
-.. sourcecode:: python+sql
+Note that if you only need to load a single subtype, such as just the
+``Engineer`` objects, :func:`.orm.with_polymorphic` is
+not needed since you would query against the ``Engineer`` class directly.
+
+:func:`.orm.with_polymorphic` is new in 0.8 and is an improved
+version of the existing :meth:`.Query.with_polymorphic` method.
+:meth:`.Query.with_polymorphic` has the same purpose, except is not as
+flexible in its usage patterns in that it only applies to the first full
+mapping, which then impacts all occurrences of that class or the target
+subclasses within the :class:`.Query`. For simple cases it might be
+considered to be more succinct::
session.query(Employee).with_polymorphic([Engineer, Manager]).\
filter(or_(Engineer.engineer_info=='w', Manager.manager_data=='q'))
-Note that if you only need to load a single subtype, such as just the
-``Engineer`` objects, :func:`~sqlalchemy.orm.query.Query.with_polymorphic` is
-not needed since you would query against the ``Engineer`` class directly.
-
The mapper also accepts ``with_polymorphic`` as a configurational argument so
that the joined-style load will be issued automatically. This argument may be
the string ``'*'``, a list of classes, or a tuple consisting of either,
-followed by a selectable.
+followed by a selectable::
-.. sourcecode:: python+sql
+ class Employee(Base):
+ __tablename__ = 'employee'
+ id = Column(Integer, primary_key=True)
+ type = Column(String(20))
+
+ __mapper_args__ = {
+ 'polymorphic_on':type,
+ 'polymorphic_identity':'employee',
+ 'with_polymorphic':'*'
+ }
+
+ class Engineer(Employee):
+ __tablename__ = 'engineer'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ __mapper_args__ = {'polymorphic_identity':'engineer'}
- mapper(Employee, employees, polymorphic_on=employees.c.type,
- polymorphic_identity='employee',
- with_polymorphic='*')
- mapper(Engineer, engineers, inherits=Employee,
- polymorphic_identity='engineer')
- mapper(Manager, managers, inherits=Employee,
- polymorphic_identity='manager')
+ class Manager(Employee):
+ __tablename__ = 'manager'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ __mapper_args__ = {'polymorphic_identity':'manager'}
The above mapping will produce a query similar to that of
``with_polymorphic('*')`` for every query of ``Employee`` objects.
-Using :func:`~sqlalchemy.orm.query.Query.with_polymorphic` with
-:class:`~sqlalchemy.orm.query.Query` will override the mapper-level
-``with_polymorphic`` setting.
+Using :func:`.orm.with_polymorphic` or :meth:`.Query.with_polymorphic`
+will override the mapper-level ``with_polymorphic`` setting.
+
+.. autofunction:: sqlalchemy.orm.with_polymorphic
Advanced Control of Which Tables are Queried
+++++++++++++++++++++++++++++++++++++++++++++
-The :meth:`.Query.with_polymorphic` method and configuration works fine for
-simplistic scenarios. However, it currently does not work with any
-:class:`.Query` that selects against individual columns or against multiple
-classes - it also has to be called at the outset of a query.
+The ``with_polymorphic`` functions work fine for
+simplistic scenarios. However, direct control of table rendering
+is called for, such as the case when one wants to
+render to only the subclass table and not the parent table.
-For total control of how :class:`.Query` joins along inheritance relationships,
-use the :class:`.Table` objects directly and construct joins manually. For example, to
+This use case can be achieved by using the mapped :class:`.Table`
+objects directly. For example, to
query the name of employees with particular criterion::
+ engineer = Engineer.__table__
+ manager = Manager.__table__
+
session.query(Employee.name).\
outerjoin((engineer, engineer.c.employee_id==Employee.employee_id)).\
outerjoin((manager, manager.c.employee_id==Employee.employee_id)).\
@@ -298,7 +317,7 @@ query the name of employees with particular criterion::
The base table, in this case the "employees" table, isn't always necessary. A
SQL query is always more efficient with fewer joins. Here, if we wanted to
-just load information specific to managers or engineers, we can instruct
+just load information specific to manager or engineer, we can instruct
:class:`.Query` to use only those tables. The ``FROM`` clause is determined by
what's specified in the :meth:`.Session.query`, :meth:`.Query.filter`, or
:meth:`.Query.select_from` methods::
@@ -321,32 +340,44 @@ of employees which are associated with a ``Company`` object. We'll add a
.. sourcecode:: python+sql
- companies = Table('companies', metadata,
- Column('company_id', Integer, primary_key=True),
- Column('name', String(50))
- )
-
- employees = Table('employees', metadata,
- Column('employee_id', Integer, primary_key=True),
- Column('name', String(50)),
- Column('type', String(30), nullable=False),
- Column('company_id', Integer, ForeignKey('companies.company_id'))
- )
+ class Company(Base):
+ __tablename__ = 'company'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
- class Company(object):
- pass
+ employees = relationship("Employee",
+ backref='company',
+ cascade='all, delete-orphan')
- mapper(Company, companies, properties={
- 'employees': relationship(Employee)
- })
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+ id = Column(Integer, primary_key=True)
+ type = Column(String(20))
+ company_id = Column(Integer, ForeignKey('company.id'))
+ __mapper_args__ = {
+ 'polymorphic_on':type,
+ 'polymorphic_identity':employee',
+ 'with_polymorphic':'*'
+ }
+
+ class Engineer(Employee):
+ __tablename__ = 'engineer'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ __mapper_args__ = {'polymorphic_identity':'engineer'}
+
+ class Manager(Employee):
+ __tablename__ = 'manager'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ __mapper_args__ = {'polymorphic_identity':'manager'}
When querying from ``Company`` onto the ``Employee`` relationship, the
``join()`` method as well as the ``any()`` and ``has()`` operators will create
-a join from ``companies`` to ``employees``, without including ``engineers`` or
-``managers`` in the mix. If we wish to have criterion which is specifically
+a join from ``company`` to ``employee``, without including ``engineer`` or
+``manager`` in the mix. If we wish to have criterion which is specifically
against the ``Engineer`` class, we can tell those methods to join or subquery
against the joined table representing the subclass using the
-:func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` operator::
+:meth:`~.orm.interfaces.PropComparator.of_type` operator::
session.query(Company).\
join(Company.employees.of_type(Engineer)).\
@@ -355,32 +386,43 @@ against the joined table representing the subclass using the
A longhand version of this would involve spelling out the full target
selectable within a 2-tuple::
+ employee = Employee.__table__
+ engineer = Engineer.__table__
+
session.query(Company).\
- join((employees.join(engineers), Company.employees)).\
+ join((employee.join(engineer), Company.employees)).\
filter(Engineer.engineer_info=='someinfo')
-Currently, :func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` accepts a
-single class argument. It may be expanded later on to accept multiple classes.
-For now, to join to any group of subclasses, the longhand notation allows this
-flexibility:
+:func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` accepts a
+single class argument. More flexibility can be achieved either by
+joining to an explicit join as above, or by using the :func:`.orm.with_polymorphic`
+function to create a polymorphic selectable::
-.. sourcecode:: python+sql
+ manager_and_engineer = with_polymorphic(
+ Employee, [Manager, Engineer],
+ aliased=True)
session.query(Company).\
- join(
- (employees.outerjoin(engineers).outerjoin(managers),
- Company.employees)
- ).\
+ join(manager_and_engineer, Company.employees).\
filter(
- or_(Engineer.engineer_info=='someinfo',
- Manager.manager_data=='somedata')
+ or_(manager_and_engineer.Engineer.engineer_info=='someinfo',
+ manager_and_engineer.Manager.manager_data=='somedata')
)
+Above, we use the ``aliased=True`` argument with :func:`.orm.with_polymorhpic`
+so that the right hand side of the join between ``Company`` and ``manager_and_engineer``
+is converted into an aliased subquery. Some backends, such as SQLite and older
+versions of MySQL can't handle a FROM clause of the following form::
+
+ FROM x JOIN (y JOIN z ON <onclause>) ON <onclause>`` - using ``aliased=True
+
+Using ``aliased=True`` instead renders it more like::
+
+ FROM x JOIN (SELECT * FROM y JOIN z ON <onclause>) AS anon_1 ON <onclause>
+
The ``any()`` and ``has()`` operators also can be used with
:func:`~sqlalchemy.orm.interfaces.PropComparator.of_type` when the embedded
-criterion is in terms of a subclass:
-
-.. sourcecode:: python+sql
+criterion is in terms of a subclass::
session.query(Company).\
filter(
@@ -389,9 +431,7 @@ criterion is in terms of a subclass:
).all()
Note that the ``any()`` and ``has()`` are both shorthand for a correlated
-EXISTS query. To build one by hand looks like:
-
-.. sourcecode:: python+sql
+EXISTS query. To build one by hand looks like::
session.query(Company).filter(
exists([1],
@@ -419,31 +459,45 @@ for the inheriting classes, leave their ``table`` parameter blank:
.. sourcecode:: python+sql
- employees_table = Table('employees', metadata,
- Column('employee_id', Integer, primary_key=True),
- Column('name', String(50)),
- Column('manager_data', String(50)),
- Column('engineer_info', String(50)),
- Column('type', String(20), nullable=False)
- )
+ class Employee(Base):
+ __tablename__ = 'employee'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+ manager_data = Column(String(50))
+ engineer_info = Column(String(50))
+ type = Column(String(20))
- employee_mapper = mapper(Employee, employees_table, \
- polymorphic_on=employees_table.c.type, polymorphic_identity='employee')
- manager_mapper = mapper(Manager, inherits=employee_mapper,
- polymorphic_identity='manager')
- engineer_mapper = mapper(Engineer, inherits=employee_mapper,
- polymorphic_identity='engineer')
+ __mapper_args__ = {
+ 'polymorphic_on':type,
+ 'polymorphic_identity':'employee'
+ }
+
+ class Manager(Employee):
+ __mapper_args__ = {
+ 'polymorphic_identity':'manager'
+ }
+
+ class Engineer(Employee):
+ __mapper_args__ = {
+ 'polymorphic_identity':'engineer'
+ }
Note that the mappers for the derived classes Manager and Engineer omit the
-specification of their associated table, as it is inherited from the
-employee_mapper. Omitting the table specification for derived mappers in
-single-table inheritance is required.
+``__tablename__``, indicating they do not have a mapped table of
+their own.
.. _concrete_inheritance:
Concrete Table Inheritance
--------------------------
+.. note::
+
+ this section is currently using classical mappings. The
+ Declarative system fully supports concrete inheritance
+ however. See the links below for more information on using
+ declarative with concrete table inheritance.
+
This form of inheritance maps each class to a distinct table, as below:
.. sourcecode:: python+sql
diff --git a/examples/inheritance/joined.py b/examples/inheritance/joined.py
new file mode 100644
index 000000000..aa4c18518
--- /dev/null
+++ b/examples/inheritance/joined.py
@@ -0,0 +1,135 @@
+"""this example illustrates a polymorphic load of two classes"""
+
+from sqlalchemy import Table, Column, Integer, String, \
+ ForeignKey, create_engine, inspect, or_
+from sqlalchemy.orm import relationship, Session, with_polymorphic
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class Company(Base):
+ __tablename__ = 'company'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+
+ employees = relationship("Person",
+ backref='company',
+ cascade='all, delete-orphan')
+
+ def __repr__(self):
+ return "Company %s" % self.name
+
+class Person(Base):
+ __tablename__ = 'person'
+ id = Column(Integer, primary_key=True)
+ company_id = Column(Integer, ForeignKey('company.id'))
+ name = Column(String(50))
+ type = Column(String(50))
+
+ __mapper_args__ = {
+ 'polymorphic_identity':'person',
+ 'polymorphic_on':type
+ }
+ def __repr__(self):
+ return "Ordinary person %s" % self.name
+
+class Engineer(Person):
+ __tablename__ = 'engineer'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ status = Column(String(30))
+ engineer_name = Column(String(30))
+ primary_language = Column(String(30))
+
+ __mapper_args__ = {
+ 'polymorphic_identity':'engineer',
+ }
+ def __repr__(self):
+ return "Engineer %s, status %s, engineer_name %s, "\
+ "primary_language %s" % \
+ (self.name, self.status,
+ self.engineer_name, self.primary_language)
+
+class Manager(Person):
+ __tablename__ = 'manager'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ status = Column(String(30))
+ manager_name = Column(String(30))
+
+ __mapper_args__ = {
+ 'polymorphic_identity':'manager',
+ }
+ def __repr__(self):
+ return "Manager %s, status %s, manager_name %s" % \
+ (self.name, self.status, self.manager_name)
+
+
+engine = create_engine('sqlite://', echo=True)
+Base.metadata.create_all(engine)
+
+session = Session(engine)
+
+c = Company(name='company1', employees=[
+ Manager(
+ name='pointy haired boss',
+ status='AAB',
+ manager_name='manager1'),
+ Engineer(name='dilbert',
+ status='BBA',
+ engineer_name='engineer1',
+ primary_language='java'),
+ Person(name='joesmith'),
+ Engineer(name='wally',
+ status='CGG',
+ engineer_name='engineer2',
+ primary_language='python'),
+ Manager(name='jsmith',
+ status='ABA',
+ manager_name='manager2')
+])
+session.add(c)
+
+session.commit()
+
+c = session.query(Company).get(1)
+for e in c.employees:
+ print e, inspect(e).key, e.company
+assert set([e.name for e in c.employees]) == set(['pointy haired boss',
+ 'dilbert', 'joesmith', 'wally', 'jsmith'])
+print "\n"
+
+dilbert = session.query(Person).filter_by(name='dilbert').one()
+dilbert2 = session.query(Engineer).filter_by(name='dilbert').one()
+assert dilbert is dilbert2
+
+dilbert.engineer_name = 'hes dilbert!'
+
+session.commit()
+
+c = session.query(Company).get(1)
+for e in c.employees:
+ print e
+
+# query using with_polymorphic.
+eng_manager = with_polymorphic(Person, [Engineer, Manager], aliased=True)
+print session.query(eng_manager).\
+ filter(
+ or_(eng_manager.Engineer.engineer_name=='engineer1',
+ eng_manager.Manager.manager_name=='manager2'
+ )
+ ).all()
+
+# illustrate join from Company,
+# We use aliased=True
+# to help when the selectable is used as the target of a join.
+eng_manager = with_polymorphic(Person, [Engineer, Manager], aliased=True)
+print session.query(Company).\
+ join(
+ eng_manager,
+ Company.employees
+ ).filter(
+ or_(eng_manager.Engineer.engineer_name=='engineer1',
+ eng_manager.Manager.manager_name=='manager2')
+ ).all()
+
+session.commit()
+
diff --git a/examples/inheritance/polymorph.py b/examples/inheritance/polymorph.py
deleted file mode 100644
index 316671bed..000000000
--- a/examples/inheritance/polymorph.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from sqlalchemy import MetaData, Table, Column, Integer, String, \
- ForeignKey, create_engine
-from sqlalchemy.orm import mapper, relationship, sessionmaker
-
-
-# this example illustrates a polymorphic load of two classes
-
-metadata = MetaData()
-
-# a table to store companies
-companies = Table('companies', metadata,
- Column('company_id', Integer, primary_key=True),
- Column('name', String(50)))
-
-# we will define an inheritance relationship between the table "people" and
-# "engineers", and a second inheritance relationship between the table
-# "people" and "managers"
-people = Table('people', metadata,
- Column('person_id', Integer, primary_key=True),
- Column('company_id', Integer, ForeignKey('companies.company_id')),
- Column('name', String(50)),
- Column('type', String(30)))
-
-engineers = Table('engineers', metadata,
- Column('person_id', Integer, ForeignKey('people.person_id'),
- primary_key=True),
- Column('status', String(30)),
- Column('engineer_name', String(50)),
- Column('primary_language', String(50)),
- )
-
-managers = Table('managers', metadata,
- Column('person_id', Integer, ForeignKey('people.person_id'),
- primary_key=True),
- Column('status', String(30)),
- Column('manager_name', String(50))
- )
-
-# create our classes. The Engineer and Manager classes extend from Person.
-class Person(object):
- def __init__(self, **kwargs):
- for key, value in kwargs.iteritems():
- setattr(self, key, value)
- def __repr__(self):
- return "Ordinary person %s" % self.name
-class Engineer(Person):
- def __repr__(self):
- return "Engineer %s, status %s, engineer_name %s, "\
- "primary_language %s" % \
- (self.name, self.status,
- self.engineer_name, self.primary_language)
-class Manager(Person):
- def __repr__(self):
- return "Manager %s, status %s, manager_name %s" % \
- (self.name, self.status, self.manager_name)
-class Company(object):
- def __init__(self, **kwargs):
- for key, value in kwargs.iteritems():
- setattr(self, key, value)
- def __repr__(self):
- return "Company %s" % self.name
-
-
-person_mapper = mapper(Person, people, polymorphic_on=people.c.type,
- polymorphic_identity='person')
-mapper(Engineer, engineers, inherits=person_mapper,
- polymorphic_identity='engineer')
-mapper(Manager, managers, inherits=person_mapper,
- polymorphic_identity='manager')
-
-mapper(Company, companies, properties={'employees'
- : relationship(Person, lazy='joined', backref='company',
- cascade='all, delete-orphan')})
-
-engine = create_engine('sqlite://', echo=True)
-
-metadata.create_all(engine)
-
-session = sessionmaker(engine)()
-
-c = Company(name='company1')
-c.employees.append(Manager(name='pointy haired boss', status='AAB',
- manager_name='manager1'))
-c.employees.append(Engineer(name='dilbert', status='BBA',
- engineer_name='engineer1', primary_language='java'))
-c.employees.append(Person(name='joesmith', status='HHH'))
-c.employees.append(Engineer(name='wally', status='CGG',
- engineer_name='engineer2', primary_language='python'
- ))
-c.employees.append(Manager(name='jsmith', status='ABA',
- manager_name='manager2'))
-session.add(c)
-
-session.commit()
-
-c = session.query(Company).get(1)
-for e in c.employees:
- print e, e._sa_instance_state.key, e.company
-assert set([e.name for e in c.employees]) == set(['pointy haired boss',
- 'dilbert', 'joesmith', 'wally', 'jsmith'])
-print "\n"
-
-dilbert = session.query(Person).filter_by(name='dilbert').one()
-dilbert2 = session.query(Engineer).filter_by(name='dilbert').one()
-assert dilbert is dilbert2
-
-dilbert.engineer_name = 'hes dibert!'
-
-session.commit()
-
-c = session.query(Company).get(1)
-for e in c.employees:
- print e
-
-# illustrate querying using direct table access:
-
-print session.query(Engineer.engineer_name).\
- select_from(engineers).\
- filter(Engineer.primary_language=='python').\
- all()
-
-
-session.delete(c)
-session.commit()
-
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 1813f57d8..d322d426b 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -35,6 +35,7 @@ from sqlalchemy.orm.util import (
outerjoin,
polymorphic_union,
with_parent,
+ with_polymorphic,
)
from sqlalchemy.orm.properties import (
ColumnProperty,
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 0771bbf3d..795447763 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -1358,7 +1358,8 @@ class Mapper(object):
spec = self.with_polymorphic[0]
if selectable is False:
selectable = self.with_polymorphic[1]
-
+ elif selectable is False:
+ selectable = None
mappers = self._mappers_from_spec(spec, selectable)
if selectable is not None:
return mappers, selectable
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index b0ce9ee13..d50c3922a 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -31,6 +31,7 @@ from sqlalchemy.orm import (
)
from sqlalchemy.orm.util import (
AliasedClass, ORMAdapter, _entity_descriptor, _entity_info,
+ _extended_entity_info,
_is_aliased_class, _is_mapped_class, _orm_columns, _orm_selectable,
join as orm_join,with_parent, _attr_as_key, aliased
)
@@ -92,6 +93,7 @@ class Query(object):
_from_obj = ()
_join_entities = ()
_select_from_entity = None
+ _mapper_adapter_map = {}
_filter_aliases = None
_from_obj_alias = None
_joinpath = _joinpoint = util.immutabledict()
@@ -114,50 +116,43 @@ class Query(object):
for ent in util.to_list(entities):
entity_wrapper(self, ent)
- self._setup_aliasizers(self._entities)
+ self._set_entity_selectables(self._entities)
- def _setup_aliasizers(self, entities):
- if hasattr(self, '_mapper_adapter_map'):
- # usually safe to share a single map, but copying to prevent
- # subtle leaks if end-user is reusing base query with arbitrary
- # number of aliased() objects
- self._mapper_adapter_map = d = self._mapper_adapter_map.copy()
- else:
- self._mapper_adapter_map = d = {}
+ def _set_entity_selectables(self, entities):
+ self._mapper_adapter_map = d = self._mapper_adapter_map.copy()
for ent in entities:
for entity in ent.entities:
if entity not in d:
- mapper, selectable, is_aliased_class = \
- _entity_info(entity)
+ mapper, selectable, \
+ is_aliased_class, with_polymorphic_mappers, \
+ with_polymorphic_discriminator = \
+ _extended_entity_info(entity)
if not is_aliased_class and mapper.with_polymorphic:
- with_polymorphic = mapper._with_polymorphic_mappers
if mapper.mapped_table not in \
self._polymorphic_adapters:
self._mapper_loads_polymorphically_with(mapper,
sql_util.ColumnAdapter(
selectable,
mapper._equivalent_columns))
- adapter = None
+ aliased_adapter = None
elif is_aliased_class:
- adapter = sql_util.ColumnAdapter(
+ aliased_adapter = sql_util.ColumnAdapter(
selectable,
mapper._equivalent_columns)
- with_polymorphic = None
else:
- with_polymorphic = adapter = None
+ aliased_adapter = None
- d[entity] = (mapper, adapter, selectable,
- is_aliased_class, with_polymorphic)
+ d[entity] = (mapper, aliased_adapter, selectable,
+ is_aliased_class, with_polymorphic_mappers,
+ with_polymorphic_discriminator)
ent.setup_entity(entity, *d[entity])
def _mapper_loads_polymorphically_with(self, mapper, adapter):
for m2 in mapper._with_polymorphic_mappers:
self._polymorphic_adapters[m2] = adapter
for m in m2.iterate_to_root():
- self._polymorphic_adapters[m.mapped_table] = \
- self._polymorphic_adapters[m.local_table] = \
- adapter
+ self._polymorphic_adapters[m.local_table] = adapter
def _set_select_from(self, *obj):
@@ -180,10 +175,9 @@ class Query(object):
for m2 in mapper._with_polymorphic_mappers:
self._polymorphic_adapters.pop(m2, None)
for m in m2.iterate_to_root():
- self._polymorphic_adapters.pop(m.mapped_table, None)
self._polymorphic_adapters.pop(m.local_table, None)
- def __adapt_polymorphic_element(self, element):
+ def _adapt_polymorphic_element(self, element):
if isinstance(element, expression.FromClause):
search = element
elif hasattr(element, 'table'):
@@ -241,7 +235,7 @@ class Query(object):
if self._polymorphic_adapters:
adapters.append(
(
- orm_only, self.__adapt_polymorphic_element
+ orm_only, self._adapt_polymorphic_element
)
)
@@ -617,35 +611,29 @@ class Query(object):
@_generative(_no_clauseelement_condition)
def with_polymorphic(self,
cls_or_mappers,
- selectable=None, discriminator=None):
- """Load columns for descendant mappers of this Query's mapper.
-
- Using this method will ensure that each descendant mapper's
- tables are included in the FROM clause, and will allow filter()
- criterion to be used against those tables. The resulting
- instances will also have those columns already loaded so that
- no "post fetch" of those columns will be required.
-
- :param cls_or_mappers: a single class or mapper, or list of
- class/mappers, which inherit from this Query's mapper.
- Alternatively, it may also be the string ``'*'``, in which case
- all descending mappers will be added to the FROM clause.
-
- :param selectable: a table or select() statement that will
- be used in place of the generated FROM clause. This argument is
- required if any of the desired mappers use concrete table
- inheritance, since SQLAlchemy currently cannot generate UNIONs
- among tables automatically. If used, the ``selectable`` argument
- must represent the full set of tables and columns mapped by every
- desired mapper. Otherwise, the unaccounted mapped columns will
- result in their table being appended directly to the FROM clause
- which will usually lead to incorrect results.
-
- :param discriminator: a column to be used as the "discriminator"
- column for the given selectable. If not given, the polymorphic_on
- attribute of the mapper will be used, if any. This is useful for
- mappers that don't have polymorphic loading behavior by default,
- such as concrete table mappers.
+ selectable=None,
+ polymorphic_on=None):
+ """Load columns for inheriting classes.
+
+ :meth:`.Query.with_polymorphic` applies transformations
+ to the "main" mapped class represented by this :class:`.Query`.
+ The "main" mapped class here means the :class:`.Query`
+ object's first argument is a full class, i.e. ``session.query(SomeClass)``.
+ These transformations allow additional tables to be present
+ in the FROM clause so that columns for a joined-inheritance
+ subclass are available in the query, both for the purposes
+ of load-time efficiency as well as the ability to use
+ these columns at query time.
+
+ See the documentation section :ref:`with_polymorphic` for
+ details on how this method is used.
+
+ As of 0.8, a new and more flexible function
+ :func:`.orm.with_polymorphic` supersedes
+ :meth:`.Query.with_polymorphic`, as it can apply the equivalent
+ functionality to any set of columns or classes in the
+ :class:`.Query`, not just the "zero mapper". See that
+ function for a description of arguments.
"""
@@ -657,7 +645,7 @@ class Query(object):
entity.set_with_polymorphic(self,
cls_or_mappers,
selectable=selectable,
- discriminator=discriminator)
+ polymorphic_on=polymorphic_on)
@_generative()
def yield_per(self, count):
@@ -881,7 +869,7 @@ class Query(object):
self._entities = list(self._entities)
m = _MapperEntity(self, entity)
- self._setup_aliasizers([m])
+ self._set_entity_selectables([m])
@_generative()
def with_session(self, session):
@@ -998,7 +986,7 @@ class Query(object):
_ColumnEntity(self, c)
# _ColumnEntity may add many entities if the
# given arg is a FROM clause
- self._setup_aliasizers(self._entities[l:])
+ self._set_entity_selectables(self._entities[l:])
@util.pending_deprecation("0.7",
":meth:`.add_column` is superseded by :meth:`.add_columns`",
@@ -2998,7 +2986,7 @@ class Query(object):
selected from the total results.
"""
- for entity, (mapper, adapter, s, i, w) in \
+ for entity, (mapper, adapter, s, i, w, d) in \
self._mapper_adapter_map.iteritems():
if entity in self._join_entities:
continue
@@ -3042,14 +3030,16 @@ class _MapperEntity(_QueryEntity):
self.entities = [entity]
self.entity_zero = self.expr = entity
- def setup_entity(self, entity, mapper, adapter,
- from_obj, is_aliased_class, with_polymorphic):
+ def setup_entity(self, entity, mapper, aliased_adapter,
+ from_obj, is_aliased_class,
+ with_polymorphic,
+ with_polymorphic_discriminator):
self.mapper = mapper
- self.adapter = adapter
+ self.aliased_adapter = aliased_adapter
self.selectable = from_obj
- self._with_polymorphic = with_polymorphic
- self._polymorphic_discriminator = None
self.is_aliased_class = is_aliased_class
+ self._with_polymorphic = with_polymorphic
+ self._polymorphic_discriminator = with_polymorphic_discriminator
if is_aliased_class:
self.path_entity = self.entity_zero = entity
self._path = (entity,)
@@ -3062,9 +3052,14 @@ class _MapperEntity(_QueryEntity):
self.entity_zero = mapper
self._label_name = self.mapper.class_.__name__
-
def set_with_polymorphic(self, query, cls_or_mappers,
- selectable, discriminator):
+ selectable, polymorphic_on):
+ if self.is_aliased_class:
+ raise NotImplementedError(
+ "Can't use with_polymorphic() against "
+ "an Aliased object"
+ )
+
if cls_or_mappers is None:
query._reset_polymorphic_adapter(self.mapper)
return
@@ -3072,15 +3067,12 @@ class _MapperEntity(_QueryEntity):
mappers, from_obj = self.mapper._with_polymorphic_args(
cls_or_mappers, selectable)
self._with_polymorphic = mappers
- self._polymorphic_discriminator = discriminator
+ self._polymorphic_discriminator = polymorphic_on
- # TODO: do the wrapped thing here too so that
- # with_polymorphic() can be applied to aliases
- if not self.is_aliased_class:
- self.selectable = from_obj
- query._mapper_loads_polymorphically_with(self.mapper,
- sql_util.ColumnAdapter(from_obj,
- self.mapper._equivalent_columns))
+ self.selectable = from_obj
+ query._mapper_loads_polymorphically_with(self.mapper,
+ sql_util.ColumnAdapter(from_obj,
+ self.mapper._equivalent_columns))
filter_fn = id
@@ -3104,11 +3096,12 @@ class _MapperEntity(_QueryEntity):
def _get_entity_clauses(self, query, context):
adapter = None
- if not self.is_aliased_class and query._polymorphic_adapters:
- adapter = query._polymorphic_adapters.get(self.mapper, None)
- if not adapter and self.adapter:
- adapter = self.adapter
+ if not self.is_aliased_class:
+ if query._polymorphic_adapters:
+ adapter = query._polymorphic_adapters.get(self.mapper, None)
+ else:
+ adapter = self.aliased_adapter
if adapter:
if query._from_obj_alias:
@@ -3194,7 +3187,10 @@ class _MapperEntity(_QueryEntity):
column_collection=context.primary_columns
)
- if self._polymorphic_discriminator is not None:
+ if self._polymorphic_discriminator is not None and \
+ self._polymorphic_discriminator \
+ is not self.mapper.polymorphic_on:
+
if adapter:
pd = adapter.columns[self._polymorphic_discriminator]
else:
@@ -3297,7 +3293,8 @@ class _ColumnEntity(_QueryEntity):
c.entities = self.entities
def setup_entity(self, entity, mapper, adapter, from_obj,
- is_aliased_class, with_polymorphic):
+ is_aliased_class, with_polymorphic,
+ with_polymorphic_discriminator):
if 'selectable' not in self.__dict__:
self.selectable = from_obj
self.froms.add(from_obj)
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 3cbe3f84a..5fcb15a9a 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -294,16 +294,28 @@ class AliasedClass(object):
``adapt_on_names`` is new in 0.7.3.
"""
- def __init__(self, cls, alias=None, name=None, adapt_on_names=False):
+ def __init__(self, cls, alias=None,
+ name=None,
+ adapt_on_names=False,
+ with_polymorphic_mappers=(),
+ with_polymorphic_discriminator=None):
self.__mapper = _class_to_mapper(cls)
self.__target = self.__mapper.class_
self.__adapt_on_names = adapt_on_names
if alias is None:
- alias = self.__mapper._with_polymorphic_selectable.alias(name=name)
+ alias = self.__mapper._with_polymorphic_selectable.alias(
+ name=name)
self.__adapter = sql_util.ClauseAdapter(alias,
- equivalents=self.__mapper._equivalent_columns,
- adapt_on_names=self.__adapt_on_names)
+ equivalents=self.__mapper._equivalent_columns,
+ adapt_on_names=self.__adapt_on_names)
self.__alias = alias
+ self.__with_polymorphic_mappers = with_polymorphic_mappers
+ self.__with_polymorphic_discriminator = \
+ with_polymorphic_discriminator
+ for poly in with_polymorphic_mappers:
+ setattr(self, poly.class_.__name__,
+ AliasedClass(poly.class_, alias))
+
# used to assign a name to the RowTuple object
# returned by Query.
self._sa_label_name = name
@@ -315,6 +327,10 @@ class AliasedClass(object):
'alias':self.__alias,
'name':self._sa_label_name,
'adapt_on_names':self.__adapt_on_names,
+ 'with_polymorphic_mappers':
+ self.__with_polymorphic_mappers,
+ 'with_polymorphic_discriminator':
+ self.__with_polymorphic_discriminator
}
def __setstate__(self, state):
@@ -323,9 +339,13 @@ class AliasedClass(object):
self.__adapt_on_names = state['adapt_on_names']
alias = state['alias']
self.__adapter = sql_util.ClauseAdapter(alias,
- equivalents=self.__mapper._equivalent_columns,
- adapt_on_names=self.__adapt_on_names)
+ equivalents=self.__mapper._equivalent_columns,
+ adapt_on_names=self.__adapt_on_names)
self.__alias = alias
+ self.__with_polymorphic_mappers = \
+ state.get('with_polymorphic_mappers')
+ self.__with_polymorphic_discriminator = \
+ state.get('with_polymorphic_discriminator')
name = state['name']
self._sa_label_name = name
self.__name__ = 'AliasedClass_' + str(self.__target)
@@ -379,10 +399,75 @@ class AliasedClass(object):
def aliased(element, alias=None, name=None, adapt_on_names=False):
if isinstance(element, expression.FromClause):
if adapt_on_names:
- raise sa_exc.ArgumentError("adapt_on_names only applies to ORM elements")
+ raise sa_exc.ArgumentError(
+ "adapt_on_names only applies to ORM elements"
+ )
return element.alias(name)
else:
- return AliasedClass(element, alias=alias, name=name, adapt_on_names=adapt_on_names)
+ return AliasedClass(element, alias=alias,
+ name=name, adapt_on_names=adapt_on_names)
+
+def with_polymorphic(base, classes, selectable=False,
+ polymorphic_on=None, aliased=False):
+ """Produce an :class:`.AliasedClass` construct which specifies
+ columns for descendant mappers of the given base.
+
+ .. note::
+
+ :func:`.orm.with_polymorphic` is new in version 0.8.
+ It is in addition to the existing :class:`.Query` method
+ :meth:`.Query.with_polymorphic`, which has the same purpose
+ but is not as flexible in its usage.
+
+ Using this method will ensure that each descendant mapper's
+ tables are included in the FROM clause, and will allow filter()
+ criterion to be used against those tables. The resulting
+ instances will also have those columns already loaded so that
+ no "post fetch" of those columns will be required.
+
+ See the examples at :ref:`with_polymorphic`.
+
+ :param base: Base class to be aliased.
+
+ :param cls_or_mappers: a single class or mapper, or list of
+ class/mappers, which inherit from the base class.
+ Alternatively, it may also be the string ``'*'``, in which case
+ all descending mapped classes will be added to the FROM clause.
+
+ :param aliased: when True, the selectable will be wrapped in an
+ alias, that is ``(SELECT * FROM <fromclauses>) AS anon_1``.
+ This can be important when using the with_polymorphic()
+ to create the target of a JOIN on a backend that does not
+ support parenthesized joins, such as SQLite and older
+ versions of MySQL.
+
+ :param selectable: a table or select() statement that will
+ be used in place of the generated FROM clause. This argument is
+ required if any of the desired classes use concrete table
+ inheritance, since SQLAlchemy currently cannot generate UNIONs
+ among tables automatically. If used, the ``selectable`` argument
+ must represent the full set of tables and columns mapped by every
+ mapped class. Otherwise, the unaccounted mapped columns will
+ result in their table being appended directly to the FROM clause
+ which will usually lead to incorrect results.
+
+ :param polymorphic_on: a column to be used as the "discriminator"
+ column for the given selectable. If not given, the polymorphic_on
+ attribute of the base classes' mapper will be used, if any. This
+ is useful for mappings that don't have polymorphic loading
+ behavior by default.
+
+ """
+ primary_mapper = class_mapper(base)
+ mappers, selectable = primary_mapper.\
+ _with_polymorphic_args(classes, selectable)
+ if aliased:
+ selectable = selectable.alias()
+ return AliasedClass(base,
+ selectable,
+ with_polymorphic_mappers=mappers,
+ with_polymorphic_discriminator=polymorphic_on)
+
def _orm_annotate(element, exclude=None):
"""Deep copy the given ClauseElement, annotating each element with the
@@ -560,19 +645,13 @@ def with_parent(instance, prop):
value_is_parent=True)
-def _entity_info(entity, compile=True):
- """Return mapping information given a class, mapper, or AliasedClass.
-
- Returns 3-tuple of: mapper, mapped selectable, boolean indicating if this
- is an aliased() construct.
-
- If the given entity is not a mapper, mapped class, or aliased construct,
- returns None, the entity, False. This is typically used to allow
- unmapped selectables through.
-
- """
+def _extended_entity_info(entity, compile=True):
if isinstance(entity, AliasedClass):
- return entity._AliasedClass__mapper, entity._AliasedClass__alias, True
+ return entity._AliasedClass__mapper, \
+ entity._AliasedClass__alias, \
+ True, \
+ entity._AliasedClass__with_polymorphic_mappers, \
+ entity._AliasedClass__with_polymorphic_discriminator
if isinstance(entity, mapperlib.Mapper):
mapper = entity
@@ -581,15 +660,32 @@ def _entity_info(entity, compile=True):
class_manager = attributes.manager_of_class(entity)
if class_manager is None:
- return None, entity, False
+ return None, entity, False, [], None
mapper = class_manager.mapper
else:
- return None, entity, False
+ return None, entity, False, [], None
if compile and mapperlib.module._new_mappers:
mapperlib.configure_mappers()
- return mapper, mapper._with_polymorphic_selectable, False
+ return mapper, \
+ mapper._with_polymorphic_selectable, \
+ False, \
+ mapper._with_polymorphic_mappers, \
+ mapper.polymorphic_on
+
+def _entity_info(entity, compile=True):
+ """Return mapping information given a class, mapper, or AliasedClass.
+
+ Returns 3-tuple of: mapper, mapped selectable, boolean indicating if this
+ is an aliased() construct.
+
+ If the given entity is not a mapper, mapped class, or aliased construct,
+ returns None, the entity, False. This is typically used to allow
+ unmapped selectables through.
+
+ """
+ return _extended_entity_info(entity, compile)[0:3]
def _entity_descriptor(entity, key):
"""Return a class attribute given an entity and string name.
diff --git a/test/orm/inheritance/test_with_poly.py b/test/orm/inheritance/test_with_poly.py
new file mode 100644
index 000000000..d0de0aa22
--- /dev/null
+++ b/test/orm/inheritance/test_with_poly.py
@@ -0,0 +1,113 @@
+from sqlalchemy import Integer, String, ForeignKey, func, desc, and_, or_
+from sqlalchemy.orm import interfaces, relationship, mapper, \
+ clear_mappers, create_session, joinedload, joinedload_all, \
+ subqueryload, subqueryload_all, polymorphic_union, aliased,\
+ class_mapper, with_polymorphic
+from sqlalchemy import exc as sa_exc
+from sqlalchemy.engine import default
+
+from test.lib import AssertsCompiledSQL, fixtures, testing
+from test.lib.schema import Table, Column
+from test.lib.testing import assert_raises, eq_
+
+from _poly_fixtures import Company, Person, Engineer, Manager, Boss, \
+ Machine, Paperwork, _PolymorphicFixtureBase, _Polymorphic,\
+ _PolymorphicPolymorphic, _PolymorphicUnions, _PolymorphicJoins,\
+ _PolymorphicAliasedJoins
+
+class _WithPolymorphicBase(_PolymorphicFixtureBase):
+ def test_join_base_to_sub(self):
+ sess = create_session()
+ pa = with_polymorphic(Person, [Engineer])
+
+ def go():
+ eq_(sess.query(pa)
+ .filter(pa.Engineer.primary_language == 'java').all(),
+ self._emps_wo_relationships_fixture()[0:1])
+ self.assert_sql_count(testing.db, go, 1)
+
+ def test_col_expression_base_plus_two_subs(self):
+ sess = create_session()
+ pa = with_polymorphic(Person, [Engineer, Manager])
+
+ eq_(
+ sess.query(pa.name, pa.Engineer.primary_language, pa.Manager.manager_name).\
+ filter(or_(pa.Engineer.primary_language=='java',
+ pa.Manager.manager_name=='dogbert')).\
+ order_by(pa.Engineer.type).all(),
+ [
+ (u'dilbert', u'java', None),
+ (u'dogbert', None, u'dogbert'),
+ ]
+ )
+
+
+ def test_join_to_join_entities(self):
+ sess = create_session()
+ pa = with_polymorphic(Person, [Engineer])
+ pa_alias = with_polymorphic(Person, [Engineer], aliased=True)
+
+ eq_(
+ [(p1.name, type(p1), p2.name, type(p2)) for (p1, p2) in sess.query(
+ pa, pa_alias
+ ).join(pa_alias,
+ or_(
+ pa.Engineer.primary_language==\
+ pa_alias.Engineer.primary_language,
+ and_(
+ pa.Engineer.primary_language == None,
+ pa_alias.Engineer.primary_language == None,
+ pa.person_id > pa_alias.person_id
+ )
+ )
+ ).order_by(pa.name, pa_alias.name)],
+ [
+ (u'dilbert', Engineer, u'dilbert', Engineer),
+ (u'dogbert', Manager, u'pointy haired boss', Boss),
+ (u'vlad', Engineer, u'vlad', Engineer),
+ (u'wally', Engineer, u'wally', Engineer)
+ ]
+ )
+
+ def test_join_to_join_columns(self):
+ sess = create_session()
+ pa = with_polymorphic(Person, [Engineer])
+ pa_alias = with_polymorphic(Person, [Engineer], aliased=True)
+
+ eq_(
+ [row for row in sess.query(
+ pa.name, pa.Engineer.primary_language,
+ pa_alias.name, pa_alias.Engineer.primary_language
+ ).join(pa_alias,
+ or_(
+ pa.Engineer.primary_language==\
+ pa_alias.Engineer.primary_language,
+ and_(
+ pa.Engineer.primary_language == None,
+ pa_alias.Engineer.primary_language == None,
+ pa.person_id > pa_alias.person_id
+ )
+ )
+ ).order_by(pa.name, pa_alias.name)],
+ [
+ (u'dilbert', u'java', u'dilbert', u'java'),
+ (u'dogbert', None, u'pointy haired boss', None),
+ (u'vlad', u'cobol', u'vlad', u'cobol'),
+ (u'wally', u'c++', u'wally', u'c++')
+ ]
+ )
+
+class PolymorphicTest(_WithPolymorphicBase, _Polymorphic):
+ pass
+
+class PolymorphicPolymorphicTest(_WithPolymorphicBase, _PolymorphicPolymorphic):
+ pass
+
+class PolymorphicUnionsTest(_WithPolymorphicBase, _PolymorphicUnions):
+ pass
+
+class PolymorphicAliasedJoinsTest(_WithPolymorphicBase, _PolymorphicAliasedJoins):
+ pass
+
+class PolymorphicJoinsTest(_WithPolymorphicBase, _PolymorphicJoins):
+ pass