3、PostgreSQL数据校验基础:数据完整性约束、主键与外键校验、唯一性校验、非空校验

在集成测试中,数据校验是确保缓存预热结果与数据库状态一致的核心环节。PostgreSQL 作为关系型数据库的典型代表,其数据完整性约束体系为自动化测试提供了天然的校验锚点。本章将系统讲解四大约束类型及其在测试脚本中的校验方法。

3.1 数据完整性约束概述

数据完整性约束是数据库表结构定义中用于保证数据准确性和一致性的规则集合。PostgreSQL 支持以下五类约束,其中前四类为本章重点:

约束类型 关键字 作用范围 违反时行为
非空约束 NOT NULL 单列 拒绝插入/更新 NULL
唯一约束 UNIQUE 单列或多列组合 拒绝重复值
主键约束 PRIMARY KEY 单列或多列组合 拒绝 NULL 和重复值
外键约束 FOREIGN KEY 跨表引用 拒绝无效引用或级联操作
检查约束 CHECK 单行表达式 拒绝不满足条件的行
测试视角:约束不仅是数据库设计规范,更是集成测试中数据校验的"黄金标准"。缓存预热后的数据若违反任何约束,测试应立即标记为失败。

3.2 主键与外键校验

主键(PRIMARY KEY)和外键(FOREIGN KEY)是关系型数据库中最核心的两种约束,它们共同维护着表内和表间的数据完整性。

3.2.1 主键校验

主键约束等价于 NOT NULL + UNIQUE。在集成测试中,校验主键需要验证两个维度:

  • 非空性:主键列不允许出现 NULL 值。
  • 唯一性:主键列的值在表内必须全局唯一。

以下 SQL 查询可用于测试主键约束是否被违反:

-- 检查主键列是否存在 NULL
SELECT COUNT(*) AS null_count
FROM users
WHERE user_id IS NULL;

-- 检查主键列是否存在重复值
SELECT user_id, COUNT(*) AS dup_count
FROM users
GROUP BY user_id
HAVING COUNT(*) > 1;

3.2.2 外键校验

外键约束确保子表中的引用值在父表的主键列中存在。在缓存预热场景中,外键校验常用于验证关联数据的完整性,例如订单表中的用户 ID 必须在用户表中存在。

外键校验的测试策略包括:

  1. 存在性校验:子表所有外键值在父表中均有对应记录。
  2. 级联操作校验:验证 ON DELETE CASCADE 或 ON UPDATE CASCADE 是否按预期执行。
  3. 引用完整性校验:验证不存在"悬挂引用"(orphan record)。
-- 查找悬挂引用:子表中有但父表中无的记录
SELECT o.order_id, o.user_id
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL;
常见陷阱:在集成测试中,如果缓存预热的数据来自多个表,务必按外键依赖顺序插入数据。先插入父表数据,再插入子表数据,否则会触发外键约束异常。
主键与外键校验流程 父表:users user_id (PK, NOT NULL) username (UNIQUE) email (NOT NULL) 子表:orders order_id (PK) user_id (FK → users) amount (CHECK > 0) 外键引用 步骤1:主键校验 检查 user_id 无 NULL 且无重复 SQL: GROUP BY HAVING COUNT > 1 步骤2:外键校验 检查 orders.user_id 在 users 中存在 SQL: LEFT JOIN WHERE NULL 校验通过 → 数据完整性确认

3.3 唯一性校验

唯一约束(UNIQUE)保证列或列组合中的数据不重复。与主键不同,唯一约束允许 NULL 值(在 PostgreSQL 中,多个 NULL 被视为不重复)。

在集成测试中,唯一性校验的典型场景包括:

  • 业务标识唯一性:如用户名、邮箱、订单编号等。
  • 组合唯一性:如同一用户下不能有重复的角色名称。
  • 缓存键唯一性:缓存预热后的数据在数据库中的唯一标识不应冲突。
-- 校验单列唯一性
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- 校验组合唯一性(例如:同一用户下角色名唯一)
SELECT user_id, role_name, COUNT(*) AS cnt
FROM user_roles
GROUP BY user_id, role_name
HAVING COUNT(*) > 1;
测试断言示例(Python + psycopg2):
def test_unique_email_constraint(cursor):
    cursor.execute("""
        SELECT email, COUNT(*)
        FROM users
        GROUP BY email
        HAVING COUNT(*) > 1
    """)
    duplicates = cursor.fetchall()
    assert len(duplicates) == 0, \
        f"发现重复邮箱: {duplicates}"

3.4 非空校验

非空约束(NOT NULL)是最基础的约束类型,确保列中不包含 NULL 值。在缓存预热测试中,非空校验常用于验证关键业务字段是否被正确填充。

非空校验的测试要点:

  1. 显式 NOT NULL 列:表结构中明确标记为 NOT NULL 的列。
  2. 隐式非空列:主键列、业务上不应为空的列(如订单金额)。
  3. 缓存预热后的空值检测:预热脚本是否遗漏了某些必填字段。
-- 批量检查多个 NOT NULL 列
SELECT COUNT(*) AS null_violations
FROM users
WHERE username IS NULL
   OR email IS NULL
   OR created_at IS NULL;

-- 按列统计 NULL 数量
SELECT
    SUM(CASE WHEN username IS NULL THEN 1 ELSE 0 END) AS username_null,
    SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) AS email_null,
    SUM(CASE WHEN created_at IS NULL THEN 1 ELSE 0 END) AS created_at_null
FROM users;
唯一性校验 vs 非空校验 唯一性校验 (UNIQUE) 允许 NULL(多个 NULL 不冲突) 不允许重复的非 NULL 值 可作用于单列或多列组合 自动创建 B-tree 索引 测试重点:GROUP BY HAVING 缓存键不应重复 非空校验 (NOT NULL) 完全禁止 NULL 值 插入/更新 NULL 时直接报错 只能作用于单列 不自动创建索引 测试重点:IS NULL 查询 预热数据不应遗漏必填字段 两者结合使用:主键 = NOT NULL + UNIQUE,是数据完整性的基石

3.5 约束校验的自动化测试实现

在 Python 自动化测试框架中,可以将上述校验逻辑封装为可复用的校验函数。以下是一个完整的校验类示例:

import psycopg2
from typing import List, Tuple

class DataIntegrityValidator:
    """PostgreSQL 数据完整性校验器"""

    def __init__(self, conn_string: str):
        self.conn = psycopg2.connect(conn_string)

    def check_not_null(self, table: str, columns: List[str]) -> List[Tuple]:
        """检查指定列是否存在 NULL 值"""
        conditions = " OR ".join([f"{col} IS NULL" for col in columns])
        query = f"SELECT * FROM {table} WHERE {conditions} LIMIT 10"
        with self.conn.cursor() as cur:
            cur.execute(query)
            return cur.fetchall()

    def check_unique(self, table: str, columns: List[str]) -> List[Tuple]:
        """检查指定列或列组合是否存在重复值"""
        cols = ", ".join(columns)
        query = f"""
            SELECT {cols}, COUNT(*) as cnt
            FROM {table}
            GROUP BY {cols}
            HAVING COUNT(*) > 1
        """
        with self.conn.cursor() as cur:
            cur.execute(query)
            return cur.fetchall()

    def check_foreign_key(self, child_table: str, fk_column: str,
                          parent_table: str, pk_column: str) -> List[Tuple]:
        """检查外键引用完整性"""
        query = f"""
            SELECT c.{fk_column}
            FROM {child_table} c
            LEFT JOIN {parent_table} p
                ON c.{fk_column} = p.{pk_column}
            WHERE p.{pk_column} IS NULL
        """
        with self.conn.cursor() as cur:
            cur.execute(query)
            return cur.fetchall()

    def close(self):
        self.conn.close()
集成测试最佳实践:在缓存预热完成后,依次执行非空校验 → 唯一性校验 → 主键校验 → 外键校验。任何一步发现违规数据,都应立即终止测试并输出详细的违规记录,包括表名、列名和具体数据行。

3.6 本章小结

PostgreSQL 的数据完整性约束为集成测试提供了四层校验防线:

  • 非空校验确保关键字段不被遗漏,是数据完整性的第一道防线。
  • 唯一性校验防止业务标识冲突,保障缓存键的唯一性。
  • 主键校验结合非空与唯一性,是表内数据完整性的核心。
  • 外键校验维护跨表引用关系,防止数据孤岛。