SQLAlchemy中的通用外键(Generic Foreign Key)实现解析

SQLAlchemy中的通用外键(Generic Foreign Key)实现解析

通用外键(Generic Foreign Key)是一种在关系型数据库中模拟多态关联的技术手段,它允许一个表的外键字段可以引用多个不同的父表。本文将深入分析SQLAlchemy中实现通用外键的技术方案,并探讨其优缺点。

什么是通用外键

通用外键是一种数据库设计模式,它通过两个字段来实现多态关联:

  • parent_id:存储关联对象的ID
  • discriminator:存储关联对象的类型标识

这种模式常见于Django、Ruby on Rails等框架中,它允许单个表(如Address)与多个不同的父表(如Customer、Supplier)建立关联关系。

SQLAlchemy实现方案解析

基础模型定义

首先定义了一个基础模型Base,它提供了自动表名和自增主键的功能:

@as_declarative()
class Base:
    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

    id = Column(Integer, primary_key=True)

地址模型(Address)

Address类是实现通用外键的核心,它包含:

  • 标准地址字段(street, city, zip)
  • discriminator字段:标识父表类型
  • parent_id字段:存储父表记录的ID
class Address(Base):
    street = Column(String)
    city = Column(String)
    zip = Column(String)
    
    discriminator = Column(String)  # 父表类型标识
    parent_id = Column(Integer)     # 父表记录ID

动态关系构建

通过HasAddresses混入类和事件监听器,动态地为每个父类构建关系:

@event.listens_for(HasAddresses, "mapper_configured", propagate=True)
def setup_listener(mapper, class_):
    name = class_.__name__
    discriminator = name.lower()
    class_.addresses = relationship(
        Address,
        primaryjoin=and_(
            class_.id == foreign(remote(Address.parent_id)),
            Address.discriminator == discriminator,
        ),
        backref=backref(
            "parent_%s" % discriminator,
            primaryjoin=remote(class_.id) == foreign(Address.parent_id),
        ),
    )

这段代码为每个继承HasAddresses的类自动创建:

  1. 一对多关系(addresses),允许父对象访问其所有地址
  2. 反向引用(parent_xxx),允许地址对象访问其父对象

使用示例

定义具体的父类模型并建立关联关系:

class Customer(HasAddresses, Base):
    name = Column(String)

class Supplier(HasAddresses, Base):
    company_name = Column(String)

使用方式与常规SQLAlchemy关系类似:

customer = Customer(
    name="customer 1",
    addresses=[
        Address(street="123 anywhere street", city="New York", zip="10110"),
        Address(street="40 main street", city="San Francisco", zip="95732"),
    ]
)

技术优缺点分析

优点

  1. 表结构简单:只需要一个关联表(Address)即可服务多个父表
  2. 灵活性高:可以随时添加新的父表类型而无需修改数据库结构
  3. 代码复用:所有关联关系使用相同的逻辑处理

缺点

  1. 缺乏数据库级完整性约束:数据库无法验证parent_id是否指向有效记录
  2. 无法使用数据库级联操作:如级联删除等需要应用层实现
  3. 查询效率较低:需要额外条件判断关联类型
  4. 复杂查询支持有限:某些复杂关联查询难以实现

替代方案建议

SQLAlchemy官方更推荐以下替代方案:

  1. 表继承策略:使用joined或single table inheritance
  2. 关联表策略:为每种关联关系创建单独的表(table_per_association)
  3. 多态关联策略:使用更严格的多态映射配置

总结

通用外键模式在某些简单场景下提供了便利,但也带来了数据完整性和查询效率方面的代价。在实际项目中,开发者需要根据具体需求权衡利弊,选择最适合的关联策略。SQLAlchemy提供了灵活的工具支持各种关联模式,理解这些模式的特性有助于做出更合理的设计决策。

对于需要严格数据完整性和复杂查询的场景,建议考虑SQLAlchemy的其他关联模式;而对于快速开发和简单关联需求,通用外键仍不失为一种可行的选择。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

