schema-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Schema Builder

Convex 模式构建工具

Build well-structured Convex schemas following best practices for relationships, indexes, and validators.
遵循关系、索引和验证器的最佳实践,构建结构合理的Convex模式。

When to Use

使用场景

  • Creating a new
    convex/schema.ts
    file
  • Adding tables to existing schema
  • Designing data model relationships
  • Adding or optimizing indexes
  • Converting nested data to relational structure
  • 创建新的
    convex/schema.ts
    文件
  • 向现有模式中添加表
  • 设计数据模型关系
  • 添加或优化索引
  • 将嵌套数据转换为关系型结构

Schema Design Principles

模式设计原则

  1. Document-Relational: Use flat documents with ID references, not deep nesting
  2. Index Foreign Keys: Always index fields used in lookups (userId, teamId, etc.)
  3. Limit Arrays: Only use arrays for small, bounded collections (<8192 items)
  4. Type Safety: Use strict validators with
    v.*
    types
  1. 文档-关系型: 使用带有ID引用的扁平文档,而非深度嵌套
  2. 索引外键: 始终为用于查询的字段(如userId、teamId等)创建索引
  3. 限制数组使用: 仅将数组用于小型、有限的集合(少于8192个元素)
  4. 类型安全: 使用
    v.*
    类型的严格验证器

Schema Template

模式模板

typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  tableName: defineTable({
    // Required fields
    field: v.string(),

    // Optional fields
    optional: v.optional(v.number()),

    // Relations (use IDs)
    userId: v.id("users"),

    // Enums with union + literal
    status: v.union(
      v.literal("active"),
      v.literal("pending"),
      v.literal("archived")
    ),

    // Timestamps
    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    // Index for queries by this field
    .index("by_user", ["userId"])
    // Compound index for common query patterns
    .index("by_user_and_status", ["userId", "status"])
    // Index for time-based queries
    .index("by_created", ["createdAt"]),
});
typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  tableName: defineTable({
    // 必填字段
    field: v.string(),

    // 可选字段
    optional: v.optional(v.number()),

    // 关系(使用ID)
    userId: v.id("users"),

    // 枚举类型(联合+字面量)
    status: v.union(
      v.literal("active"),
      v.literal("pending"),
      v.literal("archived")
    ),

    // 时间戳
    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    // 按该字段查询的索引
    .index("by_user", ["userId"])
    // 针对常见查询模式的复合索引
    .index("by_user_and_status", ["userId", "status"])
    // 基于时间查询的索引
    .index("by_created", ["createdAt"]),
});

Common Patterns

常见模式

One-to-Many Relationship

一对多关系

typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
  }).index("by_email", ["email"]),

  posts: defineTable({
    userId: v.id("users"),
    title: v.string(),
    content: v.string(),
  }).index("by_user", ["userId"]),
});
typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
  }).index("by_email", ["email"]),

  posts: defineTable({
    userId: v.id("users"),
    title: v.string(),
    content: v.string(),
  }).index("by_user", ["userId"]),
});

Many-to-Many with Junction Table

多对多关系(使用关联表)

typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
  }),

  projects: defineTable({
    name: v.string(),
  }),

  projectMembers: defineTable({
    userId: v.id("users"),
    projectId: v.id("projects"),
    role: v.union(v.literal("owner"), v.literal("member")),
  })
    .index("by_user", ["userId"])
    .index("by_project", ["projectId"])
    .index("by_project_and_user", ["projectId", "userId"]),
});
typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
  }),

  projects: defineTable({
    name: v.string(),
  }),

  projectMembers: defineTable({
    userId: v.id("users"),
    projectId: v.id("projects"),
    role: v.union(v.literal("owner"), v.literal("member")),
  })
    .index("by_user", ["userId"])
    .index("by_project", ["projectId"])
    .index("by_project_and_user", ["projectId", "userId"]),
});

Hierarchical Data

层级数据

typescript
export default defineSchema({
  comments: defineTable({
    postId: v.id("posts"),
    parentId: v.optional(v.id("comments")), // null for top-level
    userId: v.id("users"),
    text: v.string(),
  })
    .index("by_post", ["postId"])
    .index("by_parent", ["parentId"]),
});
typescript
export default defineSchema({
  comments: defineTable({
    postId: v.id("posts"),
    parentId: v.optional(v.id("comments")), // 顶级评论为null
    userId: v.id("users"),
    text: v.string(),
  })
    .index("by_post", ["postId"])
    .index("by_parent", ["parentId"]),
});

