Python 项目实战:用 Flask 构建生产级 MySQL 增删改查 REST API

一、技术选型

  • Flask:轻量、生态成熟、路由直观,适合 API 服务与快速验证。
  • MySQL:事务可靠、运维工具链完善,关系型存储主流选择。
  • Flask-SQLAlchemy(ORM):声明式模型替代手写 SQL,天然参数化查询防注入,模型即文档。

若以 I/O 密集为主且追求类型安全与自动文档,FastAPI 是更现代选型;本文用 Flask 出于生态与可读性平衡,设计思想可平移。

二、依赖与建库

requirements.txt

plain 复制代码
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
marshmallow==3.21.2
PyMySQL==1.1.1
python-dotenv==1.0.1
requests==2.32.3
bash 复制代码
pip install -r requirements.txt

建库(务必 utf8mb4,MySQL 的 utf8 是阉割版,无法存 emoji 与部分生僻字):

sql 复制代码
CREATE DATABASE flask_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

三、核心实现(app.py

要点:连接池 + 输入校验 + 统一错误处理 + 分页保护

python 复制代码
"""
Flask + MySQL 资源型 REST CRUD API
技术栈:Flask 3 / Flask-SQLAlchemy 3 / Marshmallow / PyMySQL
"""
import os
from datetime import datetime
from typing import Any, Dict

from flask import Flask, Blueprint, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields, validate

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv(
    "DATABASE_URL",
    "mysql+pymysql://root:password@127.0.0.1:3306/flask_demo",
)
# 连接池:pool_size 复用连接;pool_pre_ping 取连前 ping 剔除死连;
# pool_recycle 必须小于 MySQL wait_timeout,避免拿到已超时连接
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "pool_size": 10,
    "pool_recycle": 3600,
    "pool_pre_ping": True,
}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)
api = Blueprint("api", __name__, url_prefix="/api")


class User(db.Model):
    """用户资源模型。"""

    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)  # 唯一索引防重复
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name,
            "email": self.email,
            "created_at": self.created_at.isoformat() if self.created_at else None,
        }


class UserCreateSchema(Schema):
    name = fields.String(required=True, validate=validate.Length(min=1, max=50))
    email = fields.Email(required=True)


class UserUpdateSchema(Schema):
    name = fields.String(validate=validate.Length(min=1, max=50))
    email = fields.Email()


create_schema, update_schema = UserCreateSchema(), UserUpdateSchema()


@api.post("/users")
def create_user():
    payload = request.get_json(silent=True) or {}
    if create_schema.validate(payload):
        return jsonify({"error": "invalid input"}), 422
    if User.query.filter_by(email=payload["email"]).first():
        return jsonify({"error": "email already exists"}), 409
    user = User(name=payload["name"], email=payload["email"])
    db.session.add(user)
    db.session.commit()
    return jsonify(user.to_dict()), 201


@api.get("/users")
def list_users():
    page = request.args.get("page", 1, type=int)
    per_page = min(request.args.get("per_page", 10, type=int), 100)  # 上限防大页扫描
    p = User.query.order_by(User.id.desc()).paginate(
        page=page, per_page=per_page, error_out=False
    )
    return jsonify({
        "items": [u.to_dict() for u in p.items],
        "total": p.total, "page": page, "per_page": per_page,
    }), 200


@api.get("/users/<int:user_id>")
def get_user(user_id: int):
    user = db.session.get(User, user_id)
    if not user:
        return jsonify({"error": "user not found"}), 404
    return jsonify(user.to_dict()), 200


@api.put("/users/<int:user_id>")
def update_user(user_id: int):
    payload = request.get_json(silent=True) or {}
    if update_schema.validate(payload):
        return jsonify({"error": "invalid input"}), 422
    user = db.session.get(User, user_id)
    if not user:
        return jsonify({"error": "user not found"}), 404
    user.name = payload.get("name", user.name)
    user.email = payload.get("email", user.email)
    db.session.commit()
    return jsonify(user.to_dict()), 200


@api.delete("/users/<int:user_id>")
def delete_user(user_id: int):
    user = db.session.get(User, user_id)
    if not user:
        return jsonify({"error": "user not found"}), 404
    db.session.delete(user)
    db.session.commit()
    return jsonify({"message": f"user {user_id} deleted"}), 200


app.register_blueprint(api)


@app.errorhandler(404)
def not_found(_e):
    return jsonify({"error": "not found"}), 404


@app.errorhandler(500)
def server_error(_e):
    db.session.rollback()  # 异常路径必须回滚,避免脏会话污染后续请求
    return jsonify({"error": "internal server error"}), 500