from __future__ import annotations from datetime import datetime from typing import List, Optional from uuid import UUID, uuid4 from pydantic import EmailStr from sqlmodel import Field, Relationship, SQLModel from sqlalchemy.orm import Mapped, mapped_column # --------------------------- # 用户模型 # --------------------------- class UserBase(SQLModel): email: EmailStr = Field(unique=True, index=True, max_length=255) is_active: bool = True is_superuser: bool = False full_name: Optional[str] = Field(default=None, max_length=255) class UserCreate(UserBase): password: str = Field(min_length=8, max_length=40) class UserRegister(SQLModel): email: EmailStr = Field(max_length=255) password: str = Field(min_length=8, max_length=40) full_name: Optional[str] = Field(default=None, max_length=255) class UserUpdate(SQLModel): email: Optional[EmailStr] = Field(default=None, max_length=255) full_name: Optional[str] = Field(default=None, max_length=255) password: Optional[str] = Field(default=None, min_length=8, max_length=40) class UserUpdateMe(SQLModel): email: Optional[EmailStr] = Field(default=None, max_length=255) full_name: Optional[str] = Field(default=None, max_length=255) class UpdatePassword(SQLModel): current_password: str = Field(min_length=8, max_length=40) new_password: str = Field(min_length=8, max_length=40) class UserBarFollow(SQLModel, table=True): __tablename__ = "user_bar_follow" user_id: UUID = Field(foreign_key="user.id", primary_key=True) bar_id: UUID = Field(foreign_key="bar.bar_id", primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) class User(UserBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) hashed_password: str = Field(nullable=False) created_posts: List["Post"] = Relationship(back_populates="author") created_replies: List["Reply"] = Relationship(back_populates="author") created_bars: List["Bar"] = Relationship(back_populates="creator") followed_bars: List["Bar"] = Relationship(back_populates="followers", link_model=UserBarFollow) class UserPublic(UserBase): id: UUID class UsersPublic(SQLModel): data: List[UserPublic] count: int # --------------------------- # 吧模型 # --------------------------- class BarBase(SQLModel): name: str = Field(..., min_length=1, max_length=50) description: str = Field(..., min_length=1, max_length=500) follower_count: int = Field(default=0, ge=0) post_count: int = Field(default=0, ge=0) creator_id: UUID is_official: bool = Field(default=False) class BarCreate(BarBase): pass class BarUpdate(SQLModel): name: Optional[str] = Field(None, min_length=1, max_length=255) description: Optional[str] = Field(None, min_length=1, max_length=255) follower_count: Optional[int] = Field(None, ge=0) class Bar(SQLModel, table=True): bar_id: UUID = Field(default_factory=uuid4, primary_key=True) name: str = Field(..., min_length=1, max_length=50) description: str = Field(..., min_length=1, max_length=500) follower_count: int = Field(default=0, ge=0) post_count: int = Field(default=0, ge=0) creator_id: UUID = Field(foreign_key="user.id") is_official: bool = Field(default=False) created_at: datetime = Field(default_factory=datetime.utcnow) creator: "User" = Relationship(back_populates="created_bars") posts: List["Post"] = Relationship(back_populates="bar") followers: List["User"] = Relationship(back_populates="followed_bars", link_model=UserBarFollow) class BarPublic(BarBase): bar_id: UUID # --------------------------- # 帖子模型 # --------------------------- class PostBase(SQLModel): title: str = Field(..., max_length=100) content: str = Field(..., max_length=5000) is_top: bool = Field(default=False) like_count: int = Field(default=0, ge=0) reply_count: int = Field(default=0, ge=0) view_count: int = Field(default=0, ge=0) class PostCreate(PostBase): pass class PostUpdate(SQLModel): title: Optional[str] = Field(None, max_length=100) content: Optional[str] = Field(None, max_length=5000) is_top: Optional[bool] = None class Post(SQLModel, table=True): post_id: UUID = Field(default_factory=uuid4, primary_key=True) bar_id: UUID = Field(foreign_key="bar.bar_id") author_id: UUID = Field(foreign_key="user.id") title: str = Field(..., max_length=100) content: str = Field(..., max_length=5000) created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) bar: "Bar" = Relationship(back_populates="posts") author: "User" = Relationship(back_populates="created_posts") class PostPublic(PostBase): post_id: UUID bar_id: UUID author_id: UUID created_at: datetime updated_at: datetime # --------------------------- # 回复模型 # --------------------------- class ReplyBase(SQLModel): content: str = Field(..., max_length=2000) class ReplyCreate(ReplyBase): pass class ReplyUpdate(SQLModel): content: Optional[str] = Field(None, max_length=2000) class Reply(SQLModel, table=True): reply_id: UUID = Field(default_factory=uuid4, primary_key=True) post_id: UUID = Field(foreign_key="post.post_id") author_id: UUID = Field(foreign_key="user.id") content: str = Field(..., max_length=2000) created_at: datetime = Field(default_factory=datetime.utcnow) post: "Post" = Relationship(back_populates="replies") author: "User" = Relationship(back_populates="created_replies") class ReplyPublic(ReplyBase): reply_id: UUID post_id: UUID author_id: UUID created_at: datetime # --------------------------- # 其他模型 # --------------------------- class Message(SQLModel): message: str class Token(SQLModel): access_token: str token_type: str = "bearer" class TokenPayload(SQLModel): sub: Optional[str] = None class NewPassword(SQLModel): token: str new_password: str = Field(min_length=8, max_length=40) 报错PS C:\Users\林林子\Desktop\full-stack-fastapi-template-0.8.0\backend> docker logs full-stack-fastapi-template-080-prestart-1 + python app/backend_pre_start.py /app/app/core/config.py:105: UserWarning: The value of SECRET_KEY is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of POSTGRES_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of FIRST_SUPERUSER_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) INFO:__main__:Initializing service INFO:__main__:Starting call to '__main__.init', this is the 1st time calling it. INFO:__main__:Service finished initializing + alembic upgrade head /app/app/core/config.py:105: UserWarning: The value of SECRET_KEY is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of POSTGRES_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of FIRST_SUPERUSER_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. + python app/initial_data.py /app/app/core/config.py:105: UserWarning: The value of SECRET_KEY is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of POSTGRES_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) /app/app/core/config.py:105: UserWarning: The value of FIRST_SUPERUSER_PASSWORD is "changethis", for security, please change it, at least for deployments. warnings.warn(message, stacklevel=1) INFO:__main__:Creating initial data Traceback (most recent call last): File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 516, in _resolve_name rval = d[token] File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/util/_collections.py", line 345, in __missing__ self[key] = val = self.creator(key) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 484, in _access_cls return self.fallback[key] KeyError: "List['Post']" The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/app/app/initial_data.py", line 23, in <module> main() File "/app/app/initial_data.py", line 18, in main init() File "/app/app/initial_data.py", line 13, in init init_db(session) File "/app/app/core/db.py", line 24, in init_db user = session.exec( File "/app/.venv/lib/python3.10/site-packages/sqlmodel/orm/session.py", line 66, in exec results = super().execute( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2362, in execute return self._execute_internal( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2247, in _execute_internal result: Result[Any] = compile_state_cls.orm_execute_statement( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 305, in orm_execute_statement result = conn.execute( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1418, in execute return meth( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 515, in _execute_on_connection return connection._execute_clauseelement( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1632, in _execute_clauseelement compiled_sql, extracted_params, cache_hit = elem._compile_w_cache( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 703, in _compile_w_cache compiled_sql = self._compiler( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 316, in _compiler return dialect.statement_compiler(dialect, self, **kw) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 1429, in __init__ Compiled.__init__(self, dialect, statement, **kwargs) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 870, in __init__ self.string = self.process(self.statement, **compile_kwargs) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 915, in process return obj._compiler_dispatch(self, **kwargs) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/visitors.py", line 141, in _compiler_dispatch return meth(self, **kw) # type: ignore # noqa: E501 File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 4679, in visit_select compile_state = select_stmt._compile_state_factory( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/sql/base.py", line 683, in create_for_statement return klass.create_for_statement(statement, compiler, **kw) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 1110, in create_for_statement _QueryEntity.to_compile_state( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 2565, in to_compile_state _MapperEntity( File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 2645, in __init__ entity._post_inspect File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1253, in __get__ obj.__dict__[self.__name__] = result = self.fget(obj) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2711, in _post_inspect self._check_configure() File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2388, in _check_configure _configure_registries({self.registry}, cascade=True) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 4204, in _configure_registries _do_configure_registries(registries, cascade) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 4245, in _do_configure_registries mapper._post_configure_properties() File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2405, in _post_configure_properties prop.init() File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/interfaces.py", line 584, in init self.do_init() File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 1642, in do_init self._setup_entity() File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 1854, in _setup_entity self._clsregistry_resolve_name(argument)(), File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 520, in _resolve_name self._raise_for_name(name, err) File "/app/.venv/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 491, in _raise_for_name raise exc.InvalidRequestError( sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[User(user)], expression "relationship("List['Post']")" seems to be using a generic class as the argument to relationship(); please state the generic argument using an annotation, e.g. "created_posts: Mapped[List['Post']] = relationship()"
最新发布
05-28
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纪嫣梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值