ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Web全栈进阶】SQLAlchemy 2.0关系建模:一对多与多对多

【Web全栈进阶】SQLAlchemy 2.0关系建模:一对多与多对多 上一篇把数据搬进了PostgreSQL但早报站还在用手写SQL sqlite3的旧通道。今天换新武器SQLAlchemy 2.0正式进阶——数据层从“手写SQL时代”进入“ORM时代”并且让数据第一次“长出关系”。本篇产出users / articles / rss_sources / favorites四张表的ORM建模一对多与多对多两种关系讲透。含代码约120行。 太长不看版给想快速上手的你项目信息一句话说明本篇目标SQLAlchemy 2.0 ORM关系建模代码行数~120行模型 演示依赖sqlalchemypsycopg[binary]核心功能四张表建模 一对多 多对多跑起来的命令python demo_relations.py核心知识点Mapped类型注解、relationship、back_populates、secondary做完你能得到数据层从“手写SQL”升级到“ORM时代”关系建模一次打通⚠️诚实的工程声明早报站的网页层Flask页面暂时仍走旧通道阶段三重构时统一切换到FastAPI ORM——今天先把数据层的地基换好这是正常工程的顺序数据层先于表现层。一、为什么是现在学关系建模你见过SQLAlchemy的入门用法Flask-SQLAlchemy的db.Column经典风格。今天有两个升级#升级说明①语法升级到2.0风格Mapped[str]类型注解驱动建模Python类型即表结构——写模型像写数据类一样自然②关系从零到一早报站要变成多用户产品数据就不再是“一张孤零零的文章表”——用户提交RSS源一对多、用户收藏文章多对多关系建模是这一切的地基二、两分钟概念课外键与relationship关系建模只回答两个问题 问题①数据层面——关系存在哪——外键ForeignKey “谁引用谁”用外键列表达。一对多时外键放在“多”的那一边多条Source各自记着自己的user_id多对多时两边都放不下需要一个第三张关联表记配对。 问题②代码层面——怎么顺着关系走——relationship 外键是给数据库看的relationship是给Python看的“导航属性”user.sources直接拿到这个用户的全部源不用手写JOIN。关系数据层代码层一对多User → SourceSource表存user_id外键user.sources返回列表多对多User ↔ Article第三张favorites表记配对user.favorite_articles返回列表外键决定数据怎么存relationship决定代码怎么写——两者配合JOIN从你的代码里消失了。三、第0步setup与目录pipinstallsqlalchemypsycopg[binary]早报站项目加两个模块沿用之前的结构规矩python_daily/ ├── core/ │ ├── db.py # 引擎与会话新增 │ ├── models.py # ORM模型新增 │ ├── storage.py # 旧通道阶段三退役 │ └── ...四、第1步连接引擎与会话——把“连库”变成“会话”core/db.pycore/db.py —— 引擎与会话fromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportsessionmaker DATABASE_URLpostgresqlpsycopg://postgres:你的密码localhost:5432/dailyenginecreate_engine(DATABASE_URL)Sessionsessionmaker(bindengine)对比第17篇的Flask-SQLAlchemy那里db.session是框架替你藏好的这里显式化了——engine是“连库的通道”Session是“一次工作的现场”。 tmp_path夹具思想在这里的对应物每次业务操作开一个会话用完即关with块自动关。五、第2步四个模型——类型注解即表结构core/models.pycore/models.py —— 早报站ORM模型SQLAlchemy 2.0fromdatetimeimportdatetimefromsqlalchemyimportColumn,DateTime,ForeignKey,Table,funcfromsqlalchemy.ormimportDeclarativeBase,Mapped,mapped_column,relationshipclassBase(DeclarativeBase):pass# 多对多关联表user_id article_id记录谁收藏了哪篇favoritesTable(favorites,Base.metadata,Column(user_id,ForeignKey(users.id),primary_keyTrue),Column(article_id,ForeignKey(articles.id),primary_keyTrue),Column(created_at,DateTime,server_defaultfunc.now()),)classUser(Base):__tablename__usersid:Mapped[int]mapped_column(primary_keyTrue)username:Mapped[str]mapped_column(uniqueTrue)# 一对多一个用户拥有多个订阅源sources:Mapped[list[Source]]relationship(back_populatesuser)# 多对多一个用户收藏多篇文章经由favorites表favorite_articles:Mapped[list[Article]]relationship(secondaryfavorites,back_populatesfavorited_by)classSource(Base):__tablename__rss_sourcesid:Mapped[int]mapped_column(primary_keyTrue)url:Mapped[str]user_id:Mapped[int]mapped_column(ForeignKey(users.id))# 多的那一边用back_populates和对面互相指认user:Mapped[User]relationship(back_populatessources)classArticle(Base):__tablename__articles# 上一篇建的表现在由ORM接管id:Mapped[int]mapped_column(primary_keyTrue)url:Mapped[str]mapped_column(uniqueTrue)title:Mapped[str]date:Mapped[str|None]mapped_column(nullableTrue)summary:Mapped[str]mapped_column(default)favorited_by:Mapped[list[User]]relationship(secondaryfavorites,back_populatesfavorite_articles) 逐段读懂这段代码概念说明Mapped[str]类型注解即列定义——Mapped[int]是整数主键Mapped[str]是非空文本str | None表示可空列。读模型就像读数据结构这是2.0风格的全部魔法favorites关联表多对多的“配对本”两个外键联合主键保证“同一对收藏不重复”relationship(secondaryfavorites)告诉ORM“User和Article之间隔着favorites表走”——写代码时完全感觉不到这张表的存在back_populates关系的两端互相指认ORM才知道user.sources和source.user是同一枚硬币的两面建表fromcore.dbimportenginefromcoreimportmodels models.Base.metadata.create_all(engine)# 新增users / rss_sources / favoritescreate_all只补缺的——上一篇已建的articles表不会被动。可以在main.py中添加新的命令也可以创建新的脚本运行建库脚本。六、第3步关系实战——写数据的正确姿势demo_relations.pydemo_relations.py —— 关系建模实战演示fromsqlalchemyimportselectfromcore.dbimportSessionfromcore.modelsimportArticle,Source,Userdefmain()-None:# ① 一对多新建用户 挂上两个订阅源withSession()assession:uUser(username博主本人)u.sources.append(Source(urlhttps://coolshell.cn/feed))u.sources.append(Source(urlhttps://www.ruanyifeng.com/blog/atom.xml))session.add(u)session.commit()# ② 多对多让这个用户收藏两篇文章withSession()assession:usession.scalar(select(User).where(User.username博主本人))forarticleinsession.scalars(select(Article).limit(2)):u.favorite_articles.append(article)session.commit()# ③ 顺着关系读数据不写一行JOINwithSession()assession:usession.scalar(select(User).where(User.username博主本人))print(f用户「{u.username}」订阅了{len(u.sources)}个源)forsinu.sources:print( -,s.url)print(f收藏了{len(u.favorite_articles)}篇文章f第一篇是《{u.favorite_articles[0].title}》)if__name____main__:main() 三个关键姿势#姿势说明①用append建立关系不要手填外键u.sources.append(Source(...))时ORM自动把user_id填进Source——手填user_id1的写法一旦用户id猜错就是外键约束报错第八节③②session.scalar(select(...))2.0风格的查询——select构造查询scalar取单条scalars取多条。对比1.x的session.query2.0是“构造式”查询与类型系统配合更好③关系的读写都在会话内完成with Session()块里能顺着关系取数据出了块再访问u.sources就是“脱离会话访问”第八节⑤的经典报错✅ 运行输出用户「博主本人」订阅了 2 个源 - https://coolshell.cn/feed - https://www.ruanyifeng.com/blog/atom.xml 收藏了 2 篇文章第一篇是《……》七、验收清单1. create_all后psql\dt → 看到users / rss_sources / favorites三张新表2. demo_relations.py跑通 → 输出订阅源与收藏列表3. psql验证favorites表两行配对记录created_at自动填充4. 重跑demo → username已存在会怎样练习①的思考题5. 全程无报错后提交Gitgitadd.gitcommit-m数据层升级SQLAlchemy 2.0关系建模八、常见报错这6个关系建模的标配重点①ModuleNotFoundError: No module named sqlalchemy 原因没装或装错环境。✅ 解法老朋友三连——看(venv)前缀 →python -m pip list→pip install sqlalchemy psycopg[binary]。②sqlalchemy.exc.OperationalError: connection refused 原因PostgreSQL容器没在跑——上一篇的成果没续上。✅ 解法dockerps# 看容器状态容器没了就docker run重新拉起数据卷还在数据一条不少。③sqlalchemy.exc.IntegrityError: FOREIGN KEY constraint failed 原因手填了不存在的user_id比如Source(user_id999)。✅ 解法改用append建立关系第6节姿势①——让ORM替你填外键永远不错。④ 多对多报ArgumentError: relationship ... expects a class or mapper 原因favorites关联表定义在relationship之后或拼写不一致。✅ 解法关联表要在使用它的relationship之前定义核对secondaryfavorites的名字与Table变量一致。⑤sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session 原因会话关闭后访问懒加载关系——with块外读u.sourcesORM想查数据库却找不到会话。✅ 解法关系读取全部放进with Session()块内需要“带出”数据的场景用selectinload预加载进阶第16篇测试策略会再见到。⑥ 查询结果打印出来是(1, 博主本人)元组而不是对象 原因session.execute(select(...))默认返回行Row不是模型对象。✅ 解法要对象用session.scalar()/session.scalars()要“只取某几列”才用execute 行。2.0风格的“取数形状”由你说了算。九、课后练习#练习难度提示1幂等思考题demo重跑会怎样给User.username的unique约束一个说法⭐⭐先查后建或接受报错——INSERT ... ON CONFLICT的思路在ORM里是session.merge2收藏排行榜查出被收藏次数最多的3篇文章⭐⭐⭐select(Article, func.count(...)).join(...).group_by(...)——ORM里JOIN长这样3级联删除给User.sources加cascadeall, delete-orphan删用户时订阅源自动清理⭐⭐⭐对比默认不许删的行为差异4选做旧脚本ORM化把迁移脚本改写为“SQLAlchemy读SQLite 写PG”⭐⭐⭐⭐感受ORM抹平方言差异 配套代码完整模型与演示脚本已上传Gitpython_daily/【gitee仓库地址】
返回列表