Skip to content

heritage

Of the various types of ORM model inheritance, ormar currently supports two:

  • sneak into
  • Specific table inheritance (parent set to abstract=True)

inheritance type

A short summary of the different types of inheritance:

  • Mixins [Support] -Do not subclass ormar.Model, just define fields that are used later on different models (e.g. created_date and updated_date on each model), only the actual model creates tables, but add fields from mixins
  • concrete table inheritance[SUPPORTED] -means that the parent is marked abstract and each child has its own table containing columns from the parent and its own child columns, somewhat similar to Mixins, but the parent is also a model
  • single table inheritance[not supported] -means creating only one table where the fields are the combination/sum of the parent model and all child models, but the child model uses only a subset of the columns in the database (all parent models and its own columns, skipping other child models)
  • multi/join table inheritance[not supported] -means that some columns are saved on the parent model and some on the child model, they are connected to each other by one-to-one relationships, and both models are operated in the background immediately
  • Proxy model[not supported] -means that only the parent has an actual table, children just add methods, modify settings, etc.

sneak into

To use Mixins, simply define a class that does not inherit from ormar.Model, but ormar.Fields as a class variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
    engine=sqlalchemy.create_engine(DATABASE_URL),
)


# a mixin defines the fields but is a normal python class 
class AuditMixin:
    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


class DateFieldsMixins:
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
    updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# a models can inherit from one or more mixins
class Category(ormar.Model, DateFieldsMixins, AuditMixin):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

!!!tip Note that Mixin is not a model, so you still need to inherit ormar.Model and define the ormar_config field in the final model.

The Category class above will have four additional fields: created_date, updated_date, created_by, and Updated_by.

Only one table is created for the model Category, where the Category class field is combined with all Mixins fields.

Note that Mixin in the class name is optional, but this is a good Python practice.

concrete table inheritance

Conceptually, concrete table inheritance is very similar to Mixins, but uses the actual ormar.Models as the base class.

!!!Warning Note that the base class has Abstract=True set in the ormar_config object, and ModelDefinitionError will be raised if you attempt to inherit from a non-abstract tag class.

Since this abstract model is never initialized, you can skip metadata and databases in its ormar_config definition.

But if you provide it-it will be inherited so you don't have to provide metadata and databases in the final/concrete class

Note that you can always override it in a child/concrete class if desired.

More importantly, at least one class in the inheritance chain must provide both database and metadata-otherwise an error will be raised.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# note that base classes have abstract=True
# since this model will never be initialized you can skip metadata and database
class AuditModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


# but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action
class DateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(
        abstract=True,
        metadata=metadata,
        database=db,
    )

    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
    updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# that way you do not have to provide metadata and databases in concrete class
class Category(DateFieldsModel, AuditModel):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

The list of inherited options/settings is as follows: metadata, database, and constraints.

Except, of course, all the fields in the base class are combined and created in concrete tables in the final model.

!!!Tip Note that you do not have to provide abstarct = False in the final class-this is the default setting that is not inherited.

Redefine fields in subclasses

Note that you can redefine previously created fields just like normal Python class inheritance.

Whenever you define a field with the same name and a new definition, it completely replaces the previously defined field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# base class
class DateFieldsModel(ormar.Model):
    ormar_config = OrmarConfig(
        abstract=True,
        metadata=metadata,
        database=db,
        # note that UniqueColumns need sqlalchemy db columns names not the ormar ones
        constraints=[ormar.UniqueColumns("creation_date", "modification_date")]
    )

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )
    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class RedefinedField(DateFieldsModel):
    ormar_config = OrmarConfig(
        tablename="redefines",
        metadata=metadata,
        database=db,
    )

    id: int = ormar.Integer(primary_key=True)
    # here the created_date is replaced by the String field
    created_date: str = ormar.String(max_length=200, name="creation_date")


# you can verify that the final field is correctly declared and created
changed_field = RedefinedField.ormar_config.model_fields["created_date"]
assert changed_field.default is None
assert changed_field.alias == "creation_date"
assert any(x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns)
assert isinstance(
    RedefinedField.ormar_config.table.columns["creation_date"].type,
    sqlalchemy.sql.sqltypes.String,
)

