☰
基于Python与SQLAlchemy的DSM-5精神障碍数据库设计
2026/10/13 18:38:05 网站建设 项目流程

简介:这是一份基于Python实现的DSM-5精神障碍数据库设计源码,面向精神医学研究人员、后端开发者与医疗数据管理者,用于将《精神障碍诊断与统计手册》第五版中的分类和诊断信息进行结构化存储、查询与分析。项目共包含22个文件,压缩包仅1.03MB,其中8个Python脚本负责核心数据处理逻辑,3个JSON文件承载精神障碍分类信息,3个docx与2个rst文档提供项目说明和使用指导,其余toml、lock、pdm-python等文件用于依赖管理与构建配置,整体结构规范清晰。目前已有86人学习下载。通过该源码,读者可掌握基于Python的医学标准数据建模方法,获得可直接运行或二次开发的数据库骨架,并理解ODMantic等工具在文档模型中的应用,便于后续扩充诊断标准、关联治疗信息或搭建临床辅助系统,提升精神医学数据管理效率。

1. 为什么要把DSM-5搬进数据库:先从临床信息系统说起

做精神科相关的临床信息化系统时,你会发现一个绕不开的尴尬:医生和评估人员每天口头挂着的“DSM-5标准”,在代码里几乎没有一个可靠的数字化存在。量表可以做成选择题,患者主诉可以存成自由文本,唯独“诊断标准”这一层,大家靠手工翻阅PDF手册。于是就有了这类真实需求——基于Python把DSM-5精神障碍数据库设计出来,做成一套带源码、可落地、能直接跑查询的数据库方案。这个项目的本质不是“抄一本电子书进MySQL”,而是把DSM-5手册里反复出现的章节 → 障碍 → 诊断标准 → 严重程度分级层层拆开,变成关系模型里互相引用的表,底层让程序可以回答“焦虑障碍有哪些”“标准A里提到失眠的核心症状是什么”“哪些障碍共用社交回避这条标准”这类结构化问题。适合谁做?准备做精神科预筛查系统、临床决策支持、量表自动推荐、科研队列入组的开发者和医疗信息化工程师。

2. 数据模型设计:用SQLAlchemy把DSM-5的层级结构拆成关系表

2.1 DSM-5的先天层级结构:章节、障碍、诊断标准的依赖关系

DSM-5 不是一张平坦的一维清单,它天然是一个四级左右的树。顶层是类目章节,比如神经发育障碍、精神分裂症谱系、抑郁障碍、焦虑障碍、创伤及应激源相关障碍;每个类目章节下面挂具体的障碍诊断;每个障碍又往下挂诊断标准组。诊断标准本身也有内部结构:多数障碍分 A/B/C/D 标准,一部分标准内部带“至少满足其中X项”的计数逻辑,另一些障碍还带严重程度分级(轻度/中度/重度)和标注型别(例如伴焦虑痛苦)。

所以拿到需求后,第一件事不是写CREATE TABLE,而是先在纸上把这个层级依赖画清楚。我通常会把数据拆成五个实体:class(章节类目)、disorder(障碍)、criterion_group(标准组)、criterion(具体标准条目)、severity_level(严重程度等级)。注意 disorder 与 icd10_code 的关系是 1:N,因为 DSM-5 里不少障碍对应多个 ICD-10-CM 编码,比如“其他特定的强迫及相关障碍”下面有多个扩展码,漏掉这一个关系后面筛查会丢数据。

一个常见的反面做法是把整本手册按照“每页一行”导入数据库,理由是“反正我最终显示的是原文段落”。这个设计在今天做症状倒查、跨障碍共同症状比对时会非常痛苦,因为每一行都是大段文本,SQL 的LIKE '%失眠%'能用,但精确匹配、关联统计、按障碍聚合全都无从下手。所以无论如何都要先落结构化。

2.2 表结构实现:章节表、障碍表、标准表的Python建模

