尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SQLAlchemy CRUD 实战:模型定义、Session 管理与事务处理详解
如果你已经能画出表、能写几行原生 SQL那接下来绝大多数后台项目要解决的其实就干一件事用 SQLAlchemy 把数据通过 Python 对象安全地增删改查。尤其是 Flask 这类 Web 项目里ORM 层写得好不好直接决定你后面是在填坑还是在挖坑。这篇文章我按自己在实际项目里长期使用 SQLAlchemy 的习惯把 CRUD 操作从模型定义、Session 管理到提交事务完整捋一遍。不光给你代码还会解释每个环节背后的核心机制以及那些文档不会明说、只能靠踩坑换来的细节。适合刚开始接触 ORM、或者正在从老式session.query往select风格迁移的同学主要覆盖日常开发里最常用的路径。1. 先理清“增删改查”在 ORM 里的翻译逻辑1.1 原生 SQL 和 ORM 的差别在哪没接触 ORM 之前我们写增删改查无非就是拼 SQLINSERT INTO user(name, age) VALUES(?, ?)然后手动处理参数、结果集转字典、连接关闭。这套流程单看一次操作并不复杂麻烦的是项目规模一大几十张表互相关联查询条件一变SQL 拼接就容易失控而且类型转换、时间格式化这些细节会散落在各个业务函数里。SQLAlchemy 做的事情简单讲就是把数据库表映射成一个 Python 类把行记录映射成对象把外键关系映射成对象间的属性关系。你用 Python 操作对象属性提交时它再自动帮你翻译成 SQL。传统 CRUD 里的 INSERT、SELECT、UPDATE、DELETE 四种操作在 ORM 里分别体现为构造对象、查询对象、修改对象属性、删除对象最后统一通过事务提交落到数据库。这个抽象不是给你多绕一层而是把“业务数据”和“存储细节”分离开。比如你换了数据库从 SQLite 换成 PostgreSQL只要连接串改掉大部分 ORM 代码不用动。还有一点很关键ORM 帮你记住了对象在内存里的状态变化你不用去写UPDATE user SET name ? WHERE id ?直接改对象属性就够了。1.2 SQLAlchemy 的 CRUD 主链路一次完整的增删改查在 SQLAlchemy 里会经历这样几个环节定义模型类对应表属性对应字段建立连接create_engine得到数据库引擎创建 Session会话是操作入口负责追踪对象状态业务操作add、select、delete 等提交事务commit()才会让变更真正生效关闭会话释放连接资源这个链路里最容易被新手忽略的是 Session 的角色。一开始我也犯过错以为直接用engine就能操作表直接在engine上调方法结果发现根本没有 add、delete 这类方法。原因很简单Engine只负责建立连接和执行 SQL而 Session 负责管理对象生命周期和事务边界两者职责不同。1.3 2.x 风格和旧版 session.query 怎么选SQLAlchemy 从 1.4 开始主推 2.0 风格到 2.x 版本里老式session.query(User).filter(...)虽然还能用但新代码我更推荐写select(User).where(...)。原因有两个一是新版风格面向 SQL 语句本身语义更清晰迁移到异步也有天然路径二是工具链和类型提示对select风格的检查更友好。如果你接手的是老项目可能还在用 1.x 写法这不代表你用不了新版本。2.0 架构对老代码是做了兼容的只是新功能、新文档基本都是围绕select风格展开。我下面所有示例都采用 2.x 风格这样无论你是刚开始还是准备迁移都有一份可以直接抄的模板。2. 环境准备与模型定义先把“地基”打对2.1 安装依赖与数据库连接想跑通下面的代码首先装一个 SQLAlchemy。一般用pip install sqlalchemy就够了真正常用的场景还会配一个数据库驱动比如访问 SQLite 可以不需要额外驱动访问 MySQL 常用pymysql访问 PostgreSQL 常用psycopg2-binary或psycopgpip install sqlalchemy pip install pymysql连接引擎的写法我建议统一放在一个配置模块里方便后面对连接池和日志做调整from sqlalchemy import create_engine engine create_engine( mysqlpymysql://user:passwordlocalhost:3306/demo?charsetutf8mb4, echoFalse, pool_size10, max_overflow20, pool_pre_pingTrue, )这里有几个参数值得展开讲。echoTrue会在控制台打印所有执行的 SQL排查问题时很有用但生产环境别开日志量太大还拖慢性能。pool_size和max_overflow控制连接池的基础连接数和超出上限后临时扩容的连接数高并发下这两项没设好很容易出现TimeoutError。pool_pre_pingTrue会在每次取连接前先试探连接是否存活避免数据库重启后拿到一堆失效连接。如果是学习阶段我推荐直接用 SQLite零配置跑通最重要engine create_engine(sqlite:///./demo.db, echoTrue)2.2 用 Mapped 和 mapped_column 定义模型在 SQLAlchemy 2.x 里官方推荐的模型声明方式是from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy import String, DateTime, func class Base(DeclarativeBase): pass class User(Base): __tablename__ user id: Mapped[int] mapped_column(primary_keyTrue, autoincrementTrue) name: Mapped[str] mapped_column(String(50), nullableFalse) email: Mapped[str] mapped_column(String(120), uniqueTrue, indexTrue) age: Mapped[int] mapped_column(default0) created_at: Mapped[DateTime] mapped_column(DateTime, server_defaultfunc.now())这套写法的好处是字段类型和 Python 类型注解完全对应IDE 能帮你把拼写错误直接标出来。Mapped[str]表示非空字符串如果字段允许为空用Mapped[Optional[str]]字段名和列名不一致时在mapped_column里传namereal_column_name。定义好模型之后创建表用Base.metadata.create_all(engine)。它会检查数据库中不存在的表按模型定义自动建表。注意它不会更新已有表的字段所以开发阶段没问题生产环境的表结构变更要么手工迁移要么上迁移工具管理。2.3 模型设计时就要规划好的几个点我踩过一个浪费时间的坑一开始图省事把所有字段都定义成String等做查询排序和范围判断时才发现日期、数字类型不对又要迁数据。所以模型定义阶段就要明确字段类型主键用int还是UUID、时间用DateTime还是时间戳、金额用DECIMAL而不能用float。外键关系也要早定。比如用户和文章的关系from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship class Post(Base): __tablename__ post id: Mapped[int] mapped_column(primary_keyTrue) user_id: Mapped[int] mapped_column(ForeignKey(user.id)) title: Mapped[str] mapped_column(String(200)) user: Mapped[User] relationship(back_populatesposts) User.posts relationship(Post, back_populatesuser)relationship在 CRUD 中会极大影响删除和查询行为。你加了relationship之后访问user.posts可以自动加载关联文章但反过来也要考虑级联删除策略这块我在后面删除部分会专门讲。3. 添加数据别把事务边界想简单了3.1 Session 是增删改查的统一入口连接引擎确定了模型定义好了接下来一切操作都围绕 Session 展开。Session 的推荐创建方式是使用sessionmakerfrom sqlalchemy.orm import sessionmaker SessionLocal sessionmaker(bindengine, expire_on_commitFalse, autoflushFalse) session SessionLocal()这里有两个参数值得单独说明。expire_on_commitFalse是很多老项目容易忽略的配置项默认情况下 commit 之后对象上的所有属性都会过期下次访问属性时它会再去数据库查一遍。这在 Web 框架里经常引发“已提交但对象不可用”的困惑设置成 False 会让提交后的对象继续保留内存中的属性节省一次不必要查询。autoflush默认是 True意味着你在查询前它会自动把未提交的变更先发送到数据库。从一致性角度看它很方便但调试时容易让人意外明明是查另一张表怎么先把刚才的 INSERT 执行出来了我习惯在基础设施层把autoflush关闭业务代码里明确控制提交和刷新时机。3.2 单条添加和批量添加单条添加的经典写法user User(nametiny, emailtinyexample.com, age18) session.add(user) try: session.commit() print(user.id) # 提交后自动填充自增主键 except Exception as e: session.rollback() raise e这里要注意session.add(user)只是把对象加进 Session 的“新对象”集合此时数据库里还没有记录。真正执行 INSERT 的时刻有两个一是session.flush()二是session.commit()。所以在commit()之前你甚至拿不到user.id因为主键是数据库生成的。如果你想先拿到主键再继续业务处理可以手动调用session.flush()session.add(user) session.flush() print(user.id) # 现在有了批量添加时一个常见误区是循环里每加一条就commit()一次。这样做不是不行但每次 commit 都是一次事务提交性能很差而且一旦中间出错很难把前面那些已提交的部分一起回滚。推荐的批量方案是循环add最后统一commit()for i in range(1000): session.add(User(namefuser_{i}, emailfuser_{i}example.com, agei)) session.commit()数据量特别大时比如十多万行批量导入还是一次性塞进 Session 也不合适因为 Session 会一直持有这些对象内存压力大。更稳的做法是分块提交每处理 500 或 1000 条提交一次提交后再session.expunge_all()清空对象缓存释放内存。3.3 添加数据时最容易遇到的三个问题第一个是唯一约束冲突。比如email字段设置了uniqueTrue你插入两条相同 email 记录时commit()会抛IntegrityError。很多人以为加了判断就能避免其实高并发场景下判断和插入之间还是会有竞态最可靠的办法是捕获异常后做回滚处理。第二个问题是datetime时区。如果你用DateTime类型且不关心时区建议直接让数据库给默认值像前面示例里的server_defaultfunc.now()或者干脆统一用 UTC 时间在代码里生成。第三个问题关系到代码结构Session 的生命周期最好和业务操作绑定。在纯脚本里可以直接SessionLocal()然后用完close()但在 Web 请求里每个请求应该创建自己的 Session请求结束关闭绝不能把 Session 放在全局变量里跨请求复用因为那会导致连接被多个线程同时使用一脸蒙圈地报错。4. 查询数据查询结果的迭代细节是关键4.1 select 基础操作2.x 风格的核心查询是select()配合Session.execute()使用from sqlalchemy import select stmt select(User).where(User.age 18, User.name tiny) result session.execute(stmt) users result.scalars().all() for user in users: print(user.id, user.name)很多人看到这个会奇怪为什么execute返回的不是列表而要调用.scalars().all()。这是因为execute返回的是Result对象它可能是多列、多个实体甚至混合实体框架不替你假设你需要什么。如果查询只是单个实体.scalars()就能把每行第一列映射成实体对象再.all()拿到列表。常用过滤条件简单列一下User.id.in_([1, 2, 3])IN 查询User.age.between(18, 30)范围查询User.name.like(%tiny%)模糊查询User.created_at datetime(2024, 1, 1)日期比较~User.is_deleted不等于或取反User.name.is_(None)判断 NULL有一点很容易踩判断字段是否为 NULL 不能用 None而要用.is_(None)。虽然 SQLAlchemy 做了兼容但语义不明确容易让人误判统一写成is_(None)最省心。4.2 排序、分页、计数和联合查询排序用order_by分页用offset/limitstmt ( select(User) .where(User.age 18) .order_by(User.created_at.desc(), User.id.asc()) .offset(20) .limit(10) )注意order_by写在where之后多个排序条件按先后顺序生效。分页在数据量大时offset越翻越慢因为数据库要跳过前面所有行这个点到后面再讲优化。统计数量有两个思路。简单场景直接用count session.scalar(select(func.count()).select_from(User))复杂一点的按字段分组统计stmt ( select(User.age, func.count(User.id)) .group_by(User.age) ) for age, cnt in session.execute(stmt): print(age, cnt)session.scalar()只取单行单列适合返回聚合结果。涉及两张表的关联查询我最常推荐的是明确写join还是用relationship自动加载。如果你只想要文章和作者信息最快的方式是select(Post).join(User)一步到位。但如果想取出来就能访问post.user.name要考虑是懒加载还是预加载。简单项目直接访问post.user可能触发额外查询这在列表页会变成 N1 次查询用户体验和接口性能全毁了。4.3 优化查询的常见切入点先说 N1。什么叫 N1你查了 N 篇文章然后循环访问每篇文章的post.userORM 就额外执行 N 次查询查用户。解决办法是查询时预加载关联对象from sqlalchemy.orm import selectinload stmt select(Post).options(selectinload(Post.user)).where(...)selectinload会先用一次查询取文章再用 IN 查询把关联用户一次性取出来。另一种joinedload会更激进用 JOIN 把两张表一次查出。这两者各有适用场景列表页我用selectinload更多因为它不会因为一对多关联导致结果行膨胀。再谈分页优化。offset很深时数据库还是要线性扫描前面大量行。一个常见的替代方案是“键集分页”keyset pagination按某个唯一顺序列过滤上一页最后一条记录的位置stmt ( select(User) .where(User.age last_age, User.id last_id) .order_by(User.age, User.id) .limit(20) )这种写法对写日志、列表分页很友好能稳定走索引不会出现深分页越翻越慢的问题。最后提醒一句查询时不要把数据全部拉回内存再过滤。比如select(User)直接.all()再在 Python 里写循环跳过年龄小于 18 的这种写法在数据量小的时候还能忍一旦上了万级内存和耗时都会明显上升。数据库本来就是做数据过滤的把过滤条件写进 SQL 才是正道。5. 更新与修改两种思路差别很大5.1 对象更新查出后改属性靠 Session 跟踪更新操作最直观的写法是先查出对象再修改它的属性user session.get(User, 1) user.age 19 session.commit()session.get(User, 1)按主键查出一行的 ORM 对象随之纳入 Session 管理。你修改user.age时SQLAlchemy 会通过属性变化的跟踪机制记录这次变更commit 时自动生成 UPDATE 语句UPDATE user SET age 19 WHERE id 1;这种方式的优点是代码可读性高适合用户量不大、按主键修改的单条记录场景。而且因为对象还在 Session 里事务提交前后你都能方便地检查旧值和新值。使用这种方式要注意几点第一修改前对象必须处于持久化状态也就是必须经过查询加载不能自己User(id1, name...)后直接 commit那样 Session 会认为它是新对象走的是 INSERT 而不是 UPDATE。第二autoflushTrue时Session 会在你下一次查询数据前自动把变更 flush 到数据库如果你紧接着查别的表会发现 UPDATE 提前执行了。第三对象的属性值要在事务提交前设置好提交后再改就需要新开事务了。5.2 更新构造器性能优先的场景用它如果你要更新一批记录或者只更新某一个字段而不想先加载整条记录就该用 Core 级的update()from sqlalchemy import update stmt ( update(User) .where(User.age 18) .values(age19) ) result session.execute(stmt) session.commit() print(result.rowcount) # 影响行数这个方式直接就生成一条 UPDATE 语句不会先把对象加载进内存也不产生任何 ORM 对象状态跟踪性能比“先查后改”高很多。批量更新、定时任务里给一批用户改状态、把某类订单统一标记为失效这类场景都推荐用它。批量更新有一个隐藏陷阱它默认不更新 Python 端的对象状态。如果 Session 里刚好有一个User(age18)旧对象你执行了批量更新后这个对象内存里的age仍然是 18直到过期机制触发才会再次从数据库读。于是你读到的可能是旧值。事务里要小心这种不一致必要时在执行后调用session.expire_all()让 SQLAlchemy 下次访问属性时重新加载。还有个参数synchronize_session这个选项决定update()执行时如何同步 Session 中的对象状态。默认对 SQLite 是auto个别数据库会采用fetch意思是先查询受影响行再回填 Session 对象。如果你不关心 Session 里的旧对象可以显式写execution_options(synchronize_sessionFalse)跳过能省一部分查询开销。5.3 并发更新和乐观锁多人同时改同一条记录很容易出现“后提交覆盖先提交”的问题。你查出来是旧值另一个人改成了新值你提交时直接把别人的修改覆盖了。比如后台编辑同一条商品信息A 和 B 同时打开A 改完提交B 也提交后者的覆盖前者。数据库层面用悲观锁with_for_update()可以规避但行锁会影响并发性能。应用层更常见的是乐观锁给表加一个version_id字段SQLAlchemy 原生支持ver: Mapped[int] mapped_column(default1) # 模型定义中挂上版本字段 __mapper_args__ {version_id_col: ver}每次更新时SQLAlchemy 会自动在 UPDATE 的 WHERE 条件中带上版本号比如WHERE id 1 AND ver 1并把ver改成 2。如果更新影响行数为 0说明版本已经变化说明有别人改过了这时候你要么重新查数据要么提示用户“数据已被修改请刷新”。使用乐观锁有一个重要前提Session 里同一个对象的更新要尽量使用统一的版本机制。如果混用批量update()构造器它不会自动检查版本号这个限制要自己记住。我通常对重要业务表统一走 ORM 对象更新只有那种不需要严格并发控制的批量任务才用构造器。6. 删除数据级联和缓存是两大坑6.1 删除单条记录的完整姿势删除在 ORM 里不是“传个 id 就能删”它要先把对象加载进 Session然后调用session.delete()user session.get(User, 1) if user: session.delete(user) session.commit()执行流程是先根据主键查对象对象加载后处于持久化状态delete()把它标记为待删除commit 时生成 DELETE 语句。这就是为什么有些人试图直接传1给某个并不存在的 delete 函数会失败因为 ORM 需要对象存在才能级联处理关系删除一个“不存在”的对象没有意义。如果你不想先查出来可以直接用 Core 的delete()构造器from sqlalchemy import delete stmt delete(User).where(User.id 1) result session.execute(stmt) session.commit()这种写法少了对象加载过程效率高不少适合按条件批量删除。但它有一个关键限制不会触发 ORM 层定义的级联删除。假如User有关联的Post你直接delete(User)删掉用户文章表不会自动处理如果数据库外键没设ON DELETE CASCADE就等着删完用户后留下大量孤立文章或者外键约束报错。6.2 数据库外键ON DELETE 与 ORM 的 cascade先分清楚两个完全不同的东西数据库的ON DELETE CASCADE是数据库层面行为ORM 的cascade是 SQLAlchemy 在删除父对象时自动生成对子对象的额外 DELETE 语句。如果你完全依赖数据库外键级联那么用批量delete(User)删除用户时数据库会连带删掉关联文章这在你明确设计好外键的前提下是安全高效的。更稳的做法是模型定义时同时配置class Post(Base): __tablename__ post id: Mapped[int] mapped_column(primary_keyTrue) user_id: Mapped[int] mapped_column(ForeignKey(user.id, ondeleteCASCADE))ForeignKey(..., ondeleteCASCADE)只是影响数据库建表语句但不会影响 ORM 在删User对象时自动处理Post。想通过 ORM 对象删除自动处理子表需要在relationship里设置posts: Mapped[List[Post]] relationship(back_populatesuser, cascadeall, delete-orphan)cascadeall, delete-orphan的含义是删父对象时连带删子对象子对象从列表中移除时自动删掉这种“孤儿”。这个配置比较激进适合父子生命周期的强绑定。比如文章标签、订单明细这类从属于主记录的数据父没了子必然没有意义可以这么用。我个人的工程偏好是核心大表之间的删除尽量用数据库外键ON DELETE让数据库保证数据完整性relationship的cascade只用于内存集合操作和明确的小对象树。原因很简单数据库级联不受应用代码 bug 影响SQLAlchemy 级联有时会因为对象没被正确加载而漏删。6.3 软删除真正的生产实践业务系统里我很少直接物理删数据更多是标记删除。用户取消订单、下架文章记录还得留着只是业务上不可见。常见做法是加一个is_deleted字段is_deleted: Mapped[bool] mapped_column(defaultFalse)删除时就只做一次更新stmt update(User).where(User.id 1).values(is_deletedTrue) session.execute(stmt) session.commit()这样后面再写查询时每次都记得带上User.is_deleted False。全查时容易忘所以建议定义一个常规过滤条件或者直接用带过滤的视图。SQLAlchemy 也可以用column_property配合可变条件不过更简单可靠的是养成查询习惯列表查询、统计查询都额外加一个is_deleted判断。物理删除留给明确的“彻底清理”任务比如定时清理已注销满 30 天的数据并在清理前确认没有业务引用。7. 事务与异常处理写 ORM 代码最容易翻车的区域7.1 为什么 commit 之后还要处理 rollback很多新手写完session.commit()就认为大功告成其实事务提交前后有大量边界情况。一条 INSERT 失败比如唯一约束冲突直接抛异常。此时 Session 会处于一种“部分失败”状态如果你不处理Session 的整个事务还是开着的连接没有归还后续再用这个 Session 会一直拿到这个失败事务的残留状态。典型的表现是你明明改了其他数据commit 时却报PendingRollbackError说前一个事务还没回滚。解决之道就是异常捕获后立刻rollback()try: session.add(user) session.commit() except Exception: session.rollback() raise这段代码在工程里比任何花哨的操作都实用。多张表关联写入时必须包在同一个事务里任何一个步骤失败都要回滚全部否则会出现几张表数据对不上的脏情况。7.2 捕获写入错误时的常见选择对于双写场景我遇到过两种处理。如果两个数据源都在同一个数据库实例那就用同一个 Session 写进同一事务错误时一起回滚这是最干净的。如果两个数据源分属不同数据库或者一个关系库一个缓存库那就不存在分布式事务属于最终一致性问题。这时一般做法是先写主数据库成功后再更新缓存更新失败则记录补偿任务后续慢慢重试。这类问题不要试图用一个事务解决跨库操作否则会把简单问题复杂化。还有一点容易被忽略WEB 项目里每个请求都要自己开启和关闭 Session。即便前面代码没有报错也要在请求结束时session.close()。否则数据库连接池里的连接会越占越多最终整个服务不可用。7.3 常见报错速查表把这么多年遇过的典型报错整理成表查起来最省事报错现象根本原因处理建议PendingRollbackError事务失败后没有及时回滚异常时先rollback()再处理DetachedInstanceError对象脱离了 Session 后访问懒加载属性查询时用selectinload或者把数据转成普通字典IntegrityError唯一约束、非空约束、外键约束失败捕获异常后确认约束不要盲目重试MissingGreenlet相关报错异步场景中同步访问惰性属性异步用async_session配合selectinload预加载ObjectDeletedError对象已被删除但还在访问属性刷新对象或重新查询Could not evaluate current criteria查询比较条件无法被 SQLAlchemy 转换换成明确的.in_()、.is_()等构造这张表不需要背但建议收藏。真正写业务时遇到报错先按这些大类去定位通常比胡思乱想来得快。7.4 Session 的线程安全与作用域Session 是一个非线程安全对象。简单说它就是只能在一个线程内使用。如果多线程代码里共享同一个 Session你会看到各种诡异的现象偶尔正常偶尔报连接被占用或者数据互相串。正确做法是两个方向。线程手动管理时每个线程创建自己的 SessionWeb 框架里则按请求划分 Session 生命周期。请求开始创建请求结束关闭Session 不作为全局对象存放。SQLAlchemy 提供了scoped_session解决线程局部变量的问题但那个实现细节我不建议一上来就用理解“一个请求一个 Session”之后你会觉得这些机制都是理所当然。如果有异步需求用 SQLAlchemy 1.4 之后的async_sessionmakerfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker async_engine create_async_engine(sqliteaiosqlite:///./demo.db) AsyncSessionLocal async_sessionmaker(async_engine, expire_on_commitFalse)异步 Session 的用法和同步差不多区别是查询要用await session.execute(stmt)。异步环境里少用惰性属性访问能避免各种绿色线程切换问题。8. 一套 CRUD 层封装模板防呆又顺手8.1 封装成通用方法到项目后期我通常不会让业务代码到处写session.execute(...)而是封装一个 CRUD 基类。先定义基础仓库类class BaseRepository: def __init__(self, session): self.session session def get(self, model, pk): return self.session.get(model, pk) def add(self, obj): self.session.add(obj) self.session.commit() def bulk_add(self, objs): self.session.add_all(objs) self.session.commit() def update(self, obj): self.session.merge(obj) self.session.commit() def delete(self, obj): self.session.delete(obj) self.session.commit()merge()这个方法值得一说。当你拿到一个脱离 Session 的对象比如从缓存或反序列化来的User实体直接add()会被当成新对象而merge()会根据主键匹配已有记录把属性合并进去再执行更新。它与“查出来再改”的区别是内部有一个“是否已存在”的映射判断适用对账和导入场景。如果你不想每个数据模型都写一套仓库可以按模型继承实现通用操作。但在实际工程中我不会为了把所有表塞进同一个抽象里而牺牲查询性能。基础增删改查能抽象复杂查询必须单独写因为过滤条件、排序、预加载差异太大。8.2 自动化生成测试数据调试 CRUD 时每次手动造数据很烦。我习惯在项目测试环境写一段脚本批量生成假数据session.add_all([ User(namealice, emailaliceexample.com, age20), User(namebob, emailbobexample.com, age30), Post(titlefirst post, user_id1), ]) session.commit()这个脚本的价值不只是跑通代码更重要的是能验证模型里的字段约束、外键关系、级联删除配置是否符合预期。每次新增字段或者调整表关系先跑一遍造数脚本有问题在开发环境暴露不要留到联调或上线才炸。我自己在团队里一直强调CRUD 是任何业务系统的地基地基层的代码宁可写得啰嗦一点也不能让业务代码各处直接裸写 SQL。把事务边界、Session 管理和异常处理收拢封装好后面提需求、加功能才不会被数据一致性问题拖住后腿。
RELATED

相关推荐

51单片机语音实时采集系统:从硬件到上位机全流程详解

51单片机语音实时采集系统:从硬件到上位机全流程详解

51单片机做语音实时采集,乍一听好像挺唬人,毕竟这芯片才几百字节内存、主频也不高,但实际把整套链路跑通之后,你会发现它并没有想象中那么难,真正难的其实是“实时”这两个字背后的环环相扣。最近我把这套“51单片机语…

📅 2026/10/22 16:33:20
工业软件实时数据处理:不炸、不误报、不失控的工程实践

工业软件实时数据处理:不炸、不误报、不失控的工程实践

做工业软件这一行,待久了你会慢慢发现一个特别有意思的转变:刚入行的工程师,开口闭口都是“最优”——最优滤波、最优控制、最优调度;而在PLC、DCS、SCADA、边缘计算、嵌入式控制器这些实时/准实时系统上摸爬滚打过几年的人&#…

📅 2026/10/22 16:33:20
Gemini 3.8 Flash驱动的票据智能抽取与规则校验实践

Gemini 3.8 Flash驱动的票据智能抽取与规则校验实践

干财务自动化的同学应该都懂一个痛:票据处理这事儿,看起来就是“看图识字”,真要落地却能把人磨疯。发票、收据、运单、报销单,版式五花八门,有的抬头在左下角,有的金额横着写,还有的红章子直接…

📅 2026/10/22 16:33:20
MORE NEWS

更多资讯

📰

从 20 个图标到 5 个:Ice 菜单栏管理新手完整指南

从 20 个图标到 5 个:Ice 菜单栏管理新手完整指南 【免费下载链接】Ice Powerful menu bar manager for macOS 项目地址: https://gitcode.com/GitHub_Trending/ice/Ice 菜单栏堆了二十几个图标,蓝牙和 Wi-Fi 还被刘海挡掉一半——这是很多 MacBo…

📰

AI小说创作神器

一、项目概况91Writing 是一个面向小说创作场景的开源工具,核心目标是将大模型能力嵌入到小说的大纲、章节、人物、世界观等创作环节中。项目明确声明为纯前端实现(主分支),所有业务数据保存在浏览器本地,不提供云同步…

📰

一文读懂Llama-3.2-1B_rai_1.8.0_npu_4K文件结构:从model.onnx到40多个dd_metastate文件全解析

【免费下载链接】Llama-3.2-1B_rai_1.8.0_npu_4K 项目地址: https://ai.gitcode.com/hf_mirrors/amd/Llama-3.2-1B_rai_1.8.0_npu_4K 点击查看 免费下载 如果你刚下载了 Llama-3.2-1B_rai_1.8.0_npu_4K 这个模型仓库,第一次打开目录时可能会有点懵&…

📰

gvim快捷键声明wire和reg

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

如何免费听遍全网歌曲:LX Music 五音源聚合搜索、批量下载与定制播放完整教程

如何免费听遍全网歌曲:LX Music 五音源聚合搜索、批量下载与定制播放完整教程 【免费下载链接】lx-music-desktop 一个基于 Electron 的音乐软件 项目地址: https://gitcode.com/GitHub_Trending/lx/lx-music-desktop LX Music 是基于 Electron 与 Vue 3 的开…

📰

搭建机器人仿真资产库:V2D Challenge的GLB纹理网格与URDF文件完整使用指南

【免费下载链接】video_to_data_challenge 项目地址: https://ai.gitcode.com/hf_mirrors/nvidia/video_to_data_challenge 点击查看 免费下载 V2D Challenge(Video to Data Challenge)是 NVIDIA 推出的机器人操作基准数据集,内置…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