作为一名长期使用Python开发各种项目的工程师,我深刻体会到良好的项目结构对于代码可维护性的重要性。今天我想分享一些关于如何合理组织Python项目结构的经验,特别是结合SQLAlchemy这样的ORM工具时,如何让数据库相关代码保持清晰和可扩展。
项目结构不仅仅是文件目录的排列,它反映了你对整个系统的理解和设计思路。一个糟糕的项目结构会让后续的维护和扩展变得异常困难,而一个良好的结构则能让团队协作更加顺畅。
在小型项目中,你可能可以把所有代码都放在一个文件里。但随着项目规模扩大,这种做法的弊端会迅速显现:
我曾经接手过一个将所有模型、视图、业务逻辑都混在一起的Django项目,光是理清各个部分的职责就花了两周时间。这种经历让我深刻认识到良好项目结构的重要性。
在缺乏规划的项目中,我经常看到以下问题:
这些问题往往在项目初期不明显,但随着功能增加会变得越来越严重。
对于大多数Python项目,我推荐从这样的基础结构开始:
code复制my_project/
├── my_project/ # 主包
│ ├── __init__.py # 包初始化文件
│ ├── main.py # 入口点
│ ├── models.py # 数据模型定义
│ ├── schemas.py # 数据验证模式
│ ├── crud.py # 数据库操作
│ ├── database.py # 数据库配置
│ ├── config.py # 应用配置
│ └── utils.py # 工具函数
├── tests/ # 测试代码
│ ├── __init__.py
│ └── test_models.py
├── requirements.txt # 依赖列表
└── README.md # 项目说明
这种结构清晰划分了不同职责的代码,适合中小型项目。随着项目增长,每个文件可能会扩展成单独的包。
这种分离确保了单一职责原则,使代码更容易理解和测试。
当项目功能增多时,可以按功能模块组织代码:
code复制my_project/
├── my_project/
│ ├── __init__.py
│ ├── main.py
│ ├── core/ # 核心功能
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── crud.py
│ │ └── schemas.py
│ ├── api/ # API相关
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── deps.py
│ ├── services/ # 业务逻辑
│ │ ├── __init__.py
│ │ └── user.py
│ ├── db/ # 数据库相关
│ │ ├── __init__.py
│ │ ├── session.py
│ │ └── base.py
│ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── security.py
│ └── logger.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ └── test_api.py
├── requirements.txt
└── README.md
这种结构特别适合Web应用或微服务,每个功能模块可以独立开发和测试。
对于使用SQLAlchemy的项目,我建议这样组织数据库代码:
python复制from sqlalchemy.orm import declarative_base
Base = declarative_base()
python复制from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///example.db")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
python复制from sqlalchemy import Column, Integer, String
from ..db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
python复制from sqlalchemy.orm import Session
from .models import User
def get_user(db: Session, user_id: int):
return db.query(User).filter(User.id == user_id).first()
这种组织方式确保了数据库相关代码的高内聚和低耦合。
正确处理数据库会话是使用SQLAlchemy的关键。我推荐以下模式:
python复制# db/session.py
from contextlib import contextmanager
from sqlalchemy.orm import Session
@contextmanager
def get_db() -> Session:
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
使用时:
python复制from db.session import get_db
def create_user(username: str, password: str):
with get_db() as db:
db_user = User(username=username, hashed_password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
这种方式确保会话总是被正确关闭,事务要么提交要么回滚。
在定义模型关系时,注意以下几点:
back_populates或backref以避免混淆python复制class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", back_populates="posts")
tags = relationship("Tag", secondary="post_tags", back_populates="posts")
class PostTag(Base):
__tablename__ = "post_tags"
post_id = Column(Integer, ForeignKey("posts.id"), primary_key=True)
tag_id = Column(Integer, ForeignKey("tags.id"), primary_key=True)
selectinload或joinedload避免N+1查询问题python复制from sqlalchemy.orm import selectinload
# 避免N+1查询
posts = db.query(Post).options(selectinload(Post.tags)).all()
# 只查询需要的列
users = db.query(User.username, User.email).all()
为测试专门配置数据库,可以使用pytest fixture:
python复制# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from my_project.db.base import Base
@pytest.fixture(scope="module")
def test_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(engine)
python复制# tests/test_models.py
def test_user_model(test_db):
from my_project.core.models import User
user = User(username="testuser", hashed_password="secret")
test_db.add(user)
test_db.commit()
assert user.id is not None
assert user.username == "testuser"
对于更大型的项目,可以考虑以下扩展:
code复制my_project/
├── my_project/
│ ├── domains/ # 业务领域
│ │ ├── user/ # 用户领域
│ │ │ ├── models.py
│ │ │ ├── services.py
│ │ │ └── repositories.py
│ │ └── product/ # 产品领域
│ │ ├── models.py
│ │ └── services.py
│ ├── infrastructure/ # 基础设施
│ │ ├── database/
│ │ ├── cache/
│ │ └── messaging/
│ └── api/ # 接口层
│ ├── v1/ # API版本
│ └── v2/
问题:A模块需要B模块,B模块又需要A模块
解决方案:
问题:数据库引擎等全局状态如何管理
解决方案:
python复制# my_project/app.py
from fastapi import FastAPI
from .db.session import engine
from .core.models import Base
def create_app():
app = FastAPI()
@app.on_event("startup")
def startup():
Base.metadata.create_all(bind=engine)
return app
问题:开发、测试、生产环境的不同配置
解决方案:
python复制# config.py
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./test.db")
class Config:
env_file = ".env"
settings = Settings()
在实际项目中,我发现以下几点特别重要:
我曾经参与过一个项目,前期没有规划好结构,导致后期添加每个新功能都变得异常困难。经过痛苦的重构后,我们采用了按功能模块划分的结构,开发效率显著提高。
另一个经验是,数据库相关代码应该尽可能与业务逻辑分离。这样当需要更换ORM或数据库时,影响范围可以控制在最小。