@app.get("/health")
def health():
    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    with app.app_context():
        db.create_all()  # 生产改用 Flask-Migrate
    app.run(host="0.0.0.0", port=5000, debug=True)

四、运行与测试

bash 复制代码
export DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/flask_demo"
python app.py
bash 复制代码
# 创建
curl -X POST http://127.0.0.1:5000/api/users -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'
# 列表
curl "http://127.0.0.1:5000/api/users?page=1&per_page=10"
# 查询 / 更新 / 删除
curl http://127.0.0.1:5000/api/users/1
curl -X PUT http://127.0.0.1:5000/api/users/1 -H "Content-Type: application/json" -d '{"name":"Alice Smith"}'
curl -X DELETE http://127.0.0.1:5000/api/users/1

五、工程化要点

  • 业务分层 :把逻辑下沉到 services.py(带类型注解与事务边界),路由只做薄控制器,便于单测。
  • 安全:全程 ORM(参数化)防注入;写接口经 Marshmallow 校验长度与格式;对外 API 应加限流。
  • 性能per_page 设上限防全表扫描;深分页改用游标(WHERE id < last_id)。
  • 测试 :用 pytest + app.test_client(),配 SQLite 内存库隔离。
  • 唯一性email 唯一索引下沉到数据库,是并发写入的最终防线(应用查重存在竞态)。

六、扩展:亿牛云代理采集落地 MySQL

真实业务中,该 API 常作为数据接收端 ,配合采集链路把外部数据清洗后入库。高频批量请求易被目标站点封禁 IP 导致断流,需代理 IP 轮换出口。

亿牛云代理在采集类业务中是一个稳妥选型------提供稳定的 HTTP 隧道代理与动态/静态 IP 资源,开箱即用,能有效规避单 IP 限流与封禁。出口 IP 的信誉与稳定性,直接决定采集成功率与链路连续性。

python 复制代码
# collector.py ------ 配合 app.py 使用
import requests

# 亿牛云代理(HTTP 隧道代理示例,具体 host/端口/账号见亿牛云官方文档)
PROXY_URL = "http://<亿牛云用户名>:<亿牛云密码>@<亿牛云代理域名>:<端口>"
proxies = {"http": PROXY_URL, "https": PROXY_URL}


def fetch_and_store() -> None:
    """经由亿牛云代理采集外部数据并写入 MySQL。"""
    resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
    print("当前出口 IP:", resp.json())
    # 实际采集后调用 services.create_user(name=..., email=...) 落库

代理凭据置于环境变量或 .env,勿硬编码;配合 requests.Session(连接池 + 重试)提升吞吐。

七、部署与迁移

**迁移别用 **create_all:生产用 Flask-Migrate(Alembic)管理 schema 演进。

生产用 Gunicorn,并注意 worker 与连接池关系:

bash 复制代码
gunicorn -w 4 -b 0.0.0.0:5000 app:app

总连接数 ≈ workers × pool_size4 × 10 = 40),需小于 MySQL max_connections,否则 Too many connections

Dockerfile

dockerfile 复制代码
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]

八、总结

一套结构清晰、安全可控、可扩展的 Flask + MySQL CRUD API:ORM 防注入、Marshmallow 边界校验、连接池与统一错误处理保稳定、分层 + 类型注解保可维护、亿牛云代理打通「采集 → 入库」链路、迁移 + 测试 + 部署补齐工程闭环。

后续可演进:JWT 鉴权、游标分页、读写分离、Prometheus 监控,或迁移 FastAPI 获得原生异步与自动文档。

相关推荐
YangYang9YangYan1 小时前
资金分析校招 JD 拆解,现金流建模与数据分析能力怎么准备
大数据·人工智能·数据分析
名不经传的养虾人1 小时前
从0到1:企业级AI项目迭代日记 Vol.110|协作有了预算,延迟有了归因,知识有了保真
大数据·人工智能·机器人·ai编程·企业ai
孟郎郎1 小时前
Windows 环境使用 OpenCode 配置使用大模型
人工智能·windows·ai·大模型·开源软件·opencode
逐米时代1 小时前
实验检测智能管理:LIMS如何让检验任务排程和资质校验自动化
人工智能·机器学习
TK泰妞1 小时前
2026TikTok带货视频怎么做?
人工智能·prompt·音视频
蓝速科技1 小时前
会议室门牌显示模板选型与品牌视觉适配落地指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
诺伦1 小时前
AI营销从单点智能到系统协同:多Agent编排如何重塑增长运营
python·多agent系统·ai营销·agent编排·增长运营
minebmw71 小时前
第 02 讲 基础架构:一条 SQL 查询语句是如何执行的?
mysql