我一般用 SQLAlchemy 2.0 的声明式映射来建模,这样既能直接用session操作数据库,之后要迁到 Postgres 也不用改业务代码。下面是一版最小但完整的模型定义,落库后已经能支撑精神障碍的树状浏览和标准查询。

from datetime import datetime from sqlalchemy import ( Column, Integer, String, Text, ForeignKey, TIMESTAMP, UniqueConstraint, Index, ) from sqlalchemy.orm import relationship, declarative_base Base = declarative_base() class DisorderClass(Base): __tablename__ = "dsm5_class" id = Column(Integer, primary_key=True) class_code = Column(String(16), nullable=False, unique=True) # 如 "CLASS-07" name = Column(String(255), nullable=False) # 如 "抑郁障碍" description = Column(Text, default="") sort_order = Column(Integer, default=0) disorders = relationship("Disorder", back_populates="dsm5_class") class Disorder(Base): __tablename__ = "dsm5_disorder" __table_args__ = ( UniqueConstraint("name", "class_id", name="uq_disorder_name_class"), Index("ix_disorder_icd10", "icd10_code"), ) id = Column(Integer, primary_key=True) class_id = Column(Integer, ForeignKey("dsm5_class.id"), nullable=False) name = Column(String(255), nullable=False) icd10_code = Column(String(32), nullable=False) # 最小可用的主映射码 summary = Column(Text, default="") # 简要临床描述,不是全文 sort_order = Column(Integer, default=0) dsm5_class = relationship("DisorderClass", back_populates="disorders") criterion_groups = relationship("CriterionGroup", back_populates="disorder") class CriterionGroup(Base): __tablename__ = "dsm5_criterion_group" id = Column(Integer, primary_key=True) disorder_id = Column(Integer, ForeignKey("dsm5_disorder.id"), nullable=False) group_label = Column(String(16), nullable=False) # 如 "A组" "B组" min_count = Column(Integer, nullable=True) # 满足标准的最少条数 sort_order = Column(Integer, default=0) disorder = relationship("Disorder", back_populates="criterion_groups") criteria = relationship("Criterion", back_populates="group") class Criterion(Base): __tablename__ = "dsm5_criterion" __table_args__ = ( Index("ix_criterion_text_keyword", "text"), ) id = Column(Integer, primary_key=True) group_id = Column(Integer, ForeignKey("dsm5_criterion_group.id"), nullable=False) criterion_label = Column(String(8), nullable=False) # 如 "A1" "A2" text = Column(Text, nullable=False) # 标准条款文本 sort_order = Column(Integer, default=0) group = relationship("CriterionGroup", back_populates="criteria")

这三张表先建好,外加一张severity_level表(结构很简单:disorder_id+ 级别名 + 描述文本,这里不再占用篇幅展开)。从设计上你能看到几个刻意的选择:disorder表上建了 (name, class_id) 唯一约束,防止同章节下重复导入同名障碍;icd10_code建了单列索引,因为后面所有和电子病历对接的动作都是用 ICD 编码关联。标准文本没有建全文索引,这是刻意的——完整 DSM-5 条款文本很长,用 MySQL 的全文索引做中文或者英文分词效果都不稳定,中期要做症状倒查,正确做法见第 4 章。

2.3 为什么我不建议直接用NoSQL一股脑塞文档

有的团队图省事,想用 MongoDB 把每个障碍存成一个 JSON 文档:{name: "...", icd10: "...", criteria: ["..."]}。短期展示确实爽,但一旦开始做“两个障碍共同出现的标准”“按 ICD-10 编码回查障碍”“统计某章节下障碍数量”,要走聚合管道里嵌套$unwind加$lookup,写起来比 SQL 复杂得多,而且没有外键保护,一个脏criteria数组就会让数据一致性失控。

更实际的痛点是一个障碍挂多个严重程度、多组标准时,文档嵌套层级会很深,MongoDB 的查询会为了取一个子集而把整篇文档带回内存再过滤。而 DSM-5 这类数据的读取模式恰恰是高度关联的,它需要的是稳定的事务保证和方便的 JOIN。关系数据库在这些场景下没有悬念,所以我最后的选型是 SQLite 起步、数据量上来后直接切 PostgreSQL。SQLite 应付几万条以内的诊断和标准数据绰绰有余,单文件备份方便,适合项目初期的源码包分发和本地验证。

3. 数据入库与增删改查:把手册文本变成随时可检索的记录

3.1 从JSON/CSV导入DSM-5结构化数据的通用脚本

表建好之后,最现实的问题就是数据从哪来。你不大可能拿到 DSM-5 的官方结构化 XML,常见做法是把已有的 CSV 或者手工整理的 JSON 导入。下面这个脚本按“类目→障碍→标准组→标准”的层级逐层读取,用它你就能把一本已打散的 DSM-5 资料干净地写进数据库,且支持重复执行不产生重复数据。

import json from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from models import Base, DisorderClass, Disorder, CriterionGroup, Criterion engine = create_engine("sqlite:///dsm5.db", echo=False) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() def import_dsm5_json(path: str) -> None: with open(path, "r", encoding="utf-8") as f: data = json.load(f) for cls_item in data["classes"]: cls = session.query(DisorderClass).filter_by( class_code=cls_item["class_code"] ).first() if not cls: cls = DisorderClass( class_code=cls_item["class_code"], name=cls_item["name"], sort_order=cls_item.get("sort_order", 0), ) session.add(cls) session.flush() # 拿到自增id for dis_item in cls_item["disorders"]: disorder = session.query(Disorder).filter_by( name=dis_item["name"], class_id=cls.id ).first() if not disorder: disorder = Disorder( class_id=cls.id, name=dis_item["name"], icd10_code=dis_item["icd10_code"], summary=dis_item.get("summary", ""), ) session.add(disorder) session.flush() for group_item in dis_item["criterion_groups"]: group = session.query(CriterionGroup).filter_by( disorder_id=disorder.id, group_label=group_item["group_label"], ).first() if not group: group = CriterionGroup( disorder_id=disorder.id, group_label=group_item["group_label"], min_count=group_item.get("min_count"), ) session.add(group) session.flush() for crit_item in group_item["criteria"]: crit = session.query(Criterion).filter_by( group_id=group.id, criterion_label=crit_item["criterion_label"], ).first() if not crit: crit = Criterion( group_id=group.id, criterion_label=crit_item["criterion_label"], text=crit_item["text"], ) session.add(crit) session.commit() print("DSM-5 结构化数据导入完成") if __name__ == "__main__": import_dsm5_json("dsm5_seed.json")

这里的核心逻辑是“先查后插”——每一层级都先按自然键查一次,查不到才新增。这套写法保证同一个 JSON 文件跑十次,数据库里不会有十份重复记录。flush()的作用是让数据库立即生成自增主键并写回 Python 对象,否则后面的disorder_id和group_id会是None,子表记录全部写入失败。你导入之前先确认自己的数据里class_code是唯一的,如果原始数据里没有章节代码,就用sort_order当唯一标识,别直接把章节名当主键,章节改名你还要连带改外键。

3.2 CRUD接口封装:一个Repository类搞定增删改查

数据库导入只是开始,你的源码包里肯定要提供可调用的增删改查接口。不要把session.query(...)撒得到处都是,我习惯把对 DSM-5 数据的操作收敛到一个 Repository 类里。

