You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It's a pretty common use case in SQLAlchemy to split models up into multiple modules/files. These models will often refer to each other using ForeignKey objects, which accept either a Model.attribute or Table.column syntax. To avoid circular dependencies in Python, the Table.column syntax is often used, wherein a string is passed to ForeignKey to be resolved later. In the follow example, two related models are split between two files. Even though one is loaded before the other, no exception is raised because SQLAlchemy delays resolution until all the modules have been loaded.
models/init.py
from .employeeimportEmployeefrom .managerimportManager
Another way of structuring the above is to treat a manager as a subclass of employee. Internally, additional fields specified in the Manager class are put in a separate table. The database is queried with a JOIN on employee to retrieve all the fields for a manager.
This creates a situation where a foreign key is inherited from the base class. In the below example, the id of a child model is set to a foreign key--the id of the base class.
models/init.py
from .employeeimportEmployee, Managerfrom .reportimportReport
When creating a Mapper for a history table, chrononaut is aware of models that inherit from other classes. It checks for inheritance by setting super_mapper to the Mapper object of the base class. Later on, it inspects columns to see if there are foreign keys referencing the base class:
Normally, this works fine. In the above example, both Manager and Employee are in the same file, and if they weren't, we would still need to make sure that Employee was imported before declaring Manager.
The problem arises when considering Report. This model also has a foreign key, but it is not declared at the same time as Employee. The code that makes model inheritance from a base class work also breaks other foreign keys.
The issue is that col_references_table, the method that checks for an inherited foreign key, calls ForeignKey.references(). This method immediately tries to resolve Table.column strings rather than waiting until all the models have been loaded. This behavior breaks our ability to split related models across files.
Resolution
In the case of an inherited foreign key, the base class will already have been loaded. Therefore, ForeignKey.references() will not raise an exception. It is only in the case that the foreign key is not inherited that there is an issue. In this case, a sqlalchemy.exc.NoReferencedTableError exception is raised. If this exception is raised, we know for a fact that the key was not inherited.
The proposed fix is to just catch this exception and return False.
Second Issue
There are two additional spots in chrononaut where model resolution is forced via accessing the SQLAlchemy API.
Similarly, accessing .attrs on a mapper tries to resolve Table.column immediately.
Resolution
These can be fixed by using ._props, which is the private API that exists before the resolver runs. I think it's OK to rely on this since we are already deep into SQLAlchemy's API and many things (public or private) changed upstream will likely affect this package.
The text was updated successfully, but these errors were encountered:
polyatail
changed the title
Code to allow self-referential foreign keys breaks deferred string resolution
Code to allow self-referential foreign keys breaks deferred model resolution
Apr 4, 2019
polyatail
changed the title
Code to allow self-referential foreign keys breaks deferred model resolution
Code to allow inherited foreign keys breaks deferred model resolution
Apr 5, 2019
Background
It's a pretty common use case in SQLAlchemy to split models up into multiple modules/files. These models will often refer to each other using
ForeignKey
objects, which accept either aModel.attribute
orTable.column
syntax. To avoid circular dependencies in Python, theTable.column
syntax is often used, wherein a string is passed toForeignKey
to be resolved later. In the follow example, two related models are split between two files. Even though one is loaded before the other, no exception is raised because SQLAlchemy delays resolution until all the modules have been loaded.models/init.py
models/employee.py
models/manager.py
Another way of structuring the above is to treat a manager as a subclass of employee. Internally, additional fields specified in the Manager class are put in a separate table. The database is queried with a JOIN on employee to retrieve all the fields for a manager.
This creates a situation where a foreign key is inherited from the base class. In the below example, the
id
of a child model is set to a foreign key--theid
of the base class.models/init.py
models/employee.py
models/report.py
First Issue
When creating a
Mapper
for a history table,chrononaut
is aware of models that inherit from other classes. It checks for inheritance by settingsuper_mapper
to theMapper
object of the base class. Later on, it inspects columns to see if there are foreign keys referencing the base class:Normally, this works fine. In the above example, both
Manager
andEmployee
are in the same file, and if they weren't, we would still need to make sure thatEmployee
was imported before declaringManager
.The problem arises when considering
Report
. This model also has a foreign key, but it is not declared at the same time asEmployee
. The code that makes model inheritance from a base class work also breaks other foreign keys.The issue is that
col_references_table
, the method that checks for an inherited foreign key, callsForeignKey.references()
. This method immediately tries to resolveTable.column
strings rather than waiting until all the models have been loaded. This behavior breaks our ability to split related models across files.Resolution
In the case of an inherited foreign key, the base class will already have been loaded. Therefore,
ForeignKey.references()
will not raise an exception. It is only in the case that the foreign key is not inherited that there is an issue. In this case, asqlalchemy.exc.NoReferencedTableError
exception is raised. If this exception is raised, we know for a fact that the key was not inherited.The proposed fix is to just catch this exception and return
False
.Second Issue
There are two additional spots in
chrononaut
where model resolution is forced via accessing the SQLAlchemy API.In the above,
iterate_properties
tries to resolve everything before allowing you to iterate over the mapper's properties.Similarly, accessing
.attrs
on a mapper tries to resolveTable.column
immediately.Resolution
These can be fixed by using
._props
, which is the private API that exists before the resolver runs. I think it's OK to rely on this since we are already deep into SQLAlchemy's API and many things (public or private) changed upstream will likely affect this package.The text was updated successfully, but these errors were encountered: