【Python 进阶 | 第四篇】Psycopg3 + Flask 实现 PostgreSQL CRUD 全流程:从连接池到RESTful接口

文章目录

    • [1. 环境准备:依赖安装](#1. 环境准备:依赖安装)
      • [1.1 安装依赖](#1.1 安装依赖)
      • [1.2 项目结构](#1.2 项目结构)
      • [1.3 环境变量配置](#1.3 环境变量配置)
    • [2. Psycopg3:新一代 PostgreSQL 驱动](#2. Psycopg3:新一代 PostgreSQL 驱动)
      • [2.1 基本连接](#2.1 基本连接)
      • [2.2 执行 SQL:cursor 的使用](#2.2 执行 SQL:cursor 的使用)
      • [2.3 SQL 注入防护:参数化查询](#2.3 SQL 注入防护:参数化查询)
      • [2.4 查询结果处理](#2.4 查询结果处理)
      • [2.5 事务管理:commit 与 rollback](#2.5 事务管理:commit 与 rollback)
    • [3. 连接池管理:性能与资源的关键](#3. 连接池管理:性能与资源的关键)
      • [3.1 为什么需要连接池](#3.1 为什么需要连接池)
      • [3.2 连接池配置详解](#3.2 连接池配置详解)
      • [3.3 完整连接池管理器](#3.3 完整连接池管理器)
      • [3.4 上下文管理器封装](#3.4 上下文管理器封装)
    • [4. Flask 路由设计:RESTful 接口规范](#4. Flask 路由设计:RESTful 接口规范)
      • [4.1 RESTful 路由设计原则](#4.1 RESTful 路由设计原则)
      • [4.2 Flask 基础结构](#4.2 Flask 基础结构)
      • [4.3 统一响应格式](#4.3 统一响应格式)
      • [4.4 请求参数验证](#4.4 请求参数验证)
    • [5. 完整 CRUD 流程:用户管理实战](#5. 完整 CRUD 流程:用户管理实战)
      • [5.1 数据库初始化](#5.1 数据库初始化)
      • [5.2 用户模型](#5.2 用户模型)
      • [5.3 用户路由(CRUD 完整实现)](#5.3 用户路由(CRUD 完整实现))
      • [5.4 PUT vs PATCH 的区别](#5.4 PUT vs PATCH 的区别)
      • [5.5 常见 HTTP 状态码](#5.5 常见 HTTP 状态码)
    • [6. 总结](#6. 总结)

前言   此文是我学习 Python Web 开发过程中整理的数据库操作笔记,涵盖 Psycopg3 连接 PostgreSQL、连接池管理、Flask 路由设计以及完整的 CRUD 操作流程,结合代码实践总结而来,分享给大家。一是为了记录,二是用于日后的回顾,三也是希望能给其他初学者带来一点点帮助。

目录

    1. 环境准备:依赖安装
    1. Psycopg3:新一代 PostgreSQL 驱动
    1. 连接池管理:性能与资源的关键
    1. Flask 路由设计:RESTful 接口规范
    1. 完整 CRUD 流程:用户管理实战
    1. 总结

1. 环境准备:依赖安装

1.1 安装依赖

bash 复制代码
pip install psycopg[pip]     # Psycopg3 核心驱动
pip install psycopg-binary   # 二进制版本(无需编译)
pip install flask            # Web 框架
pip install python-dotenv    # 环境变量管理

1.2 项目结构

复制代码
project/
├── app.py                  # Flask 应用入口
├── config.py               # 配置文件
├── models/
│   └── user.py            # 用户模型
├── routes/
│   └── user_routes.py     # 用户路由
├── db/
│   ├── __init__.py
│   ├── pool.py            # 连接池管理
│   └── init_db.py         # 数据库初始化
├── .env                    # 环境变量
└── requirements.txt

1.3 环境变量配置

bash 复制代码
# .env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=testdb
DB_USER=postgres
DB_PASSWORD=your_password
FLASK_ENV=development
FLASK_DEBUG=True
python 复制代码
# db_config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    DB_HOST = os.getenv("DB_HOST", "localhost")
    DB_PORT = int(os.getenv("DB_PORT", 5432))
    DB_NAME = os.getenv("DB_NAME", "testdb")
    DB_USER = os.getenv("DB_USER", "postgres")
    DB_PASSWORD = os.getenv("DB_PASSWORD", "")
    DB_MIN_CONN = int(os.getenv("DB_MIN_CONN", 2))
    DB_MAX_CONN = int(os.getenv("DB_MAX_CONN", 10))

一句话总结:Psycopg3 是 PostgreSQL 的最新官方驱动,相比 Psycopg2,它支持 async/await、更现代的连接池、更好的类型提示,是未来趋势。


2. Psycopg3:新一代 PostgreSQL 驱动

2.1 基本连接

python 复制代码
import psycopg

# 最简单的连接方式
conn = psycopg.connect(
    host="localhost",
    port=5432,
    dbname="testdb",
    user="postgres",
    password="your_password"
)
print(conn)  # <Connection ...>

# 使用连接字符串(类似 SQLAlchemy)
conn = psycopg.connect("postgresql://postgres:your_password@localhost:5432/testdb")

# 关闭连接
conn.close()

2.2 执行 SQL:cursor 的使用

python 复制代码
conn = psycopg.connect("postgresql://postgres:your_password@localhost:5432/testdb")

# 创建 cursor
cursor = conn.cursor()

# 执行查询
cursor.execute("SELECT version();")
result = cursor.fetchone()
print(f"PostgreSQL 版本: {result[0]}")

# 执行 DML 操作(需要 commit)
cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("张三", "zhangsan@example.com"))
conn.commit()  # 不要忘记 commit!

# 批量插入
users = [
    ("李四", "lisi@example.com"),
    ("王五", "wangwu@example.com"),
    ("赵六", "zhaoliu@example.com")
]
cursor.executemany(
    "INSERT INTO users (name, email) VALUES (%s, %s)",
    users
)
conn.commit()

# 关闭
cursor.close()
conn.close()

2.3 SQL 注入防护:参数化查询

Psycopg3 永远使用参数化查询,永远不要用字符串拼接!

python 复制代码
# ❌ 错误:SQL 注入风险
user_input = "' OR '1'='1"
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")  # 危险!

# ✅ 正确:参数化查询
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
# 或者用 $1, $2 占位符
cursor.execute("SELECT * FROM users WHERE name = $1", (user_input,))

2.4 查询结果处理

python 复制代码
# 查询单条
cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
user = cursor.fetchone()       # 返回元组: (1, "张三", "zhangsan@example.com")
user = cursor.fetchone()       # 返回字典: {'id': 1, 'name': '张三', ...}

# 查询多条
cursor.execute("SELECT * FROM users LIMIT 10")
users = cursor.fetchall()      # 返回列表

# 使用 named tuple(更易读)
cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
user = cursor.fetchone(named=True)  # 返回 asyncpg.Row 对象,支持 user.name 访问

# 遍历查询结果
cursor.execute("SELECT * FROM users")
for user in cursor:
    print(user['name'], user['email'])

2.5 事务管理:commit 与 rollback

python 复制代码
try:
    cursor.execute("BEGIN")
    cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("张三", "zhangsan@example.com"))
    cursor.execute("UPDATE users SET email = %s WHERE name = %s", ("new@example.com", "张三"))
    conn.commit()  # 所有操作一起提交
except Exception as e:
    conn.rollback()  # 出错时回滚所有操作
    print(f"事务回滚: {e}")
finally:
    cursor.close()
    conn.close()

一句话总结:Psycopg3 的核心是 cursor 和 connection------cursor 执行 SQL,connection 管理事务;永远用参数化查询防注入,永远记得 commit。


3. 连接池管理:性能与资源的关键

3.1 为什么需要连接池

每次请求都创建新连接开销巨大。连接池预先建立一组连接,请求复用,用完归还,避免频繁创建销毁连接带来的性能损耗。

python 复制代码
# db/db_pool.py
import psycopg
from psycopg_pool import AsyncConnectionPool
from contextlib import contextmanager

# 同步连接池
from psycopg_pool import ConnectionPool

pool = ConnectionPool(
    "postgresql://postgres:your_password@localhost:5432/testdb",
    min_size=2,      # 最小连接数
    max_size=10,    # 最大连接数
    timeout=30,     # 获取连接超时时间(秒)
)

# 使用连接
with pool.connection() as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    users = cursor.fetchall()

# 上下文管理器会自动归还连接到池中

3.2 连接池配置详解

python 复制代码
pool = ConnectionPool(
    conninfo="postgresql://postgres:your_password@localhost:5432/testdb",
    min_size=2,                    # 池中最小连接数
    max_size=10,                   # 池中最大连接数
    timeout=30,                    # 获取连接超时(秒)
    max_idle_time=600,             # 空闲连接最大存活时间(秒)
    reconnect_timeout=300,         # 断开重连超时(秒)
)

3.3 完整连接池管理器

python 复制代码
# db/db_pool.py
import psycopg
from psycopg_pool import ConnectionPool
from config import Config


class DatabasePool:
    _instance = None
    _pool = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._pool = ConnectionPool(
                conninfo=(
                    f"postgresql://{Config.DB_USER}:{Config.DB_PASSWORD}"
                    f"@{Config.DB_HOST}:{Config.DB_PORT}/{Config.DB_NAME}"
                ),
                min_size=Config.DB_MIN_CONN,
                max_size=Config.DB_MAX_CONN,
                timeout=30,
            )
        return cls._instance

    @property
    def pool(self):
        return self._pool

    def get_connection(self):
        return self._pool.getconn()

    def release_connection(self, conn):
        self._pool.putconn(conn)

    def close_all(self):
        if self._pool:
            self._pool.close()


db_pool = DatabasePool()

3.4 上下文管理器封装

python 复制代码
# db/db_pool.py 补充

@contextmanager
def get_db_connection():
    """获取数据库连接的上下文管理器"""
    conn = db_pool.get_connection()
    try:
        yield conn
    except Exception as e:
        conn.rollback()
        raise e
    finally:
        db_pool.release_connection(conn)

@contextmanager
def get_db_cursor():
    """获取数据库 cursor 的上下文管理器"""
    with get_db_connection() as conn:
        cursor = conn.cursor()
        try:
            yield cursor
            conn.commit()
        except Exception as e:
            conn.rollback()
            raise e
        finally:
            cursor.close()

一句话总结:连接池是 Web 应用的性能关键------通过复用连接减少开销,通过上下文管理器自动管理连接的生命周期。


4. Flask 路由设计:RESTful 接口规范

4.1 RESTful 路由设计原则

操作 HTTP 方法 路由 说明
查询所有 GET /api/users 获取用户列表
查询单个 GET /api/users/<id> 获取指定用户
创建 POST /api/users 创建新用户
更新 PUT /api/users/<id> 完整更新
部分更新 PATCH /api/users/<id> 部分更新
删除 DELETE /api/users/<id> 删除用户

4.2 Flask 基础结构

python 复制代码
# start_up.py
from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

# 注册蓝图
from routes.user_routes import user_bp

app.register_blueprint(user_bp, url_prefix='/api')

if __name__ == '__main__':
    app.run(debug=True)

4.3 统一响应格式

python 复制代码
# utils/response.py
from flask import jsonify

def success_response(data=None, message="操作成功", code=200):
    """统一成功响应"""
    return jsonify({
        "code": code,
        "message": message,
        "data": data
    }), code

def error_response(message="操作失败", code=400, errors=None):
    """统一错误响应"""
    response = {
        "code": code,
        "message": message,
    }
    if errors:
        response["errors"] = errors
    return jsonify(response), code

4.4 请求参数验证

python 复制代码
# utils/validators.py
from functools import wraps
from flask import request
from utils.response import error_response

def validate_json(*required_fields):
    """验证请求 JSON 必填字段"""
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            if not request.is_json:
                return error_response("请求必须是 JSON 格式", 400)
            data = request.get_json()
            missing = [field for field in required_fields if field not in data or not data[field]]
            if missing:
                return error_response(f"缺少必填字段: {', '.join(missing)}", 400)
            return f(*args, **kwargs)
        return wrapper
    return decorator

def validate_pagination(f):
    """验证分页参数"""
    @wraps(f)
    def wrapper(*args, **kwargs):
        page = request.args.get('page', 1, type=int)
        page_size = request.args.get('page_size', 10, type=int)
        if page < 1:
            return error_response("page 必须大于等于 1", 400)
        if page_size < 1 or page_size > 100:
            return error_response("page_size 必须在 1-100 之间", 400)
        kwargs['page'] = page
        kwargs['page_size'] = page_size
        return f(*args, **kwargs)
    return wrapper

一句话总结:RESTful 路由设计的关键是 HTTP 方法语义正确、响应格式统一、参数验证前置------让 API 既规范又好维护。


5. 完整 CRUD 流程:用户管理实战

5.1 数据库初始化

python 复制代码
# db/init_db.py
from config.db_pool import db_pool


def init_database():
    """初始化数据库表"""
    with db_pool.connection() as conn:
        cursor = conn.cursor()

        # 创建 users 表
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id SERIAL PRIMARY KEY,
                name VARCHAR(100) NOT NULL,
                email VARCHAR(255) UNIQUE NOT NULL,
                age INTEGER,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)

        # 创建索引
        cursor.execute("""
            CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)
        """)

        conn.commit()
        print("数据库初始化完成")


if __name__ == '__main__':
    init_database()

5.2 用户模型

python 复制代码
# models/user.py
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List

@dataclass
class User:
    id: Optional[int]
    name: str
    email: str
    age: Optional[int]
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

    def to_dict(self):
        """转换为字典"""
        return {
            "id": self.id,
            "name": self.name,
            "email": self.email,
            "age": self.age,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }

    @classmethod
    def from_row(cls, row):
        """从数据库行转换为 User 对象"""
        return cls(
            id=row[0],
            name=row[1],
            email=row[2],
            age=row[3],
            created_at=row[4],
            updated_at=row[5],
        )

5.3 用户路由(CRUD 完整实现)

python 复制代码
# routes/user_routes.py
from flask import Blueprint, request
from config.db_pool import db_pool
from models.user import User
from utils.response import success_response, error_response
from utils.validators import validate_json, validate_pagination

user_bp = Blueprint('users', __name__)


# ---------- Create ----------
@user_bp.route('/users', methods=['POST'])
@validate_json('name', 'email')
def create_user():
    """创建用户"""
    data = request.get_json()

    try:
        with db_pool.connection() as conn:
            cursor = conn.cursor()
            cursor.execute(
                """
                INSERT INTO users (name, email, age)
                VALUES (%s, %s, %s)
                RETURNING id, name, email, age, created_at, updated_at
                """,
                (data['name'], data['email'], data.get('age'))
            )
            row = cursor.fetchone()
            conn.commit()

            user = User.from_row(row)
            return success_response(data=user.to_dict(), message="用户创建成功", code=201)

    except psycopg.errors.UniqueViolation:
        return error_response("邮箱已存在", 409)
    except Exception as e:
        return error_response(f"创建用户失败: {str(e)}", 500)


# ---------- Read All (分页) ----------
@user_bp.route('/users', methods=['GET'])
@validate_pagination
def get_users(page, page_size):
    """获取用户列表(分页)"""
    offset = (page - 1) * page_size

    with db_pool.connection() as conn:
        cursor = conn.cursor()

        # 查询总数
        cursor.execute("SELECT COUNT(*) FROM users")
        total = cursor.fetchone()[0]

        # 查询分页数据
        cursor.execute(
            """
            SELECT id, name, email, age, created_at, updated_at
            FROM users
            ORDER BY created_at DESC
            LIMIT %s OFFSET %s
            """,
            (page_size, offset)
        )
        rows = cursor.fetchall()

        users = [User.from_row(row).to_dict() for row in rows]

        return success_response(data={
            "users": users,
            "pagination": {
                "page": page,
                "page_size": page_size,
                "total": total,
                "total_pages": (total + page_size - 1) // page_size
            }
        })


# ---------- Read One ----------
@user_bp.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """获取单个用户"""
    with db_pool.connection() as conn:
        cursor = conn.cursor()
        cursor.execute(
            "SELECT id, name, email, age, created_at, updated_at FROM users WHERE id = %s",
            (user_id,)
        )
        row = cursor.fetchone()

        if not row:
            return error_response("用户不存在", 404)

        user = User.from_row(row)
        return success_response(data=user.to_dict())


# ---------- Update (PUT 全量更新) ----------
@user_bp.route('/users/<int:user_id>', methods=['PUT'])
@validate_json('name', 'email')
def update_user(user_id):
    """完整更新用户"""
    data = request.get_json()

    with db_pool.connection() as conn:
        cursor = conn.cursor()

        # 检查用户是否存在
        cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
        if not cursor.fetchone():
            return error_response("用户不存在", 404)

        # 更新用户
        cursor.execute(
            """
            UPDATE users
            SET name = %s, email = %s, age = %s, updated_at = CURRENT_TIMESTAMP
            WHERE id = %s
            RETURNING id, name, email, age, created_at, updated_at
            """,
            (data['name'], data['email'], data.get('age'), user_id)
        )
        row = cursor.fetchone()
        conn.commit()

        user = User.from_row(row)
        return success_response(data=user.to_dict(), message="用户更新成功")


# ---------- Update (PATCH 部分更新) ----------
@user_bp.route('/users/<int:user_id>', methods=['PATCH'])
def patch_user(user_id):
    """部分更新用户"""
    if not request.is_json:
        return error_response("请求必须是 JSON 格式", 400)

    data = request.get_json()
    if not data:
        return error_response("没有要更新的字段", 400)

    # 动态构建更新语句
    allowed_fields = ['name', 'email', 'age']
    update_fields = []
    values = []

    for field in allowed_fields:
        if field in data:
            update_fields.append(f"{field} = %s")
            values.append(data[field])

    if not update_fields:
        return error_response("没有有效的更新字段", 400)

    update_fields.append("updated_at = CURRENT_TIMESTAMP")
    values.append(user_id)

    with db_pool.connection() as conn:
        cursor = conn.cursor()

        # 检查用户是否存在
        cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
        if not cursor.fetchone():
            return error_response("用户不存在", 404)

        sql = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s RETURNING id, name, email, age, created_at, updated_at"
        cursor.execute(sql, values)
        row = cursor.fetchone()
        conn.commit()

        user = User.from_row(row)
        return success_response(data=user.to_dict(), message="用户更新成功")


# ---------- Delete ----------
@user_bp.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    """删除用户"""
    with db_pool.connection() as conn:
        cursor = conn.cursor()

        # 检查用户是否存在
        cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
        if not cursor.fetchone():
            return error_response("用户不存在", 404)

        cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
        conn.commit()

        return success_response(message="用户删除成功")

5.4 PUT vs PATCH 的区别

维度 PUT PATCH
语义 完整替换资源 部分更新字段
必填字段 所有字段都要传 只传要改的字段
不传字段 通常设为 null 或默认值 保持不变
示例 {"name": "新名字", "email": "新邮箱"} {"age": 25}

5.5 常见 HTTP 状态码

状态码 含义 使用场景
200 OK 查询成功、更新成功
201 Created 资源创建成功
204 No Content 删除成功(无返回体)
400 Bad Request 请求参数错误
404 Not Found 资源不存在
409 Conflict 资源冲突(如邮箱重复)
500 Internal Server Error 服务器内部错误

一句话总结:CRUD 的核心是语义正确------Create 用 POST 返回 201,Read 用 GET 返回数据,Update 用 PUT/PATCH,Delete 用 DELETE 并返回 204;参数验证前置,异常处理兜底。


6. 总结

通过这次梳理,我们完成了一个完整的 Flask + Psycopg3 + PostgreSQL 的 CRUD 流程:

  • Psycopg3 提供了现代的 PostgreSQL 连接方式,参数化查询防注入、RETURNING 子句获取新记录
  • 连接池 是 Web 应用性能的关键,避免频繁创建销毁连接的开销
  • Flask 蓝图 让路由模块化,RESTful 设计让接口语义清晰
  • 统一响应格式参数验证 是 API 可维护性的保障

如果用一句话总结:

构建一个可靠的 Web API,核心就三件事:数据库连接稳定(连接池)、SQL 执行安全(参数化查询)、接口设计规范(RESTful + 统一响应)。

当你能够独立完成从数据库设计到接口开发的完整流程时,Python Web 开发才算是真正入门了。


如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,你们的支持就是我坚持下去的动力!

相关推荐
2401_871492852 小时前
Vue.js监听器watch利用回调函数处理级联下拉框数据联动
jvm·数据库·python
FreakStudio2 小时前
亲测可用!可本地部署的 MicroPython 开源仿真器
python·单片机·嵌入式·面向对象·并行计算·电子diy·电子计算机
SilentSamsara3 小时前
Python 环境搭建完整指南:从下载安装到运行第一个程序
开发语言·python
zhoutongsheng3 小时前
C#怎么实现Swagger文档 C#如何在ASP.NET Core中集成Swagger自动生成API文档【框架】
jvm·数据库·python
.5484 小时前
## Sorting(排序算法)
python·算法·排序算法
ydmy4 小时前
注意力机制(个人理解)
pytorch·python·深度学习
iwhitney5 小时前
【次方量化】3分钟搞懂什么是量化策略
python
高洁016 小时前
大模型部署资源不足?轻量化部署解决方案
python·深度学习·机器学习·数据挖掘·transformer
阿里云大数据AI技术6 小时前
MaxFrame 视频帧智能分析:从视频到语义向量的端到端分布式处理
人工智能·python