Inheritance Configuration
Declarative supports all three forms of inheritance as intuitivelyas possible. The inherits
mapper keyword argument is not neededas declarative will determine this from the class itself. The various“polymorphic” keyword arguments are specified using mapper_args
.
See also
This section describes some specific details on how the Declarative systeminteracts with SQLAlchemy ORM inheritance configuration. SeeMapping Class Inheritance Hierarchies for a general introduction to inheritancemapping.
Joined Table Inheritance
Joined table inheritance is defined as a subclass that defines its owntable:
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- discriminator = Column('type', String(50))
- __mapper_args__ = {'polymorphic_on': discriminator}
- class Engineer(Person):
- __tablename__ = 'engineers'
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- id = Column(Integer, ForeignKey('people.id'), primary_key=True)
- primary_language = Column(String(50))
Note that above, the Engineer.id
attribute, since it shares thesame attribute name as the Person.id
attribute, will in factrepresent the people.id
and engineers.id
columns together,with the “Engineer.id” column taking precedence if queried directly.To provide the Engineer
class with an attribute that representsonly the engineers.id
column, give it a different attribute name:
- class Engineer(Person):
- __tablename__ = 'engineers'
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- engineer_id = Column('id', Integer, ForeignKey('people.id'),
- primary_key=True)
- primary_language = Column(String(50))
Single Table Inheritance
Single table inheritance is defined as a subclass that does not haveits own table; you just leave out the table
and tablename
attributes:
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- discriminator = Column('type', String(50))
- __mapper_args__ = {'polymorphic_on': discriminator}
- class Engineer(Person):
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- primary_language = Column(String(50))
When the above mappers are configured, the Person
class is mappedto the people
table before the primarylanguage
column isdefined, and this column will not be included in its own mapping.When Engineer
then defines the primary_language
column, thecolumn is added to the people
table so that it is included in themapping for Engineer
and is also part of the table’s full set ofcolumns. Columns which are not mapped to Person
are also excludedfrom any other single or joined inheriting classes using theexclude_properties
mapper argument. Below, Manager
will haveall the attributes of Person
and Manager
but _not theprimary_language
attribute of Engineer
:
- class Manager(Person):
- __mapper_args__ = {'polymorphic_identity': 'manager'}
- golf_swing = Column(String(50))
The attribute exclusion logic is provided by theexcludeproperties
mapper argument, and declarative’s defaultbehavior can be disabled by passing an explicit excludeproperties
collection (empty or otherwise) to the __mapper_args
.
Resolving Column Conflicts
Note above that the primarylanguage
and golfswing
columnsare “moved up” to be applied to Person._table
, as a result of theirdeclaration on a subclass that has no table of its own. A tricky casecomes up when two subclasses want to specify _the same column, as below:
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- discriminator = Column('type', String(50))
- __mapper_args__ = {'polymorphic_on': discriminator}
- class Engineer(Person):
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- start_date = Column(DateTime)
- class Manager(Person):
- __mapper_args__ = {'polymorphic_identity': 'manager'}
- start_date = Column(DateTime)
Above, the start_date
column declared on both Engineer
and Manager
will result in an error:
- sqlalchemy.exc.ArgumentError: Column 'start_date' on class
- <class '__main__.Manager'> conflicts with existing
- column 'people.start_date'
In a situation like this, Declarative can’t be sureof the intent, especially if the startdate
columns had, for example,different types. A situation like this can be resolved by usingdeclared_attr
to define the Column
conditionally, takingcare to return the existing column via the parent _table
if italready exists:
- from sqlalchemy.ext.declarative import declared_attr
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- discriminator = Column('type', String(50))
- __mapper_args__ = {'polymorphic_on': discriminator}
- class Engineer(Person):
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- @declared_attr
- def start_date(cls):
- "Start date column, if not present already."
- return Person.__table__.c.get('start_date', Column(DateTime))
- class Manager(Person):
- __mapper_args__ = {'polymorphic_identity': 'manager'}
- @declared_attr
- def start_date(cls):
- "Start date column, if not present already."
- return Person.__table__.c.get('start_date', Column(DateTime))
Above, when Manager
is mapped, the start_date
column isalready present on the Person
class. Declarative lets us returnthat Column
as a result in this case, where it knows to skipre-assigning the same column. If the mapping is mis-configured suchthat the start_date
column is accidentally re-assigned to adifferent table (such as, if we changed Manager
to be joinedinheritance without fixing start_date
), an error is raised whichindicates an existing Column
is trying to be re-assigned toa different owning Table
.
The same concept can be used with mixin classes (seeMixin and Custom Base Classes):
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- discriminator = Column('type', String(50))
- __mapper_args__ = {'polymorphic_on': discriminator}
- class HasStartDate(object):
- @declared_attr
- def start_date(cls):
- return cls.__table__.c.get('start_date', Column(DateTime))
- class Engineer(HasStartDate, Person):
- __mapper_args__ = {'polymorphic_identity': 'engineer'}
- class Manager(HasStartDate, Person):
- __mapper_args__ = {'polymorphic_identity': 'manager'}
The above mixin checks the local table
attribute for the column.Because we’re using single table inheritance, we’re sure that in this case,cls.table
refers to Person.table
. If we were mixing joined-and single-table inheritance, we might want our mixin to check more carefullyif cls.table
is really the Table
we’re looking for.
Concrete Table Inheritance
Concrete is defined as a subclass which has its own table and sets theconcrete
keyword argument to True
:
- class Person(Base):
- __tablename__ = 'people'
- id = Column(Integer, primary_key=True)
- name = Column(String(50))
- class Engineer(Person):
- __tablename__ = 'engineers'
- __mapper_args__ = {'concrete':True}
- id = Column(Integer, primary_key=True)
- primary_language = Column(String(50))
- name = Column(String(50))
Usage of an abstract base class is a little less straightforward as itrequires usage of polymorphic_union()
,which needs to be created with the Table
objectsbefore the class is built:
- engineers = Table('engineers', Base.metadata,
- Column('id', Integer, primary_key=True),
- Column('name', String(50)),
- Column('primary_language', String(50))
- )
- managers = Table('managers', Base.metadata,
- Column('id', Integer, primary_key=True),
- Column('name', String(50)),
- Column('golf_swing', String(50))
- )
- punion = polymorphic_union({
- 'engineer':engineers,
- 'manager':managers
- }, 'type', 'punion')
- class Person(Base):
- __table__ = punion
- __mapper_args__ = {'polymorphic_on':punion.c.type}
- class Engineer(Person):
- __table__ = engineers
- __mapper_args__ = {'polymorphic_identity':'engineer', 'concrete':True}
- class Manager(Person):
- __table__ = managers
- __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
The helper classes AbstractConcreteBase
and ConcreteBase
provide automation for the above system of creating a polymorphic union.See the documentation for these helpers as well as the main ORM documentationon concrete inheritance for details.
See also
inheritance_concrete_helpers