From: Mike Bayer Date: Wed, 20 Jan 2016 18:34:57 +0000 (-0500) Subject: - redo the docs for concrete inheritance to more strongly X-Git-Tag: rel_1_1_0b1~84^2~27 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=632c4f21fa3cab353b801f585183494c529c6896;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - redo the docs for concrete inheritance to more strongly favor declarative, fixes #2890 --- diff --git a/doc/build/orm/extensions/declarative/inheritance.rst b/doc/build/orm/extensions/declarative/inheritance.rst index 59d6854565..bf0f2a6e1e 100644 --- a/doc/build/orm/extensions/declarative/inheritance.rst +++ b/doc/build/orm/extensions/declarative/inheritance.rst @@ -194,6 +194,8 @@ Because we're using single table inheritance, we're sure that in this case, and single-table inheritance, we might want our mixin to check more carefully if ``cls.__table__`` is really the :class:`.Table` we're looking for. +.. _declarative_concrete_table: + Concrete Table Inheritance ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -254,8 +256,6 @@ Helper classes provides a simpler pattern for concrete inheritance. With these objects, the ``__declare_first__`` helper is used to configure the "polymorphic" loader for the mapper after all subclasses have been declared. -.. versionadded:: 0.7.3 - An abstract base can be declared using the :class:`.AbstractConcreteBase` class:: diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst index 290d8099ec..5645b55991 100644 --- a/doc/build/orm/inheritance.rst +++ b/doc/build/orm/inheritance.rst @@ -579,53 +579,111 @@ their own. 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: +This form of inheritance maps each class to a distinct table. As concrete +inheritance has a bit more conceptual overhead, first we'll illustrate +what these tables look like as Core table metadata: .. sourcecode:: python+sql - employees_table = Table('employees', metadata, - Column('employee_id', Integer, primary_key=True), + employees_table = Table( + 'employee', metadata, + Column('id', Integer, primary_key=True), Column('name', String(50)), ) - managers_table = Table('managers', metadata, - Column('employee_id', Integer, primary_key=True), + managers_table = Table( + 'manager', metadata, + Column('id', Integer, primary_key=True), Column('name', String(50)), Column('manager_data', String(50)), ) - engineers_table = Table('engineers', metadata, - Column('employee_id', Integer, primary_key=True), + engineers_table = Table( + 'engineer', metadata, + Column('id', Integer, primary_key=True), Column('name', String(50)), Column('engineer_info', String(50)), ) -Notice in this case there is no ``type`` column. If polymorphic loading is not -required, there's no advantage to using ``inherits`` here; you just define a -separate mapper for each class. +Notice in this case there is no ``type`` column; for polymorphic loading, +additional steps will be needed in order to "manufacture" this information +during a query. -.. sourcecode:: python+sql +Using classical mapping, we can map our three classes independently without +any relationship between them; the fact that ``Engineer`` and ``Manager`` +inherit from ``Employee`` does not have any impact on a classical mapping:: + + class Employee(object): + pass + + class Manager(Employee): + pass + + class Engineer(Employee): + pass mapper(Employee, employees_table) mapper(Manager, managers_table) mapper(Engineer, engineers_table) -To load polymorphically, the ``with_polymorphic`` argument is required, along -with a selectable indicating how rows should be loaded. In this case we must -construct a UNION of all three tables. SQLAlchemy includes a helper function -to create these called :func:`~sqlalchemy.orm.util.polymorphic_union`, which +However when using Declarative, Declarative assumes an inheritance mapping +between the classes because they are already in an inheritance relationship. +So to map our three classes declaratively, we must include the +:paramref:`.orm.mapper.concrete` parameter within the ``__mapper_args__``:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + class Manager(Employee): + __tablename__ = 'manager' + + id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(50)) + + __mapper_args__ = { + 'concrete': True + } + + class Engineer(Employee): + __tablename__ = 'engineer' + + id = Column(Integer, primary_key=True) + name = Column(String(50)) + engineer_info = Column(String(50)) + + __mapper_args__ = { + 'concrete': True + } + +Two critical points should be noted: + +* We must **define all columns explicitly** on each subclass, even those of + the same name. A column such as + ``Employee.name`` here is **not** copied out to the tables mapped + by ``Manager`` or ``Engineer`` for us. + +* while the ``Engineer`` and ``Manager`` classes are + mapped in an inheritance relationship with ``Employee``, they still **do not + include polymorphic loading**. + +To load polymorphically, the :paramref:`.orm.mapper.with_polymorphic` argument is required, along +with a selectable indicating how rows should be loaded. Polymorphic loading +is most inefficient with concrete inheritance, so if we do seek this style of +loading, while it is possible it's less recommended. In the case of concrete +inheritance, it means we must construct a UNION of all three tables. + +First illustrating this with classical mapping, SQLAlchemy includes a helper +function to create this UNION called :func:`~sqlalchemy.orm.util.polymorphic_union`, which will map all the different columns into a structure of selects with the same numbers and names of columns, and also generate a virtual ``type`` column for -each subselect: +each subselect. The function is called **after** all three tables are declared, +and is then combined with the mappers:: -.. sourcecode:: python+sql + from sqlalchemy.orm import polymorphic_union pjoin = polymorphic_union({ 'employee': employees_table, @@ -652,34 +710,167 @@ Upon select, the polymorphic union produces a query like this: session.query(Employee).all() {opensql} - SELECT pjoin.type AS pjoin_type, - pjoin.manager_data AS pjoin_manager_data, - pjoin.employee_id AS pjoin_employee_id, - pjoin.name AS pjoin_name, pjoin.engineer_info AS pjoin_engineer_info + SELECT + pjoin.id AS pjoin_id, + pjoin.name AS pjoin_name, + pjoin.type AS pjoin_type, + pjoin.manager_data AS pjoin_manager_data, + pjoin.engineer_info AS pjoin_engineer_info FROM ( - SELECT employees.employee_id AS employee_id, - CAST(NULL AS VARCHAR(50)) AS manager_data, employees.name AS name, - CAST(NULL AS VARCHAR(50)) AS engineer_info, 'employee' AS type - FROM employees - UNION ALL - SELECT managers.employee_id AS employee_id, - managers.manager_data AS manager_data, managers.name AS name, - CAST(NULL AS VARCHAR(50)) AS engineer_info, 'manager' AS type - FROM managers - UNION ALL - SELECT engineers.employee_id AS employee_id, - CAST(NULL AS VARCHAR(50)) AS manager_data, engineers.name AS name, - engineers.engineer_info AS engineer_info, 'engineer' AS type - FROM engineers + SELECT + employee.id AS id, + employee.name AS name, + CAST(NULL AS VARCHAR(50)) AS manager_data, + CAST(NULL AS VARCHAR(50)) AS engineer_info, + 'employee' AS type + FROM employee + UNION ALL + SELECT + manager.id AS id, + manager.name AS name, + manager.manager_data AS manager_data, + CAST(NULL AS VARCHAR(50)) AS engineer_info, + 'manager' AS type + FROM manager + UNION ALL + SELECT + engineer.id AS id, + engineer.name AS name, + CAST(NULL AS VARCHAR(50)) AS manager_data, + engineer.engineer_info AS engineer_info, + 'engineer' AS type + FROM engineer ) AS pjoin - [] -Concrete Inheritance with Declarative -++++++++++++++++++++++++++++++++++++++ +The above UNION query needs to manufacture "NULL" columns for each subtable +in order to accommodate for those columns that aren't part of the mapping. + +In order to map with concrete inheritance and polymorphic loading using +Declarative, the challenge is to have the polymorphic union ready to go +when the mappings are created. One way to achieve this is to continue to +define the table metadata before the actual mapped classes, and specify +them to each class using ``__table__``:: + + class Employee(Base): + __table__ = employee_table + __mapper_args__ = { + 'polymorphic_on':pjoin.c.type + 'with_polymorphic': ('*', pjoin), + 'polymorphic_identity':'employee' + } + + class Engineer(Employee): + __table__ = engineer_table + __mapper_args__ = {'polymorphic_identity':'engineer', 'concrete':True} + + class Manager(Employee): + __table__ = manager_table + __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True} + +Another way is to use a special helper class that takes on the fairly +complicated task of deferring the production of :class:`.Mapper` objects +until all table metadata has been collected, and the polymorphic union to which +the mappers will be associated will be available. This is available via +the :class:`.AbstractConcreteBase` and :class:`.ConcreteBase` classes. For +our example here, we're using a "concrete" base, e.g. an ``Employee`` row +can exist by itself that is not an ``Engineer`` or a ``Manager``. The +mapping would look like:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True + } + + class Manager(Employee): + __tablename__ = 'manager' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True + } + + class Engineer(Employee): + __tablename__ = 'engineer' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + engineer_info = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'engineer', + 'concrete':True + } + +There is also the option to use a so-called "abstract" base; where we wont +actually have an ``employee`` table at all, and instead will only have +``manager`` and ``engineer`` tables. The ``Employee`` class will never be +instantiated directly. The change here is that the base mapper is mapped +directly to the "polymorphic union" selectable, which no longer includes +the ``employee`` table. In classical mapping, this is:: + + from sqlalchemy.orm import polymorphic_union + + pjoin = polymorphic_union({ + 'manager': managers_table, + 'engineer': engineers_table + }, 'type', 'pjoin') + + employee_mapper = mapper(Employee, pjoin, + with_polymorphic=('*', pjoin), + polymorphic_on=pjoin.c.type) + manager_mapper = mapper(Manager, managers_table, + inherits=employee_mapper, + concrete=True, + polymorphic_identity='manager') + engineer_mapper = mapper(Engineer, engineers_table, + inherits=employee_mapper, + concrete=True, + polymorphic_identity='engineer') + +Using the Declarative helpers, the :class:`.AbstractConcreteBase` helper +can produce this; the mapping would be:: + + from sqlalchemy.ext.declarative import AbstractConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + + class Manager(Employee): + __tablename__ = 'manager' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True + } + + class Engineer(Employee): + __tablename__ = 'engineer' + id = Column(Integer, primary_key=True) + name = Column(String(50)) + engineer_info = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'engineer', + 'concrete':True + } + +.. seealso:: + + :ref:`declarative_concrete_table` - in the Declarative reference documentation -.. versionadded:: 0.7.3 - The :ref:`declarative_toplevel` module includes helpers for concrete - inheritance. See :ref:`declarative_concrete_helpers` for more information. + :ref:`declarative_concrete_helpers` - in the Declarative reference documentation Using Relationships with Inheritance ------------------------------------