!!!Warning If you declare UniqueColumns constraints using column names, the final model must declare columns with the same names. Otherwise, ModelDefinitionError will be raised.

1
2
3
4
5
6
So in example above if you do not provide `name` for `created_date` in `RedefinedField` model
ormar will complain.

`created_date: str = ormar.String(max_length=200) # exception`

`created_date: str = ormar.String(max_length=200, name="creation_date2") # exception`

inheritance relationship

You can declare relationships at every step of inheritance, whether in a parent or child class.

When you define a relationship at the child model level, it overrides the relationship defined in the parent model if you use the same field name, or if you define a new relationship, it is accessible only by that child model.

When inheriting relationships, you always need to pay attention to the related_name parameter, which must be unique in the related model when you define multiple subclasses that inherit the same relationship.

If you do not provide the related_name parameter, ormar calculates it for you. This applies to inheritance because all submodels must have different class names, which are used to calculate the default related_name (class.name.lower()+'s').

However, if you provide related_name, that name cannot be reused in all submodels because they overlap each other at the related model end.

So you have two choices:

  • Redefine the relationship fields in the submodel and manually provide different related_name parameters
  • let ormar process-> auto-adjusted related_name is: original related_name + "_" + sub-model table name

This may sound complicated, but let's look at the following example:

foreign key relationship

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# normal model used in relation
class Person(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)


# parent model - needs to be abstract
class Car(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50)
    owner: Person = ormar.ForeignKey(Person)
    # note that we refer to the Person model again so we **have to** provide related_name
    co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


class Truck(Car):
    ormar_config = base_ormar_config.copy()

    max_capacity: int = ormar.Integer()


class Bus(Car):
    ormar_config = base_ormar_config.copy(tablename="buses")

    max_persons: int = ormar.Integer()

Now, when you examine the fields on the Person model, you get:

1
2
3
4
5
6
7
8
9
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buss': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>}
"""

Notice how you own the Truck and Bus fields, which result in the Truck and Bus categories this person owns. There is no related_name parameter, so default names are used.

Co-owned cars need to be referenced by coowned_trucks and coowned_buses. Ormar appends the_trucks and_buses suffixes from the child model table names.

It looks good, but the default name for owned trucks is good (truck), but buses are ugly, so how do we change it?

The solution is simple-just redefine the fields in the Bus class and provide different related_name parameters.

1
2
3
4
5
6
7
# rest of the above example remains the same
class Bus(Car):
    ormar_config = base_ormar_config.copy(tablename="buses")

    # new field that changes the related_name
    owner: Person = ormar.ForeignKey(Person, related_name="buses")
    max_persons: int = ormar.Integer()

The post looks better now.

1
2
3
4
5
6
7
8
9
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>}
"""

!!!Note that you can also provide related_name for the owner field, which adds the correct suffix.

1
2
3
`owner: Person = ormar.ForeignKey(Person, related_name="owned")`

and model fields for Person owned cars would become `owned_trucks` and `owned_buses`.

many-to-many relationship

Similarly, you can inherit from a model that declares the ManyToMany relationship, but with one substantial difference-the Through model.

Since the Through model will be able to hold other fields, and now it links only two tables (from and to one table), each child that inherits an m2m relational field must have a separate Through model.

Of course, you can override relationships in each submodel, but this requires extra code and destroys the meaning of the entire inheritance. Ormar will handle this for you if you agree with the default naming convention, and you can override the convention manually in a subproject at any time if you want.

Again, let's look at examples to get a better grasp of the concept.

We will modify the previous example described above to use the m2m relationship of co_owners.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# person remain the same as above
class Person(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)

# new through model between Person and Car2
class PersonsCar(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="cars_x_persons")

# note how co_owners is now ManyToMany relation
class Car2(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50)
    # note the related_name - needs to be unique across Person
    # model, regardless of how many different models leads to Person
    owner: Person = ormar.ForeignKey(Person, related_name="owned")
    co_owners: List[Person] = ormar.ManyToMany(
        Person, through=PersonsCar, related_name="coowned"
    )
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# child models define only additional Fields
class Truck2(Car2):
    ormar_config = base_ormar_config.copy(tablename="trucks2")

    max_capacity: int = ormar.Integer()