Small Bounded Arrays (OK to use)

小型有限数组(允许使用)

typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
    // Small, bounded collections are fine
    roles: v.array(v.union(
      v.literal("admin"),
      v.literal("editor"),
      v.literal("viewer")
    )),
    tags: v.array(v.string()), // e.g., max 10 tags
  }),
});
typescript
export default defineSchema({
  users: defineTable({
    name: v.string(),
    // 小型、有限的集合是可行的
    roles: v.array(v.union(
      v.literal("admin"),
      v.literal("editor"),
      v.literal("viewer")
    )),
    tags: v.array(v.string()), // 例如:最多10个标签
  }),
});

Validator Reference

验证器参考

typescript
// Primitives
v.string()
v.number()
v.boolean()
v.null()
v.id("tableName")

// Optional
v.optional(v.string())

// Union types (enums)
v.union(v.literal("a"), v.literal("b"))

// Objects
v.object({
  key: v.string(),
  nested: v.number(),
})

// Arrays
v.array(v.string())

// Records (arbitrary keys)
v.record(v.string(), v.boolean())

// Any (avoid if possible)
v.any()
typescript
// 基本类型
v.string()
v.number()
v.boolean()
v.null()
v.id("tableName")

// 可选类型
v.optional(v.string())

// 联合类型(枚举)
v.union(v.literal("a"), v.literal("b"))

// 对象类型
v.object({
  key: v.string(),
  nested: v.number(),
})

// 数组类型
v.array(v.string())

// 记录类型(任意键)
v.record(v.string(), v.boolean())

// 任意类型(尽可能避免使用)
v.any()

Index Strategy

索引策略

  1. Single-field indexes: For simple lookups
    • by_user: ["userId"]
    • by_email: ["email"]
  2. Compound indexes: For filtered queries
    • by_user_and_status: ["userId", "status"]
    • by_team_and_created: ["teamId", "createdAt"]
  3. Remove redundant:
    by_a_and_b
    usually covers
    by_a
  1. 单字段索引: 用于简单查询
    • by_user: ["userId"]
    • by_email: ["email"]
  2. 复合索引: 用于带过滤条件的查询
    • by_user_and_status: ["userId", "status"]
    • by_team_and_created: ["teamId", "createdAt"]
  3. 移除冗余索引:
    by_a_and_b
    通常可以覆盖
    by_a
    的查询需求

Checklist

检查清单

  • All foreign keys have indexes
  • Common query patterns have compound indexes
  • Arrays are small and bounded (or converted to relations)
  • All fields have proper validators
  • Enums use
    v.union(v.literal(...))
    pattern
  • Timestamps use
    v.number()
    (milliseconds since epoch)
  • 所有外键都已创建索引
  • 常见查询模式都有对应的复合索引
  • 数组仅用于小型有限集合(或已转换为关系型结构)
  • 所有字段都配置了适当的验证器
  • 枚举类型使用
    v.union(v.literal(...))
    模式
  • 时间戳使用
    v.number()
    (纪元以来的毫秒数)

Migration from Nested to Relational

从嵌套结构迁移到关系型结构

If converting from nested structures:
Before:
typescript
users: defineTable({
  posts: v.array(v.object({
    title: v.string(),
    comments: v.array(v.object({ text: v.string() })),
  })),
})
After:
typescript
users: defineTable({
  name: v.string(),
}),

posts: defineTable({
  userId: v.id("users"),
  title: v.string(),
}).index("by_user", ["userId"]),

comments: defineTable({
  postId: v.id("posts"),
  text: v.string(),
}).index("by_post", ["postId"]),
如果需要从嵌套结构转换:
转换前:
typescript
users: defineTable({
  posts: v.array(v.object({
    title: v.string(),
    comments: v.array(v.object({ text: v.string() })),
  })),
})
转换后:
typescript
users: defineTable({
  name: v.string(),
}),

posts: defineTable({
  userId: v.id("users"),
  title: v.string(),
}).index("by_user", ["userId"]),

comments: defineTable({
  postId: v.id("posts"),
  text: v.string(),
}).index("by_post", ["postId"]),