Polymorphic Entities

Introduction

Polymorphic entities are entities that inherit from the same base model, are stored in the same collection but can have different properties and behaviours depending on a guard value.

They are usually available to provide inheritance and specialised behaviours in models as you would for Python subclasses.

Declaring Polymorphic Entities

First we need to declare our base model which will provide the common properties and the property on which the model identity is recognized:

class Transport(MappedClass):
    class __mongometa__:
        session = session
        name = 'transport'
        polymorphic_on = '_type'
        polymorphic_identity = 'base'

    _id = FieldProperty(schema.ObjectId)
    origin = FieldProperty(schema.String(required=True))
    destination = FieldProperty(schema.String(if_missing=''))
    _type = FieldProperty(schema.String(if_missing='base'))

    def move(self):
        return f'moving from {self.origin} to {self.destination}'

The _type property is used to store the identity of the model, we usually won’t be writing this property, it will be automatically filled using if_missing with a different value for each subclass.

__mongometa__.polymorphic_on attribute is used to tell Ming on which property the polymorphism is happening and __mongometa__.polymorphic_identity will be provided by each class to tell which value of _type is bound to that class:

class Bus(Transport):
    class __mongometa__:
        polymorphic_identity = 'bus'

    _type=FieldProperty(str, if_missing='bus')
    passengers_count = FieldProperty(schema.Int(if_missing=0))

    def move(self):
        return f'driving from {self.origin} to {self.destination}'
class AirBus(Bus):
    class __mongometa__:
        polymorphic_identity = 'airbus'

    _type=FieldProperty(str, if_missing='airbus')
    wings_count = FieldProperty(schema.Int(if_missing=2))

    def move(self):
        return f'flying from {self.origin} to {self.destination}'

Now we can create Bus or AirBus instances freely and use the additional properties provided by each one of them:

>>> # Create 2 Bus
>>> Bus(origin='Rome', destination='London', passengers_count=20)
<Bus _type='bus' passengers_count=20
  _id=ObjectId('64135757bd1c2ce4e0454e09') origin='Rome'
  destination='London'>
>>> Bus(origin='Turin', destination='London', passengers_count=20)
<Bus _type='bus' passengers_count=20
  _id=ObjectId('64135757bd1c2ce4e0454e0a') origin='Turin'
  destination='London'>
>>> # And an AirBus
>>> AirBus(origin='Turin', destination='London', passengers_count=60, wings_count=3)
<AirBus _type='airbus' wings_count=3 passengers_count=60
  _id=ObjectId('64135757bd1c2ce4e0454e0b') origin='Turin'
  destination='London'>
>>> session.flush()

When querying them back we can see that they all ended up in the Transport collection and that as expected only two Bus instances got created (the third is an AirBus):

>>> session.clear()
>>> Transport.query.find().count()
3
>>> Transport.query.find({'_type': 'bus'}).count()
2

Querying Polymorphic Entities

When querying back polymorphic entities you should always query them back from their base type (in this case Transport) as the type promotion will be automatically performed by ming:

>>> Transport.query.find().all()
[<Bus _type='bus' passengers_count=20
  _id=ObjectId('64135757bd1c2ce4e0454e09') origin='Rome'
  destination='London'>, <Bus _type='bus' passengers_count=20
  _id=ObjectId('64135757bd1c2ce4e0454e0a') origin='Turin'
  destination='London'>, <AirBus _type='airbus' wings_count=3 passengers_count=60
  _id=ObjectId('64135757bd1c2ce4e0454e0b') origin='Turin'
  destination='London'>]

As types are properly promoted it is also possible to rely on behaviours specific of each single subclass:

>>> transports = Transport.query.find().all()
>>> transports[0].move()
'driving from Rome to London'
>>> transports[1].move()
'driving from Turin to London'
>>> transports[2].move()
'flying from Turin to London'