[英]How do I construct a self-referential/recursive SQLModel
我想使用 SQLModel 定义一个具有自引用(或递归)外键的SQLModel
。 (这种关系模式有时也称为邻接列表。)纯SQLAlchemy
实现在其文档中进行了描述。
假设我想实现上面链接的SQLAlchemy
示例中描述的基本树结构,其中我有一个Node
model 并且每个实例都有一个id
主键、一个data
字段(例如str
类型)和一个可选引用(读取外键)到另一个我们称之为父节点的节点(字段名称parent_id
)。
理想情况下,每个Node
object 都应该有一个parent
属性,如果该节点没有父节点,则该属性将为None
; 否则它将包含(指向)父Node
object 的指针。
更好的是,每个Node
object 都应该有一个children
属性,这将是一个Node
对象的列表,将其作为其父级引用。
问题是双重的:
用SQLModel
实现这一点的优雅方法是什么?
我将如何创建这样的节点实例并将它们插入数据库?
sqlmodel.Relationship
sa_relationship_kwargs
允许将其他关键字参数显式传递给sqlalchemy.orm.relationship
构造函数,该构造函数通过参数调用。 此参数需要将表示SQLAlchemy
参数名称的字符串映射到我们希望作为 arguments 传递的值。
由于SQLAlchemy
关系为这种情况提供了remote_side
参数,我们可以直接利用它以最少的代码构造自引用模式。 文档顺便提到了这一点,但至关重要的是remote_side
值
使用声明式时,可以作为 Python 可评估的字符串传递。
这正是我们所需要的。 唯一缺少的是正确使用back_populates
参数,我们可以像这样构建 model:
from typing import Optional
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class Node(SQLModel, table=True):
__tablename__ = 'node' # just to be explicit
id: Optional[int] = Field(default=None, primary_key=True)
data: str
parent_id: Optional[int] = Field(
foreign_key='node.id', # notice the lowercase "n" to refer to the database table name
default=None,
nullable=True
)
parent: Optional['Node'] = Relationship(
back_populates='children',
sa_relationship_kwargs=dict(
remote_side='Node.id' # notice the uppercase "N" to refer to this table class
)
)
children: list['Node'] = Relationship(back_populates='parent')
# more code below...
旁注:我们按照SQLModel
的惯例将id
定义为可选的,以避免在我们想要创建实例时被 IDE 唠叨,只有在我们将其添加到数据库后才能知道该实例的id
。 parent_id
和parent
属性显然被定义为可选,因为并非每个节点都需要在我们的 model 中有父节点。
为了测试一切都按照我们期望的方式工作:
def test() -> None:
# Initialize database & session:
sqlite_file_name = 'database.db'
sqlite_uri = f'sqlite:///{sqlite_file_name}'
engine = create_engine(sqlite_uri, echo=True)
SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
session = Session(engine)
# Initialize nodes:
root_node = Node(data='I am root')
# Set the children's `parent` attributes;
# the parent nodes' `children` lists are then set automatically:
node_a = Node(parent=root_node, data='a')
node_b = Node(parent=root_node, data='b')
node_aa = Node(parent=node_a, data='aa')
node_ab = Node(parent=node_a, data='ab')
# Add to the parent node's `children` list;
# the child node's `parent` attribute is then set automatically:
node_ba = Node(data='ba')
node_b.children.append(node_ba)
# Commit to DB:
session.add(root_node)
session.commit()
# Do some checks:
assert root_node.children == [node_a, node_b]
assert node_aa.parent.parent.children[1].parent is root_node
assert node_ba.parent.data == 'b'
assert all(n.data.startswith('a') for n in node_ab.parent.children)
assert (node_ba.parent.parent.id == node_ba.parent.parent_id == root_node.id) \
and isinstance(root_node.id, int)
if __name__ == '__main__':
test()
所有断言都得到满足,测试运行顺利。
此外,通过使用数据库引擎的echo=True
开关,我们可以在日志 output 中验证该表是否按预期创建:
CREATE TABLE node (
id INTEGER,
data VARCHAR NOT NULL,
parent_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(parent_id) REFERENCES node (id)
)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.