class Bus2(Car2):
    ormar_config = base_ormar_config.copy(tablename="buses2")

    max_persons: int = ormar.Integer()

Ormar automatically modifies the related_name of the field to include the table name of the submodel. The default name is the original related_name +'_'+ subtable name.

Thus for Truck2 class, the defined relationship owner: Person = ormar.ForeignKey(Person, related_name="owned") becomes owned_trucks2

You can verify the name by examining the list of fields that exist on the people model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Person.ormar_config.model_fields
{
# note how all relation fields need to be unique on Person
# regardless if autogenerated or manually overwritten
'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
# note that we expanded on previous example so all 'old' fields are here
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
# newly defined related fields
'owned_trucks2': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks2': <class 'abc.ManyToMany'>, 
'owned_buses2': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses2': <class 'abc.ManyToMany'>
}

But that's not all. It's the internal structure of ormar, but it affects the data structure in the database, so let's examine the pass-through model for Bus2 and Truck2 models.

1
2
3
4
5
6
7
8
9
Bus2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarBus2'>
Bus2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_buses2'

Truck2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarTruck2'>
Truck2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_trucks2'

As you can see above, ormar clones the Through model for each subclass and adds the subclass name at the end, changing the table name of the cloned field to use the child's table name.

Note that not only is the original model not used, but the tables of that model are also deleted from the metadata:

1
2
3
Bus2.ormar_config.metadata.tables.keys()
dict_keys(['test_date_models', 'categories', 'subjects', 'persons', 'trucks', 'buses', 
           'cars_x_persons_trucks2', 'trucks2', 'cars_x_persons_buses2', 'buses2'])

So be aware that if you introduce inheritance all the way and convert the model to an abstract parent model, you might lose data in the table if you're not careful.

!!!note Note that the original table name and model name of the Through model are never used. Only cloned model tables are created and used.

!!!Warning Note that each subclass of the model that defines the ManyToMany relationship generates a new Through model, which also means new database tables.

1
2
That means that each time you define a Child model you need to either manually create
the table in the database, or run a migration (with alembic).

exclude parent field

Ormar allows you to skip certain fields in an inherited model from a parent model.

!!!Note that the same behavior can be achieved by splitting the model into more abstract models and mixins-which is the preferred way in normal cases.

To skip certain fields in the submodel, list all fields to skip model.ormar_config.exclude_parent_fields parameters as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
base_ormar_config = OrmarConfig(
    metadata=sa.MetaData(),
    database=databases.Database(DATABASE_URL),
)


class AuditModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


class DateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )
    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class Category(DateFieldsModel, AuditModel):
    ormar_config = base_ormar_config.copy(
        tablename="categories",
        # set fields that should be skipped
        exclude_parent_fields=["updated_by", "updated_date"],
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

# Note that now the update fields in Category are gone in all places -> ormar fields, pydantic fields and sqlachemy table columns
# so full list of available fields in Category is: ["created_by", "created_date", "id", "name", "code"]

Note that you only need to provide the field name, which excludes the parent field, regardless of which parent model the field comes from.

!!!Note that if you want to override a field in a submodel, you don't have to exclude it, just overwrite the field declaration in the submodel with the same field name.

Warning Note that this behavior may confuse mypy and static type checker, but accessing nonexistent fields will fail at runtime. This is why splitting base classes is preferred.

The same effect can be achieved by splitting the base class, for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
base_ormar_config = OrmarConfig(
    metadata=sa.MetaData(),
    database=databases.Database(DATABASE_URL),
)


class AuditCreateModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)


class AuditUpdateModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    updated_by: str = ormar.String(max_length=100, default="Sam")

class CreateDateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )

class UpdateDateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class Category(CreateDateFieldsModel, AuditCreateModel):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

That way, you can inherit from both the create class and the update class if you want, otherwise you can inherit only one of them.