PHP速学视频免费教程(入门到精通)
PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
在数据库设计中,处理“文件夹包含项目”这类关系时,常常需要维护项目在文件夹中的特定顺序。最初,开发者可能倾向于采用简单的“一对多”(1:M)关系,并在父对象(Folder)中存储一个列表(例如 ARRAY(String))来记录项目ID的顺序。
class Folder(Base): __tablename__ = "folder" id = Column(Integer, primary_key=True) items = relationship( "Item", back_populates="folder", cascade="all, delete-orphan", ) item_ordering = Column(ARRAY(String), default=[]) # 存储顺序的列表 class Item(Base): __tablename__ = "item" id = Column(Integer, primary_key=True) folder_id = Column(String, ForeignKey("folder.id", ondelete="CASCADE")) folder = relationship("Folder", back_populates="items")
这种方法虽然直观,但存在一个显著的缺点:item_ordering 列表中的ID可能与实际 Folder 中 Item 对象的集合不一致,导致数据冗余和潜在的同步问题。为了提高健壮性并支持更复杂的 N:M 关系(即使在这个场景中,一个 Item 最终只属于一个 Folder,但通过关联对象可以更好地管理顺序),引入了 SQLAlchemy 的关联对象模式。
关联对象模式通过引入一个中间表(即关联对象)来管理两个实体之间的关系,并允许在该中间表中存储额外的数据,例如本例中的 order 字段。
# 关联对象定义 class FolderItemAssociation(Base): __tablename__ = "folder_item_association" project_id = Column(Integer, ForeignKey("folder.id", ondelete="CASCADE"), primary_key=True) item_id = Column(Integer, ForeignKey("item.id", ondelete="CASCADE"), primary_key=True, unique=True) order = Column(BigInteger, autoincrement=True) # 存储顺序 folder = relationship("Folder", back_populates="item_associations") item = relationship("Item", back_populates="folder_association") # Folder 和 Item 的修改 class Folder(Base): __tablename__ = "folder" id = Column(Integer, primary_key=True) item_associations = relationship( "FolderItemAssociation", back_populates="folder", order_by="desc(FolderItemAssociation.order)", single_parent=True, cascade="all, delete-orphan", ) class Item(Base): __tablename__ = "item" id = Column(Integer, primary_key=True) folder_association = relationship( "FolderItemAssociation", back_populates="item", passive_deletes=True, uselist=False, )
在这种设置下,当删除一个 Folder 对象时,预期行为是:
然而,实际测试发现,虽然 Folder 和 FolderItemAssociation 记录被删除,Item 对象却仍然存在于数据库中,成为了“孤立项”。这表明级联删除并未完全生效。
SQLAlchemy 的级联删除行为由 relationship 上的 cascade 参数控制。cascade="all, delete-orphan" 意味着当父对象被删除时,其关联的子对象也会被删除。其中 delete-orphan 选项特别重要,它指示 SQLAlchemy 跟踪子对象的“父级”状态。如果一个子对象不再与任何父对象关联,且其父关系被标记为 single_parent=True,则该子对象将被视为孤立并被删除。
在这个特定的关联对象模式中,Folder 通过 item_associations 关系级联删除了 FolderItemAssociation 实例。问题在于,当 FolderItemAssociation 实例被删除时,它并没有将关联的 Item 实例视为其“孤立”子项并触发删除。这是因为 FolderItemAssociation.item 关系缺少了必要的 cascade 和 single_parent 配置。
要解决 Item 对象未被级联删除的问题,核心在于明确 FolderItemAssociation 对其关联 Item 的所有权。这意味着在 FolderItemAssociation 类中,指向 Item 的 relationship 应该配置 cascade="all, delete-orphan" 和 single_parent=True。
from sqlalchemy import create_engine, Integer, String, BigInteger, Column, ForeignKey from sqlalchemy.orm import declarative_base, Session, relationship Base = declarative_base() class Folder(Base): __tablename__ = "folder" id = Column(Integer, primary_key=True) # Folder 通过 item_associations 关系管理 FolderItemAssociation 实例 # 当 Folder 删除时,其关联的 FolderItemAssociation 实例也会被删除 item_associations = relationship( "FolderItemAssociation", back_populates="folder", order_by="desc(FolderItemAssociation.order)", single_parent=True, # 确保 Folder 是 FolderItemAssociation 的唯一父级 cascade="all, delete-orphan", # 级联删除 FolderItemAssociation ) def __repr__(self): return f"Folder(id={self.id}, item_associations={', '.join(repr(assoc) for assoc in self.item_associations)})" class FolderItemAssociation(Base): __tablename__ = "folder_item_association" project_id = Column(Integer, ForeignKey("folder.id", ondelete="CASCADE"), primary_key=True) item_id = Column(Integer, ForeignKey("item.id", ondelete="CASCADE"), primary_key=True, unique=True) order = Column(BigInteger) # autoincrement 可能因数据库而异,此处简化 folder = relationship( "Folder", back_populates="item_associations", ) item = relationship( "Item", back_populates="folder_association", # --- 关键修改 --- # 当 FolderItemAssociation 被删除时,如果 Item 成为孤立,则删除 Item cascade="all, delete-orphan", single_parent=True # 确保 FolderItemAssociation 是 Item 在此上下文中的唯一父级 ) def __repr__(self): return f"Assoc(id={(self.project_id, self.item_id)}, order={self.order}, item={repr(self.item)})" class Item(Base): __tablename__ = "item" id = Column(Integer, primary_key=True) folder_association = relationship( "FolderItemAssociation", back_populates="item", passive_deletes=True, # 优化删除性能,避免在删除前加载关联对象 uselist=False, # 表示 Item 只有一个 FolderItemAssociation ) def __repr__(self): return f"Item(id={self.id})" # 数据库连接和表创建 (示例使用 SQLite) engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) # 辅助函数 def get_counts(session): return ( session.query(Folder).count(), session.query(FolderItemAssociation).count(), session.query(Item).count(), ) def assert_counts(session, expected_counts): counts = get_counts(session) assert counts == expected_counts, f'Expected {expected_counts} but got {counts}' def reset(session): session.query(Folder).delete() session.query(FolderItemAssociation).delete() session.query(Item).delete() session.commit() assert_counts(session, (0, 0, 0)) def create_sample_folders(session): folder1 = Folder( id=1, item_associations=[ FolderItemAssociation(item=Item(id=101)), FolderItemAssociation(item=Item(id=102)) ] ) session.add(folder1) folder2 = Folder( id=2, item_associations=[ FolderItemAssociation(item=Item(id=201)), FolderItemAssociation(item=Item(id=202)) ] ) session.add(folder2) session.commit() ### 5. 验证级联删除行为 以下测试用例将验证修改后的模型是否正确实现了级联删除: ```python def test_folder_deletion_cascades_to_items(): """ 验证删除 Folder 时,关联的 Item 是否被删除。""" with Session(engine) as session: reset(session) create_sample_folders(session) assert_counts(session, (2, 4, 4)) # 2个Folder, 4个Association, 4个Item # 删除第一个 Folder folder_to_delete = session.query(Folder).filter_by(id=1).first() session.delete(folder_to_delete) session.commit() # 预期:剩余1个Folder, 2个Association, 2个Item assert_counts(session, (1, 2, 2)) print(f"Test 1 (Folder deletion): Counts after delete: {get_counts(session)}") reset(session) def test_item_deletion_does_not_delete_folder(): """ 验证删除 Item 时,Folder 不被删除。""" with Session(engine) as session: reset(session) create_sample_folders(session) assert_counts(session, (2, 4, 4)) # 删除一个 Item item_to_delete = session.query(Item).filter_by(id=101).first() session.delete(item_to_delete) session.commit() # 预期:2个Folder, 3个Association, 3个Item (因为一个Item和它的Association被删除了) assert_counts(session, (2, 3, 3)) print(f"Test 2 (Item deletion): Counts after delete: {get_counts(session)}") reset(session) def test_association_deletion_cascades_to_item(): """ 验证删除 Association 时,关联的 Item 是否被删除。""" with Session(engine) as session: reset(session) create_sample_folders(session) assert_counts(session, (2, 4, 4)) # 删除一个 FolderItemAssociation assoc_to_delete = session.query(FolderItemAssociation).first() session.delete(assoc_to_delete) session.commit() # 预期:2个Folder, 3个Association, 3个Item (因为一个Association和它的Item被删除了) assert_counts(session, (2, 3, 3)) print(f"Test 3 (Association deletion): Counts after delete: {get_counts(session)}") reset(session) # 运行测试 test_folder_deletion_cascades_to_items() test_item_deletion_does_not_delete_folder() test_association_deletion_cascades_to_item()
通过上述修改,我们成功地在 SQLAlchemy 的关联对象模式中实现了预期的级联删除行为。核心要点在于:
通过深入理解 cascade 和 single_parent 参数的机制,开发者可以在 SQLAlchemy 中构建更复杂、更健壮的数据模型,有效管理对象生命周期和数据完整性。
已抢2128个
抢已抢2600个
抢已抢3108个
抢已抢4778个
抢已抢4185个
抢已抢34407个
抢