from typing import Optional, List from sqlalchemy.orm import Session from models import Disorder, CriterionGroup, Criterion, DisorderClass class Dsm5Repository: """DSM-5 数据库仓储层,业务代码只跟这个类打交道""" def __init__(self, session: Session): self.session = session def get_disorder(self, disorder_id: int) -> Optional[Disorder]: return self.session.get(Disorder, disorder_id) def list_disorders_by_class(self, class_id: int) -> List[Disorder]: return ( self.session.query(Disorder) .filter(Disorder.class_id == class_id) .order_by(Disorder.sort_order) .all() ) def get_criteria_texts(self, disorder_id: int) -> List[str]: return [ c.text for g in ( self.session.query(CriterionGroup) .filter(CriterionGroup.disorder_id == disorder_id) .order_by(CriterionGroup.sort_order) .all() ) for c in ( self.session.query(Criterion) .filter(Criterion.group_id == g.id) .order_by(Criterion.sort_order) .all() ) ] def update_disorder_summary(self, disorder_id: int, new_summary: str) -> None: disorder = self.session.get(Disorder, disorder_id) if disorder: disorder.summary = new_summary self.session.commit() def delete_criterion(self, criterion_id: int) -> bool: criterion = self.session.get(Criterion, criterion_id) if not criterion: return False self.session.delete(criterion) self.session.commit() return True

增删改查的“查”部分重点看get_criteria_texts:它会返回该障碍下面全部标准的平铺文本列表。这个接口在临床辅助系统里很常用,前端拿到列表可以直接做勾选,后端拿它做标准命中率计算。值得提醒的是update_disorder_summary和delete_criterion都直接commit(),这在高频接口里效率偏低,生产环境我一般只在 service 层需要事务边界时才 commit,Repository 方法只做flush()。团队里如果有人坚持要“每个方法都 commit”,你要警惕——多个操作组合成一个事务时,这个方法就是事故隐患。

3.3 多人协作场景:数据库锁与同步需要注意的最小习惯

DSM-5 数据库一旦投入真实项目,通常不是一个人维护。最常见的问题是两个人同时改同一份诊断标准描述,最后一个提交的覆盖前者。关系数据库解决这事靠两点:行级锁和乐观锁版本号。

class Disorder(Base): __tablename__ = "dsm5_disorder" # ... 其他字段 version_id = Column(Integer, nullable=False, default=1)
# 乐观锁更新示例 disorder = session.get(Disorder, 1) old_version = disorder.version_id disorder.summary = "新的描述" disorder.version_id = old_version + 1 result = session.execute( update(Disorder) .where(Disorder.id == 1, Disorder.version_id == old_version) .values(summary=disorder.summary, version_id=old_version + 1) ) session.commit() if result.rowcount == 0: print("更新冲突:有人先改了这条记录,请刷新后重试")

这种“版本号 + where 条件带上旧版本”的做法代价小、又省心。真遇到需要多人同时高频改标准的团队,也可以换 PostgreSQL 的SELECT ... FOR UPDATE行锁,但就我所见,精神科辅助系统的标准文本修改频率远低于诊断代码映射更新,乐观锁完全够用。数据库同步软件这类外部工具在这个场景里反而多余——DSM-5 数据是有权威源头的主数据,不需要跨区域多活同步,你要聚焦的是冲突消解,不是数据搬运。

4. 查询设计:症状倒查、ICD-10映射和结果排序

4.1 按类别浏览障碍:为什么不直接SELECT WHERE

多数人拿到 DSM-5 数据库后第一需求是“把某个章节下面的障碍列出来”。这看着就是个SELECT * FROM disorder WHERE class_id = ?的事情,但真做了你就发现:用户在临床系统里看到的不是章节编号,而是“抑郁障碍”这个章节名,而且有些障碍横跨多个章节概念(比如焦虑和抑郁共病条目)。所以查询层要提供“按名称或者编码模糊找章节,再展开全部障碍”的两步接口。

def search_class_and_disorders(keyword: str) -> dict: """按章节名或 ICD 编码前缀定位章节,返回该章节下全部障碍""" classes = ( session.query(DisorderClass) .filter( DisorderClass.name.like(f"%{keyword}%") | DisorderClass.class_code.like(f"%{keyword}%") ) .all() ) result = {} for cls in classes: disorders = repo.list_disorders_by_class(cls.id) result[cls.name] = [ { "id": d.id, "name": d.name, "icd10_code": d.icd10_code, "criterion_texts": repo.get_criteria_texts(d.id), } for d in disorders ] return result

这段代码刻意把所有标准文本一次性查出来,看起来“笨”,但避免了一个经典翻车:列表页要显示“每张障碍卡有 3 组标准”,如果先查障碍列表再循环查标准,会造成 N+1 次 SQL 查询。数据量在几千条障碍以内时,一条联表查询或者一次批量查询后内存组装,比 ORM 懒加载舒服得多。等你真的要把每个障碍下面全部标准都平铺做全文展示,则建议使用contains_eager或者直接映射成 DTO。

4.2 症状倒查:用关键词匹配先跑通最小可用版本

真正让 DSM-5 数据库值钱的场景是“症状倒查”。比如患者主诉里出现了“回避社交”,系统想让医生看到哪些诊断标准提到这个词,以便提供参考。早期版本不需要上向量数据库那么重的方案,先用关键词精确匹配就能支撑第一版功能。

def match_criteria_by_keyword(keyword: str, limit: int = 20): """按诊断标准文本反向定位障碍,返回障碍名、章节名和命中标准""" rows = ( session.query(Criterion, CriterionGroup, Disorder, DisorderClass) .join(CriterionGroup, Criterion.group_id == CriterionGroup.id) .join(Disorder, CriterionGroup.disorder_id == Disorder.id) .join(DisorderClass, Disorder.class_id == DisorderClass.id) .filter(Criterion.text.like(f"%{keyword}%")) .limit(limit) .all() ) return [ { "disorder_name": d.name, "class_name": c.name, "criterion_label": cr.criterion_label, "matched_text": cr.text, } for cr, cg, d, c in rows ]

这里有个容易踩的细节:DSM-5 的标准文本用词非常统一,比如“失眠”在抑郁障碍、焦虑障碍、创伤后应激障碍里都会出现,但睡瘫、入睡困难这类表达并不总含“失眠”两个字。所以第一版关键词匹配跑通后,下一步强烈建议做同义词表:把用户输入的“睡不好”“睡不着”映射到“失眠”“睡眠障碍”。这个同义词表不需要很复杂,普通关系表维护几十个词就够了,真到需要语义相似度时再考虑向量数据库也不迟——在数据量只有几万条、词汇高度受控的领域里,关键词加同义词表的效果和数据清洗成本,往往比向量检索更可控。

4.3 排序与排除规则:让诊断建议更贴近临床判断

症状倒查返回的结果不能平铺给医生,否则一次搜索“情绪低落”会拉出十几个障碍,没有指导价值。我一般会在查询层加两层过滤排序。第一层是标准命中数量排序:障碍 A 命中 6 条标准,障碍 B 命中 2 条,A 排在前面。第二层是排除规则:DSM-5 的很多障碍标准里自带排除条款,比如“这种发作不能归因于某种物质的生理效应或另一种躯体疾病”,如果患者病历里同时记录了一个明确的躯体诊断,这部分障碍应该被抑制掉。

def rank_disorders_by_criteria_hits(patient_complaints: list[str]): """将患者主诉词列表对每个障碍的标准文本做命中统计并排序""" scores = {} for complaint in patient_complaints: rows = match_criteria_by_keyword(complaint, limit=200) for d in rows: scores[d["disorder_name"]] = scores.get(d["disorder_name"], 0) + 1 ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) # 排除规则示例:如果患者主诉是“骨折术后”且文本命中“躯体疾病排除条款”,跳过去 final = [ (name, score) for name, score in ranked if not _is_excluded_by_comorbidity(name, patient_complaints) ] return final

