简介:这是一份面向Python初学者与数据库开发入门者的三层架构实践源码,聚焦MySQL数据库操作的分层解耦设计,帮助开发者理解数据访问层、业务逻辑层与表现层的职责划分及协作方式。资源包含12个文件,以6个核心Python源码为主(含数据库助手MySqlHelper.py、业务模块student.py与Operate.py、入口测试脚本test.py),辅以4个编译字节码文件及2个PyDev项目配置文件,整体压缩包仅8KB,轻量易读,适合快速导入IDE运行调试。已有858人学习下载,反映出其在教学演示与小项目原型开发中的实用价值。读者可直接运行获得完整执行链路:自动创建数据库与表、插入两条示例数据、完成查询验证,同时清晰掌握目录结构中各层对应文件的定位与调用关系,是理解Python+MySQL工程化组织方式的典型轻量级范例。
1. 为什么“Python + MySQL 三层架构”不是教科书概念,而是你明天就能上线的工程底座?
很多开发者第一次听到“三层架构”,下意识觉得是面试八股、是PPT里的分层图、是导师画在白板上却从不跑通的抽象模型。但真实项目里,它恰恰是最先被砍掉又最晚能补上的那块承重墙——当你的 Flask 接口开始返回500 Internal Server Error,日志里只有一行pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query');当你把所有数据库操作硬塞进路由函数,改一个字段要翻遍 7 个.py文件;当你想加个缓存、换种存储、或者把用户模块抽出来给新系统复用时,发现代码像毛线团一样扯不动……这时候你才真正需要它:Python-MySQL 数据库三层架构,不是设计模式炫技,而是让 CRUD 变得可维护、可测试、可替换的最小工程契约。它适合所有正在用 Python 写后端、数据服务或内部工具的开发者——无论你是刚写完第一个flask run的新手,还是正被遗留系统压得喘不过气的熟手。本篇不讲 UML 图,不画依赖箭头,只带你用 3 个清晰目录、4 类核心文件、1 套可验证的调用链,把“三层”从黑匣子变成你 IDE 里可跳转、可断点、可单测的实体。接下来每一步,都对应真实项目中我亲手踩过坑、回滚过三次、最终稳定运行超 18 个月的落地路径。
2. 从零构建三层骨架:目录结构、职责边界与初始化逻辑
三层架构的本质,是把“谁该知道什么”用物理隔离固化下来。它不靠注释约定,而靠目录强制、导入限制和单元测试兜底。下面这个结构不是理想化模板,而是我在某跨平台数据中台项目中持续迭代 11 个版本后沉淀下来的最小可行形态:
project_root/ ├── app/ │ ├── __init__.py │ ├── models/ # 【数据模型层】:仅定义 ORM 映射、字段约束、基础校验 │ │ ├── __init__.py │ │ └── user.py # class User(Base): id = Column(Integer, primary_key=True) ... │ ├── repositories/ # 【数据访问层】:封装 SQL/ORM 操作,屏蔽数据库细节,返回模型实例 │ │ ├── __init__.py │ │ └── user_repo.py # class UserRepository: def get_by_id(self, user_id: int) -> Optional[User]: ... │ └── services/ # 【业务逻辑层】:组合多个仓库、处理事务、执行领域规则、返回 DTO 或 dict │ ├── __init__.py │ └── user_service.py # class UserService: def create_user(self, name: str, email: str) -> dict: ... ├── config.py # 数据库连接配置(含连接池参数) ├── main.py # 应用入口,初始化各层依赖并注册路由 └── requirements.txt提示:三层之间严格禁止反向依赖。
services可以 importrepositories,repositories可以 importmodels,但反过来绝对不行。这是靠 IDE 的 import 检查+CI 阶段的pylint --disable=all --enable=import-error来守住的底线。
2.1 初始化数据库连接池:别再用create_engine(url)硬编码
直接create_engine("mysql+pymysql://...")是初学者最常犯的错误——它每次调用都新建连接,高并发下秒变连接数爆炸。三层架构的第一道防线,是把连接池作为全局单例注入到repositories层:
# config.py from sqlalchemy import create_engine from sqlalchemy.pool import QueuePool def get_db_engine(): return create_engine( "mysql+pymysql://user:pass@localhost:3306/mydb", poolclass=QueuePool, pool_size=10, # 核心连接数,非最大值 max_overflow=20, # 超出 pool_size 后可临时创建的连接数 pool_timeout=30, # 获取连接超时秒数(关键!避免请求卡死) pool_recycle=3600, # 连接复用 1 小时后自动回收(防 MySQL wait_timeout 断连) echo=False, # 生产环境必须 False,否则每条 SQL 都打日志 connect_args={"charset": "utf8mb4"} )逻辑说明:pool_size=10不代表最多 10 个连接,而是“常驻连接池”的数量;max_overflow=20允许在流量高峰时临时扩容到 30 个连接。pool_recycle=3600是血泪经验——MySQL 默认wait_timeout=28800(8小时),但网络抖动或中间件(如 ProxySQL)可能提前 kill 连接,设为 1 小时能确保连接在失效前被主动回收重连。
2.2 模型层:用 SQLAlchemy 声明式基类定义领域实体
models/目录下不写业务逻辑,只做一件事:精准映射数据库表结构,并声明领域约束。这里的关键是用__table_args__统一管理索引和注释,而非散落在各字段:
# app/models/user.py from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Index from sqlalchemy.ext.declarative import declarative_base from datetime import datetime Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(50), nullable=False, index=True) # 显式声明索引 email = Column(String(255), nullable=False, unique=True) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) bio = Column(Text, nullable=True) # 表级索引和注释集中管理 __table_args__ = ( Index('ix_users_username_email', 'username', 'email'), # 复合索引 {"comment": "用户主表,存储注册用户基础信息"} # 表注释,生成文档时可提取 )参数说明:index=True在username字段上建单列索引,Index(...)建复合索引,两者不冲突;{"comment": ...}是 SQLAlchemy 1.4+ 支持的表级注释,后续用sqlacodegen反向生成模型时能保留。新手易错点:不要在Column里写default="xxx",字符串默认值要用default=text("CURRENT_TIMESTAMP")或 Python 函数,否则迁移时会报错。
2.3 仓库层:封装 CRUD,暴露接口而非实现
repositories/是三层中最薄的一层,但它决定了上层能否解耦。核心原则:每个仓库只操作一张表,方法名直译 SQL 动作,返回模型实例或None:
# app/repositories/user_repo.py from typing import Optional, List from sqlalchemy.orm import Session from app.models.user import User class UserRepository: def __init__(self, db_session: Session): self.db_session = db_session # 依赖注入,非全局变量 def get_by_id(self, user_id: int) -> Optional[User]: return self.db_session.query(User).filter(User.id == user_id).first() def get_by_email(self, email: str) -> Optional[User]: return self.db_session.query(User).filter(User.email == email).first() def create(self, username: str, email: str) -> User: user = User(username=username, email=email) self.db_session.add(user) self.db_session.flush() # 关键!获取自增 ID,但不提交事务 return user def list_active(self, limit: int = 100) -> List[User]: return self.db_session.query(User).filter(User.is_active == True).limit(limit).all()逻辑说明:self.db_session.flush()是关键动作——它把对象写入数据库并生成user.id,但不commit(),把事务控制权交给上层services。这样UserService.create_user()才能在同一事务里完成“创建用户+发通知+写日志”三步操作。切记:仓库层绝不调用commit()或rollback(),那是业务层的责任。
3. 业务逻辑层:组合仓库、控制事务、输出 DTO
services/层是三层架构的“大脑”,它不碰 SQL,不关心表结构,只专注领域规则。它的价值体现在三个地方:事务边界清晰、异常语义明确、DTO 与模型分离。我们以“用户注册”为例,展示如何把分散的原子操作聚合成有业务意义的流程:
3.1 定义业务异常:用自定义 Exception 替代 HTTP 状态码硬编码
HTTP 状态码是表现层的事,业务层只管“对错”。定义清晰的异常类型,能让上层路由精准映射状态码,且便于监控告警:
# app/services/exceptions.py class ServiceException(Exception): """所有业务异常的基类""" pass class UserAlreadyExistsException(ServiceException): """邮箱已被注册""" pass class InvalidEmailException(ServiceException): """邮箱格式非法""" pass3.2 UserService 实现:事务控制 + 异常转换 + DTO 输出
# app/services/user_service.py import re from typing import Dict, Any from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.repositories.user_repo import UserRepository from app.services.exceptions import UserAlreadyExistsException, InvalidEmailException class UserService: def __init__(self, user_repo: UserRepository, db_session: Session): self.user_repo = user_repo self.db_session = db_session def create_user(self, username: str, email: str) -> Dict[str, Any]: # 1. 领域规则校验(业务层职责) if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email): raise InvalidEmailException(f"Invalid email format: {email}") # 2. 检查邮箱是否已存在(调用仓库) if self.user_repo.get_by_email(email): raise UserAlreadyExistsException(f"Email already registered: {email}") # 3. 创建用户(调用仓库,注意:未 commit) user = self.user_repo.create(username=username, email=email) # 4. 业务扩展点:例如发送欢迎邮件(此处伪代码) # self._send_welcome_email(user.email) # 5. 提交事务(业务层决定成功边界) try: self.db_session.commit() except IntegrityError as e: self.db_session.rollback() # 捕获唯一键冲突,转为业务异常 if "users_email_key" in str(e.orig): raise UserAlreadyExistsException(f"Email already registered: {email}") raise # 6. 返回 DTO(Data Transfer Object),非模型实例 return { "id": user.id, "username": user.username, "email": user.email, "created_at": user.created_at.isoformat() if user.created_at else None } def _send_welcome_email(self, email: str): # 此处可集成 Celery 或直接调用邮件 SDK pass逻辑说明:create_user方法内self.db_session.commit()是事务提交点,意味着“校验通过→创建成功→发邮件→提交”是一体的。若_send_welcome_email失败,commit()已执行,用户数据已落库——这不符合业务语义。正确做法是把发邮件改为异步任务(如 Celery),或在此方法内捕获所有异常并rollback()。本例为简化,假设发邮件无失败风险。返回的dict是 DTO,剥离了 SQLAlchemy 的_sa_instance_state等 ORM 元数据,前端或 API 直接序列化无压力。
3.3 在路由中组装三层:依赖注入与异常映射
main.py是胶水,它把三层粘合成可用的服务。重点看get_user_service()如何构造依赖链:
# main.py from flask import Flask, jsonify, request from sqlalchemy.orm import sessionmaker from app.config import get_db_engine from app.repositories.user_repo import UserRepository from app.services.user_service import UserService from app.services.exceptions import UserAlreadyExistsException, InvalidEmailException app = Flask(__name__) engine = get_db_engine() SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_user_service() -> UserService: """工厂函数:每次请求创建新 service 实例,保证 session 隔离""" db_session = SessionLocal() user_repo = UserRepository(db_session) return UserService(user_repo=user_repo, db_session=db_session) @app.route("/api/users", methods=["POST"]) def create_user(): data = request.get_json() username = data.get("username") email = data.get("email") try: service = get_user_service() result = service.create_user(username=username, email=email) return jsonify({"success": True, "data": result}), 201 except UserAlreadyExistsException as e: return jsonify({"success": False, "error": "Email already exists"}), 409 except InvalidEmailException as e: return jsonify({"success": False, "error": "Invalid email format"}), 400 except Exception as e: # 未预期异常,记录日志并返回 500 app.logger.error(f"Unexpected error in create_user: {e}") return jsonify({"success": False, "error": "Internal server error"}), 500 if __name__ == "__main__": app.run(debug=False) # 生产环境禁用 debug参数说明:sessionmaker(autocommit=False, autoflush=False)是关键配置。autocommit=False确保commit()必须显式调用;autoflush=False避免 SQLAlchemy 在query()前自动flush()导致脏数据写入——这会让事务边界失控。get_user_service()每次请求新建实例,保证不同请求的db_session互不干扰。
4. 避坑指南:三层架构落地中最痛的 5 个翻车现场
三层架构看似简单,但实际落地时,90% 的问题出在“以为懂了”和“真跑通了”之间的鸿沟。以下是我在模拟项目 X 和某高校实验室数据平台中反复验证过的 5 个高频坑,每一条都附带现象、根因和可立即执行的修复方案。
4.1 现象:应用启动后,前几个请求正常,随后大量pymysql.err.OperationalError: (2013, 'Lost connection...')
原因:MySQL 服务端wait_timeout(默认 28800 秒)与 SQLAlchemy 连接池pool_recycle参数不匹配,导致连接池中“存活但失效”的连接被复用。
解决:在config.py的create_engine()中,将pool_recycle设为比wait_timeout小至少 300 秒的值。例如 MySQLwait_timeout=28800,则设pool_recycle=28500(7 小时 55 分)。同时,在connect_args中添加"autocommit": True(仅对读操作安全)或确保所有写操作都在commit()后释放连接。
4.2 现象:UserRepository.get_by_id()返回User对象,但访问user.bio时抛出DetachedInstanceError
原因:db_session在仓库方法返回后被关闭(如main.py中未正确管理生命周期),导致模型实例脱离 session,懒加载字段(bio是Text类型,常设为 lazy load)无法触发查询。
解决:在repositories层方法中,对需要返回的字段显式load_only或joinedload。例如:
# app/repositories/user_repo.py def get_by_id(self, user_id: int) -> Optional[User]: return self.db_session.query(User).options( load_only(User.id, User.username, User.email, User.created_at) # 显式加载所需字段 ).filter(User.id == user_id).first()或在模型定义中将bio设为deferred=True,避免默认加载。
4.3 现象:UserService.create_user()中调用self.user_repo.create()后,user.id为None
原因:create()方法中用了self.db_session.add(user)但漏掉了self.db_session.flush()。add()只是标记对象为 pending,flush()才触发 INSERT 并获取自增 ID。
解决:所有需要立即获取自增主键的场景,add()后必须跟flush()。commit()会隐式flush(),但业务层通常需要flush()后继续操作(如用user.id创建关联记录),故必须显式调用。
4.4 现象:单元测试中test_create_user通过,但集成测试时get_by_email()查不到刚创建的用户
原因:测试用的db_session与get_by_email()使用的 session 不是同一个实例,且未共享事务上下文。常见于在pytest中用SessionLocal()创建 session,但未在测试函数间传递或使用scoped_session。
解决:为测试单独创建TestingSessionLocal,并在conftest.py中用pytest.fixture管理:
# conftest.py from sqlalchemy.orm import scoped_session, sessionmaker from app.config import get_db_engine TestingSessionLocal = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=get_db_engine()) ) @pytest.fixture def db_session(): session = TestingSessionLocal() yield session session.rollback() session.close()然后在测试中注入该 fixture,确保仓库和业务层使用同一 session。
4.5 现象:UserModel中定义了__table_args__ = {"comment": "用户表"},但用sqlacodegen生成模型时注释丢失
原因:sqlacodegen默认不解析__table_args__中的comment,需手动指定--tables或升级到支持--noinflect的新版。
解决:使用sqlacodegen mysql+pymysql://... --outfile models.py --noinflect,或更可靠的方式——放弃sqlacodegen,改用 SQLAlchemy 1.4+ 的MetaData.reflect()+ 自定义模板生成,确保注释完整。生产环境推荐手写模型,sqlacodegen仅作初稿参考。
5. 进阶验证:用 pytest 写三层单元测试,让架构自己证明可靠性
写测试不是为了应付 KPI,而是让三层架构的契约关系可视化、可量化。真正的“跑通”,不是flask run能返回 JSON,而是当你修改repositories/user_repo.py的 SQL 逻辑时,services/user_service.py的测试用例会立刻失败,逼你确认业务规则是否被破坏。下面这套测试策略,是我在线上系统中坚持执行 2 年的最小闭环。
5.1 测试目录结构:与源码一一映射,命名即意图
tests/ ├── __init__.py ├── test_repositories/ │ ├── __init__.py │ └── test_user_repo.py # 测试 UserRepository 的每个方法 ├── test_services/ │ ├── __init__.py │ └── test_user_service.py # 测试 UserService 的每个业务流 └── conftest.py # 全局 fixture,提供测试数据库 session注意:测试目录与
app/目录平行,不嵌套其中。这样pytest tests/可独立运行,且 IDE 能自动识别测试文件。
5.2 用内存 SQLite 模拟 MySQL:零外部依赖,秒级启动
为避免测试依赖真实 MySQL 服务,用 SQLite 内存数据库模拟,通过 SQLAlchemy 的方言兼容性保证逻辑一致:
# conftest.py import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models import Base # 导入所有模型的 Base @pytest.fixture(scope="function") def db_session(): # 创建内存 SQLite 引擎,echo=False 关闭 SQL 日志 engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) # 创建所有表 SessionLocal = sessionmaker(bind=engine) session = SessionLocal() yield session session.rollback() session.close()逻辑说明:sqlite:///:memory:是纯内存数据库,每次yield新建一个干净实例,session.rollback()确保测试间数据隔离。Base.metadata.create_all(engine)自动创建User等所有模型对应的表,无需手动建表 SQL。
5.3 Repository 层测试:验证数据存取的原子性
test_user_repo.py只测试UserRepository本身,不涉及业务规则。重点覆盖:空结果、单条查询、批量查询、创建后可查:
# tests/test_repositories/test_user_repo.py import pytest from app.repositories.user_repo import UserRepository from app.models.user import User def test_get_by_id_not_exists(db_session): repo = UserRepository(db_session) assert repo.get_by_id(999) is None def test_get_by_id_exists(db_session): # 先插入测试数据 user = User(username="test", email="test@example.com") db_session.add(user) db_session.commit() repo = UserRepository(db_session) found = repo.get_by_id(user.id) assert found is not None assert found.username == "test" def test_create_returns_user_with_id(db_session): repo = UserRepository(db_session) user = repo.create(username="new", email="new@example.com") assert user.id is not None # flush() 后 id 已生成 assert user.username == "new"参数说明:每个测试函数接收db_sessionfixture,保证独享数据库实例。test_create_returns_user_with_id验证flush()是否生效——这是仓库层最核心的契约。
5.4 Service 层测试:验证业务规则与异常流
test_user_service.py测试UserService的完整业务流,包括正常路径和所有异常分支。这里用pytest.raises精准捕获业务异常:
# tests/test_services/test_user_service.py import pytest from app.repositories.user_repo import UserRepository from app.services.user_service import UserService from app.services.exceptions import UserAlreadyExistsException, InvalidEmailException def test_create_user_success(db_session): user_repo = UserRepository(db_session) service = UserService(user_repo=user_repo, db_session=db_session) result = service.create_user(username="alice", email="alice@example.com") assert result["username"] == "alice" assert result["email"] == "alice@example.com" assert "id" in result def test_create_user_duplicate_email(db_session): user_repo = UserRepository(db_session) service = UserService(user_repo=user_repo, db_session=db_session) # 先创建一个用户 service.create_user(username="bob", email="bob@example.com") # 再用相同 email 创建,应抛出业务异常 with pytest.raises(UserAlreadyExistsException): service.create_user(username="charlie", email="bob@example.com") def test_create_user_invalid_email(db_session): user_repo = UserRepository(db_session) service = UserService(user_repo=user_repo, db_session=db_session) with pytest.raises(InvalidEmailException): service.create_user(username="dan", email="invalid-email")逻辑说明:test_create_user_duplicate_email验证了“检查邮箱存在性”这一业务规则是否被正确封装在UserService中,且异常类型准确。如果未来有人把邮箱去重逻辑误删,这个测试会立刻红。
5.5 运行与验证:一条命令,看到架构的健康度
在项目根目录执行:
pip install pytest pytest-cov pytest tests/ --cov=app --cov-report=html --cov-fail-under=80--cov-fail-under=80表示单元测试覆盖率低于 80% 则 CI 失败。这不是为了数字好看,而是因为:
models/层覆盖率应接近 100%(字段定义无逻辑,但需验证__table_args__加载)repositories/层覆盖率应 ≥ 95%(每个 CRUD 方法必须有对应测试)services/层覆盖率应 ≥ 85%(覆盖所有正常流 + 主要异常流)
当我第一次把模拟项目 X 的覆盖率从 32% 拉到 87% 时,最直观的变化是:重构UserRepository的 SQL 时,不再需要手动检查所有调用方;新增UserService的“密码重置”功能时,只需写新测试,旧测试自动守护现有逻辑。这种确定性,就是三层架构给工程师的“后悔药”。
希望帮到你。
本文还有配套的精品资源,点击获取