SQLModel零基础教程(二)- 字段高级配置 & 数据校验,复用Pydantic能力

这里写目录标题

  • 前言
  • 一、阶段学习目标
  • [二、Field 双端高级配置(数据库约束 + Pydantic校验)](#二、Field 双端高级配置(数据库约束 + Pydantic校验))
    • [2.1 Field 参数两大分类](#2.1 Field 参数两大分类)
    • [2.2 default 与 default_factory 核心区别(高频踩坑点)](#2.2 default 与 default_factory 核心区别(高频踩坑点))
    • [2.3 高级数据库约束:联合索引、自定义列类型](#2.3 高级数据库约束:联合索引、自定义列类型)
  • 三、复用Pydantic专用校验类型(开箱即用)
    • [3.1 邮箱、链接、UUID、IP](#3.1 邮箱、链接、UUID、IP)
    • [3.2 Enum 枚举状态约束(角色、订单状态)](#3.2 Enum 枚举状态约束(角色、订单状态))
  • 四、标准分层DTO模型(企业开发规范,隔离数据库与接口)
    • [4.1 四层分层设计思路](#4.1 四层分层设计思路)
    • [4.2 完整用户分层实战代码](#4.2 完整用户分层实战代码)
    • [4.3 DTO转换与脱敏示例](#4.3 DTO转换与脱敏示例)
  • [五、复用Pydantic自定义校验 @field_validator](#五、复用Pydantic自定义校验 @field_validator)
    • [5.1 用户名自动去空格、小写清洗](#5.1 用户名自动去空格、小写清洗)
    • [5.2 密码复杂度自定义校验(大写+小写+数字)](#5.2 密码复杂度自定义校验(大写+小写+数字))
    • [5.3 手机号正则校验](#5.3 手机号正则校验)
  • 六、序列化精细化控制(敏感字段隐藏)
    • [6.1 Field(exclude=True) 永久不序列化](#6.1 Field(exclude=True) 永久不序列化)
    • [6.2 model_dump 动态过滤参数](#6.2 model_dump 动态过滤参数)
  • 七、阶段综合完整可运行案例
  • 八、阶段核心总结(半天必掌握)
  • 九、新手高频避坑指南

前言

上一篇我们学习了SQLModel基础概念、Engine、Session与单表CRUD,掌握了最基础的表定义与数据库操作。但真实业务里,单纯基础字段完全无法满足需求:

  • 字符串长度、数字区间、邮箱/URL格式需要强校验;
  • 枚举状态、联合索引、UUID主键、自动时间戳需要高级字段配置;
  • 新增、更新、接口返回需要分层模型,隔离敏感字段;
  • 自定义密码、手机号等复杂业务校验逻辑。

SQLModel底层完全融合Pydantic v2,所有Pydantic校验能力可直接复用,不用两套模型重复写校验规则。

一、阶段学习目标

  1. 精通Field双端配置:数据库列参数 + Pydantic校验参数;
  2. 掌握默认值区分:default静态值 / default_factory动态生成(时间、UUID、列表);
  3. 学会高级数据库约束:联合索引、自定义字段类型、非空/唯一;
  4. 复用Pydantic专用类型:EmailStrHttpUrlUUID、枚举Enum;
  5. 标准分层DTO设计:Base基础模型 / Create创建模型 / Update更新模型 / Public返回模型;
  6. 在SQLModel中使用@field_validator自定义业务校验;
  7. 掌握序列化脱敏、隐藏敏感字段(密码不返回前端)。

二、Field 双端高级配置(数据库约束 + Pydantic校验)

2.1 Field 参数两大分类

SQLModel的Field一套参数同时作用两层:

  1. SQL层参数 :控制数据库表结构
    primary_keyindexuniquenullablesa_columnsa_type
  2. Pydantic校验参数 :入参自动校验(和Pydantic完全通用)
    min_lengthmax_lengthgtgeltlepatterndescription

2.2 default 与 default_factory 核心区别(高频踩坑点)

  • default=xxx:静态常量,模块加载时只计算一次,禁止列表、字典、时间、UUID
  • default_factory=函数:每次实例化动态执行,可变类型、动态值必用。

示例:

python 复制代码
from sqlmodel import SQLModel, Field
from datetime import datetime
import uuid

class User(SQLModel, table=True):
    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key, description="UUID主键"
    )
    # 静态默认,固定布尔值
    is_active: bool = Field(default=True, nullable=False)
    # 动态生成创建时间,每次新建自动赋值当前UTC时间
    create_time: datetime = Field(default_factory=datetime.utcnow)
    # 错误写法:default=datetime.utcnow() 全局只会生成同一个时间

2.3 高级数据库约束:联合索引、自定义列类型

单字段索引直接index=True,多字段联合索引使用__table_args__定义索引元组:

python 复制代码
from sqlmodel import SQLModel, Field
from sqlalchemy import Index

class User(SQLModel, table=True):
    __tablename__ = "sys_user"
    # 联合索引:用户名+邮箱联合唯一
    __table_args__ = (
        Index("idx_username_email", "username", "email", unique=True),
    )
    id: int | None = Field(default=None, primary_key=True)
    username: str = Field(max_length=32, nullable=False)
    email: str = Field(max_length=128, nullable=False)
    age: int | None = Field(default=None, ge=0, le=120)

自定义数据库字段类型(长文本、大整数):

python 复制代码
from sqlalchemy import Text, BigInteger

class Article(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key, sa_type=BigInteger)
    content: str = Field(sa_column=Text(), description="文章长文本内容")

三、复用Pydantic专用校验类型(开箱即用)

SQLModel可直接导入使用Pydantic全部专用校验类型,无需手写正则,自动拦截非法格式。

3.1 邮箱、链接、UUID、IP

python 复制代码
from sqlmodel import SQLModel, Field
from pydantic import EmailStr, HttpUrl, UUID4, IPvAnyAddress
import uuid

class User(SQLModel, table=True):
    id: UUID4 = Field(default_factory=uuid.uuid4, primary_key=True)
    # 自动校验合法邮箱
    email: EmailStr = Field(unique=True, index=True, max_length=128)
    # 头像合法URL校验
    avatar: HttpUrl | None = Field(default=None)
    # 登录IP自动校验IPv4/IPv6
    login_ip: IPvAnyAddress | None = Field(default=None)

3.2 Enum 枚举状态约束(角色、订单状态)

使用str枚举,配合SQLEnum数据库原生枚举约束,同时Pydantic自动校验传值范围:

python 复制代码
from enum import Enum
from sqlmodel import SQLModel, Field
from sqlalchemy import SQLEnum

class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    username: str = Field(min_length=3, max_length=16)
    # 数据库枚举 + Pydantic值范围校验
    role: UserRole = Field(
        default=UserRole.GUEST,
        sa_column=SQLEnum(UserRole, name="user_role_enum")
    )

测试非法值会直接抛出ValidationError,无需手动if判断。

四、标准分层DTO模型(企业开发规范,隔离数据库与接口)

4.1 四层分层设计思路

  1. XxxBase:基础公共模型,存放所有通用字段,统一校验规则;
  2. XxxCreate:新增接口入参模型,不含主键、自动生成字段;
  3. XxxUpdate:更新接口模型,所有字段全部可选,支持局部修改;
  4. XxxPublic:接口返回模型,剔除密码等敏感字段,仅对外暴露安全数据;
  5. Xxx(table=True):数据库实体,继承Base,增加主键、数据库专属字段。

4.2 完整用户分层实战代码

python 复制代码
from sqlmodel import SQLModel, Field
from pydantic import EmailStr
from typing import Optional
import uuid

# 1. 基础公共模型(统一校验规则)
class UserBase(SQLModel):
    username: str = Field(min_length=3, max_length=16, description="用户名3-16位")
    email: EmailStr = Field(max_length=128)
    age: Optional[int] = Field(default=None, ge=0, le=120)

# 2. 创建入参模型:继承Base,新增密码字段
class UserCreate(UserBase):
    password: str = Field(min_length=8, max_length=20, description="密码8-20位")

# 3. 更新入参模型:全部字段可选,支持局部更新
class UserUpdate(SQLModel):
    username: Optional[str] = Field(None, min_length=3, max_length=16)
    email: Optional[EmailStr] = None
    age: Optional[int] = Field(None, ge=0, le=120)

# 4. 返回脱敏模型:隐藏密码,携带主键
class UserPublic(UserBase):
    id: uuid.UUID

# 5. 数据库实体:继承Base,映射数据表
class User(UserBase, SQLModel, table=True):
    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    password: str = Field(max_length=100, description="加密密码,不对外返回")
    create_time: str = Field(default_factory=lambda: str(uuid.uuid4()))

4.3 DTO转换与脱敏示例

python 复制代码
# 数据库实体转返回DTO,自动剔除password敏感字段
db_user = User(username="test", email="test@qq.com", password="Abc123456")
resp = UserPublic.model_validate(db_user)
print(resp.model_dump())  # 输出无password,安全返回前端

五、复用Pydantic自定义校验 @field_validator

SQLModel完全兼容Pydantic v2校验装饰器,单字段清洗、复杂业务规则直接复用,数据库实体与DTO模型均可使用。

5.1 用户名自动去空格、小写清洗

python 复制代码
from sqlmodel import SQLModel, Field
from pydantic import field_validator

class UserCreate(SQLModel):
    username: str = Field(min_length=3, max_length=16)
    email: str

    @field_validator("username", mode="before")
    def clean_username(cls, v):
        if isinstance(v, str):
            return v.strip().lower()
        return v

5.2 密码复杂度自定义校验(大写+小写+数字)

python 复制代码
import re
from sqlmodel import SQLModel, Field
from pydantic import field_validator, ValidationError

class UserCreate(SQLModel):
    password: str = Field(min_length=8, max_length=20)

    @field_validator("password", mode="after")
    def check_pwd_rule(cls, v):
        if not re.search(r"[A-Z]", v):
            raise ValueError("密码必须包含大写字母")
        if not re.search(r"[a-z]", v):
            raise ValueError("密码必须包含小写字母")
        if not re.search(r"\d", v):
            raise ValueError("密码必须包含数字")
        return v

# 测试校验
try:
    UserCreate(password="123456")
except ValidationError as e:
    print(e.errors()[0]["msg"])

5.3 手机号正则校验

python 复制代码
from sqlmodel import SQLModel, Field
from pydantic import field_validator
import re

class UserCreate(SQLModel):
    phone: str

    @field_validator("phone")
    def check_phone(cls, v):
        if not re.match(r"^1[3-9]\d{9}$", v):
            raise ValueError("请输入合法11位手机号")
        return v

六、序列化精细化控制(敏感字段隐藏)

6.1 Field(exclude=True) 永久不序列化

数据库密码字段,无论怎么转字典/JSON永远隐藏:

python 复制代码
class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    username: str
    # 序列化永久排除密码
    password: str = Field(exclude=True, max_length=100)

user = User(username="admin", password="123Abc666")
print(user.model_dump())  # 输出不含password

6.2 model_dump 动态过滤参数

python 复制代码
user = User(...)
# 过滤所有None空值
user.model_dump(exclude_none=True)
# 仅保留用户传入字段,过滤默认值
user.model_dump(exclude_unset=True)
# 临时手动排除指定字段
user.model_dump(exclude={"password"})

七、阶段综合完整可运行案例

整合高级字段、枚举、分层DTO、自定义校验、脱敏全部知识点:

python 复制代码
from sqlmodel import SQLModel, Field, create_engine, Session
from pydantic import EmailStr, field_validator, ValidationError
from enum import Enum
from sqlalchemy import SQLEnum
import re

# 1. 枚举定义
class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"

# 2. 基础模型
class UserBase(SQLModel):
    username: str = Field(min_length=3, max_length=16)
    email: EmailStr

# 3. 创建模型(带密码校验)
class UserCreate(UserBase):
    password: str = Field(min_length=8)
    role: UserRole = Field(default=UserRole.USER)

    @field_validator("password")
    def pwd_check(cls, v):
        if not re.search(r"[A-Za-z0-9]", v):
            raise ValueError("密码需包含字母数字")
        return v

# 4. 返回脱敏模型
class UserPublic(UserBase):
    id: int
    role: UserRole

# 5. 数据库实体
class User(UserBase, SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    password: str = Field(exclude=True)
    role: UserRole = Field(sa_column=SQLEnum(UserRole))

# 数据库初始化
engine = create_engine("sqlite:///stage2.db", echo=False)
SQLModel.metadata.create_all(bind=engine)

# 新增测试
if __name__ == "__main__":
    try:
        create_data = UserCreate(
            username="  Admin666  ",
            email="admin@test.com",
            password="Abc123456",
            role="admin"
        )
        with Session(engine) as session:
            db_user = User.model_validate(create_data)
            session.add(db_user)
            session.commit()
            session.refresh(db_user)
            # 脱敏返回
            resp = UserPublic.model_validate(db_user)
            print("接口返回数据:", resp.model_dump())
    except ValidationError as e:
        print("参数校验失败:", e.errors())

八、阶段核心总结(半天必掌握)

  1. Field同时承载数据库列配置Pydantic数据校验,一套代码两用;
  2. 动态默认值统一使用default_factory,避免可变类型全局复用问题;
  3. 联合索引通过__table_args__定义,枚举搭配SQLEnum实现数据库层约束;
  4. 直接复用Pydantic内置EmailStr/HttpUrl/UUID等专用校验类型;
  5. 四层分层DTO(Base/Create/Update/Public)是前后端项目标准规范,隔离敏感字段;
  6. SQLModel原生支持@field_validator,可复用全部Pydantic自定义校验逻辑;
  7. Field(exclude=True)永久隐藏密码等敏感数据,接口序列化自动脱敏。

九、新手高频避坑指南

  1. ❌ 可变类型(list/datetime/uuid)使用default,所有实例共享同一个值;
  2. ❌ 数据库实体直接返回前端,泄露password、密钥等敏感字段;
  3. ❌ 更新模型复用Base基类,字段不带Optional,导致必须传全量参数;
  4. ❌ 不用枚举,直接用字符串存储角色/状态,缺少入参校验;
  5. ✅ 所有接口分层定义DTO,数据库实体仅用于数据库操作;
  6. ✅ 复杂格式校验(密码、手机号)统一使用@field_validator,减少业务if判断。