_is_excluded_by_comorbidity的具体逻辑请你从诊断标准的排除条款中抽关键词建规则,这里不写死——这也是 DSM-5 数据库源码里最容易做“活”的部分。排序规则必须做得很克制,因为诊断是最严肃的临床决策,数据库的作用是辅助提示,不是自动诊断,所以接口说明里要写清楚“结果需要临床确认”。这套排序在真实系统里价值和风险并存,排序一旦让医生产生了路径依赖,出问题就是医疗事故级别,务必加上足够的提示。

5. DSM-5数据库设计避坑指南:5个踩坑纪录

5.1 乱码:导入后中文和特殊字符直接成问号

现象:CSV 里有“广泛性焦虑障碍”,导入 MySQL 后变成“????????”或者“广泛�焦虑”。原因:要么是 CSV 文件本身是 GBK 编码,导入时 Python 默认utf-8打开直接抛异常;要么是数据库表的字符集是latin1,或者连接串里没有指定charset=utf8mb4。解决:统一链路,文件用 UTF-8 编码保存,SQLAlchemy 连接字符串写成mysql+pymysql://user:pass@host/db?charset=utf8mb4,SQLite 则无需在意,但读取文件时显式指定:

with open("dsm5_raw.csv", "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f)

utf-8-sig会帮你自动吃掉 Excel 导出的 BOM 头,这是血泪经验里最少见但最容易忽略的一行。如果已经有脏数据,尽早清理后重新导入,别写清洗脚本去猜乱码原内容。

5.2 JOIN放大:标准多对多排查时结果集翻倍

现象:查“失眠”命中 30 条标准,联表后返回 90 行。原因:标准表和障碍、章节联表 JOI N 时,中间表还有多条关系,尤其当标准 A 同时挂在标准组 X 和标准组 Y 下面(因为 DSM-5 不同障碍的标准会重复引用),笛卡尔积直接把结果放大。解决:查询时明确返回的路由,用DISTINCT或者先做子查询去重:

distinct_disorder_ids = ( session.query(Disorder.id) .join(CriterionGroup, Disorder.id == CriterionGroup.disorder_id) .join(Criterion, CriterionGroup.id == Criterion.group_id) .filter(Criterion.text.like("%失眠%")) .distinct() .subquery() ) rows = session.query(Disorder).join( distinct_disorder_ids, Disorder.id == distinct_disorder_ids.c.id ).all()

5.3 字段自创:等你对代码的时候就知道了

现象:项目里有人把诊断标准的group_label写成g1/g2/g3,另一个人按A/B/C去写代码,结果标准组匹配全部失败。原因:DSM-5 的标准组命名是通用的 A/B/C 或专门的“诊断标准”,不要为了图省事自创缩写。解决:严格遵循 DSM-5 原文的 label 命名,并写字段映射文档。如果你确实要自定义编号,在表里单独建一列code_alias并保留原 label,绝不覆盖标准字段。

5.4 无索引:症状搜索直接拖垮生产接口

现象:数据库写了 5 万条标准,症状倒查时一个%Keyword%查询跑了 800ms,加上排序直接超时。原因:标准文本上的LIKE查询无法用普通 B-Tree 索引。解决:第一,控制数据集规模,DSM-5 本身只有不到 200 个障碍类别,标准文本量级不会特别大;第二,如果确实有中文模糊匹配需求,MySQL 可以考虑加倒排索引(FULLTEXT),SQLite 用FTS5;第三,把高频搜索的关键词做成同义词映射表,用等值查询替代模糊查询。

5.5 版权与伦理:把全文放进数据库的隐形风险

现象:项目交付时把 DSM-5 全文 PDF 的内容一并导入了数据库,导致后续在商业系统里分发时面临版权风险。原因:DSM-5 是有版权的出版物,直接数字化全文再分发,在多数商用医疗信息化项目里都有合规问题。解决:数据库中只保存结构化的元数据,比如分类、障碍名称、标准组的标签和序号、以及你自己编写的摘要/说明文字,原始条款内容留链接指向授权电子版,或者只保留片段引用。不要为了图方便把整本手册塞进去,这个边界从一开始就要想清楚。

6. 进阶:用FastAPI给数据库加一层只读查询服务

6.1 只读API暴露:查询路由的最小实现

当数据库设计完成,下一步通常是把它做成 HTTP 服务供前端调用。我把服务刻意设计成只读的,原因很简单:诊断标准数据是权威主数据,不应该被业务方随意改动。写操作回到数仓管理端去处理,线上只留 GET 接口。

from fastapi import FastAPI, Depends from sqlalchemy.orm import Session app = FastAPI() @app.get("/dsm5/classes") def list_classes(db: Session = Depends(get_db)): classes = db.query(DisorderClass).order_by(DisorderClass.sort_order).all() return [{"id": c.id, "name": c.name, "class_code": c.class_code} for c in classes] @app.get("/dsm5/disorders/{disorder_id}/criteria") def get_criteria(disorder_id: int, db: Session = Depends(get_db)): repo = Dsm5Repository(db) return repo.get_criteria_texts(disorder_id)

接口的返回尽量直接用 Pydantic 模型做序列化,别把 ORM 对象直接返回给前端。之前我见过return db.query(...)直接出去,结果懒加载触发一堆非预期 SQL。生产环境再加一层缓存,比如 Redis 缓存按障碍 ID 的标准列表,因为那份数据基本不变。

6.2 验证测试:pytest 做完整性和回归排查

优雅设计要经得起回归验证。我通常在源码包里带一组 pytest 测试,专门检查数据库的完整性——这比上线后手工点查靠谱一个量级。

import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from models import Base, Disorder, CriterionGroup, Criterion TEST_ENGINE = create_engine("sqlite:///:memory:") TEST_SESSION = sessionmaker(bind=TEST_ENGINE) @pytest.fixture() def db_session(): Base.metadata.create_all(TEST_ENGINE) session = TEST_SESSION() yield session session.rollback() session.close() def test_disorder_has_at_least_one_criterion_group(db_session): """每个障碍必须至少挂一个标准组""" disorder_ids = db_session.query(Disorder.id).all() for (disorder_id,) in disorder_ids: count = ( db_session.query(CriterionGroup) .filter(CriterionGroup.disorder_id == disorder_id) .count() ) assert count > 0, f"障碍 {disorder_id} 没有标准组" def test_criterion_group_label_unique_per_disorder(db_session): """同一障碍下的标准组标签必须唯一""" groups = db_session.query(CriterionGroup).all() seen = set() for g in groups: key = (g.disorder_id, g.group_label) assert key not in seen, f"重复标准组: {key}" seen.add(key)

这两个测试对 DSM-5 结构完整性很有用,是我自己的常规检查项。运行pytest看到全部通过,你就不会在后续增删数据时慌里慌张去猜哪里少了标准组。源码包里如果有 CI,这个测试文件在每次提交时自动跑一遍,是事故概率最低的做法。

6.3 一个小习惯:查标准永远优先查标准组

在这类精神障碍数据库的长期维护里,我最大的教训是:不要绕过标准组直接操作标准。遇到需求“帮我把某个障碍的标准加一条”,先去找它对应该挂在哪个group_label下,而不是随手往criterion表里插一条group_id=None的记录。一旦脏数据出现,查询时带有过滤条件的代码和你没过滤的代码会同时翻车,而且数据已经流到下游,那时候后悔药就很难吃了。每次把标准文本写进库之前,我也会自问一句:“这真的是一条独立标准,还是某条现有标准的补充描述?”把这一步抓好,DSM-5 数据库就能稳稳当当地撑起诊断筛查、科研入组和临床文书结构化这些大场景。希望这些经验和踩坑纪录,能帮你在自己项目里少走一程弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询