I want to define a model that has a self-referential (or recursive) foreign key using SQLModel. (This relationship pattern is also sometimes referred to as an adjacency list.) The pure SQLAlchemy implementation is described here in their documentation.
Let's say I want to implement the basic tree structure as described in the SQLAlchemy example linked above, where I have a Node model and each instance has an id primary key, a data field (say of type str), and an optional reference (read foreign key) to another node that we call its parent node (field name parent_id).
Ideally, every Node object should have a parent attribute, which will be None, if the node has no parent node; otherwise it will contain (a pointer to) the parent Node object.
And even better, every Node object should have a children attribute, which will be a list of Node objects that reference it as their parent.
The question is twofold:
What is an elegant way to implement this with
SQLModel?How would I create such node instances and insert them into the database?
The
sqlmodel.Relationshipfunction allows explicitly passing additional keyword-arguments to thesqlalchemy.orm.relationshipconstructor that is being called under the hood via thesa_relationship_kwargsparameter. This parameter expects a mapping of strings representing theSQLAlchemyparameter names to the values we want to pass through as arguments.Since
SQLAlchemyrelationships provide theremote_sideparameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. The documentation mentions this in passing, but crucially theremote_sidevalueThis is exactly what we need. The only missing piece then is the proper use of the
back_populatesparameter and we can build the model like so:Side note: We define
idas optional as is customary withSQLModelto avoid being nagged by our IDE when we want to create an instance, for which theidwill only be known, after we have added it to the database. Theparent_idandparentattributes are obviously defined as optional because not every node needs to have a parent in our model.To test that everything works the way we expect it to:
All the assertions are satisfied and the test runs without a hitch.
Also, by using the
echo=Trueswitch for the database engine, we can verify in our log output that the table is created as we expected: