Drizzle ORM Comprehensive Course – Sakura Dev
播放列表:https://www.youtube.com/playlist?list=PLhnVDNT5zYN8PLdYddaU3jiZXeOyehhoU
总集数:10 集(2023-2024 年上传,适用于 2026 年 Drizzle 版本,核心概念兼容,但建议检查最新 docs 如 relations v2 更新)
整体风格:逐步构建从零到高级的 Drizzle ORM 项目,使用 PostgreSQL(部分集用 SQLite),代码密集型教学。配套 GitHub repo:https://github.com/vahid-nejad/drizzle-orm-course(包含完整代码示例,可 clone 跟练)。
核心重点:PostgreSQL 配置、schema 定义、查询过滤、关系建模(1:1、1:N、N:N)、NestJS 集成、数据插入与种子填充。强调类型安全、接近原生 SQL 的设计哲学。
这个视频演示了在 Node.js 应用(具体是 Next.js v13 项目,但适用于任何 Node.js 框架)中使用 PostgreSQL 初始设置 Drizzle ORM。重点是使用 Docker 配置 PostgreSQL 数据库、定义 schema、生成迁移并将其应用到数据库。
在项目根目录创建 docker-compose.yml 文件,定义两个服务:
localhost:8080 访问。设置环境变量:
POSTGRES_DB=testdbPOSTGRES_USER=postgresPOSTGRES_PASSWORD=postgres使用 docker compose up 在终端运行 Docker Compose 文件,或通过 VS Code Docker 扩展运行。
启动后,PostgreSQL 数据库(testdb)可通过管理界面在 localhost:8080 使用凭证 postgres/postgres 访问。
安装 Drizzle ORM 以及 PostgreSQL 驱动程序 (pg) 和 Drizzle Kit:
xxxxxxxxxx21npm install drizzle-orm pg drizzle-kit2npm install --save-dev @types/pg安装遵循 Node.js 和 PostgreSQL 的 Drizzle ORM 文档,使用 node-postgres (pg)。
在 src/db/ 中创建 schema.ts 文件,使用 TypeScript 定义数据库表。
示例:定义 users 表:
xxxxxxxxxx71import { pgTable, serial, text, varchar } from 'drizzle-orm/pg-core';23export const users = pgTable('users', {4 id: serial('id').primaryKey(),5 fullName: text('full_name'),6 phone: varchar('phone', { length: 256 }),7});pgTable 创建名为 'users' 的表。serial 创建自增整数 (id),设置为主键。text 和 varchar 定义字符串字段,可选长度约束。Schema 必须导出,以便 Drizzle Kit 检测更改。
在 package.json 中添加脚本生成迁移文件:
xxxxxxxxxx11"migration:generate": "drizzle-kit generate:pg --schema=./src/db/schema.ts"运行 npm run migration:generate:
schema.ts 中的更改。drizzle 文件夹中创建 SQL 迁移文件(例如 drizzle/0000_create_users_table.sql)。users 表的 CREATE TABLE 语句,包含 id、full_name 和 phone 列。在 src/db/ 中创建 migrate.ts 文件应用迁移:
xxxxxxxxxx221import { Pool } from 'pg';2import { drizzle } from 'drizzle-orm/node-postgres';3import { migrate } from 'drizzle-orm/node-postgres/migrator';4import './env/config'; // 加载环境变量56const pool = new Pool({7 connectionString: process.env.DATABASE_URL,8});910const db = drizzle(pool);1112const main = async () => {13 console.log('Migration started');14 await migrate(db, { migrationsFolder: 'drizzle' });15 console.log('Migration ended');16 process.exit(0);17};1819main().catch((err) => {20 console.error(err);21 process.exit(1);22});在 .env 文件中定义 DATABASE_URL:
xxxxxxxxxx11DATABASE_URL=postgres://postgres:postgres@localhost:5432/testdb?schema=public
在 package.json 中添加脚本运行迁移:
xxxxxxxxxx11"migration:push": "node -r esbuild-register src/db/migrate.ts"运行 npm run migration:push 执行迁移,在 PostgreSQL 数据库中创建 users 表。
创建组合脚本以方便:
xxxxxxxxxx11"migrate": "npm run migration:generate && npm run migration:push"进行 schema 更改(例如添加 address 字段),运行 npm run migrate:
users 表(例如 ADD COLUMN address VARCHAR(256))。drizzle 文件夹存储迁移文件,这些文件被版本化和顺序应用。此过程为使用 Drizzle ORM 与 PostgreSQL 建立了坚实基础,在 Node.js 应用中启用类型安全的数据库交互。
视频解释了 PostgreSQL 中的 SQL 数据类型及其在 Drizzle ORM 中的用法,强调它们在定义表字段的重要性。将类型分类为数字、布尔、文本、JSON、日期/时间、枚举和二进制,并提供详细解释和代码示例。
Integer:有符号 4 字节数字(32 位),1 位用于符号,31 位用于值,范围从 -2³¹ 到 2³¹-1(约 -20 亿到 20 亿)。在 Drizzle ORM 中,使用从 drizzle-orm/pg-core 导入的 integer('q2y')。
SmallInt:有符号 2 字节数字(8 位),1 位用于符号,7 位用于值,范围从 -2⁷ 到 2⁷-1(约 -32,000 到 32,000)。使用 smallint('fieldName')。
BigInt:有符号 8 字节数字(64 位),1 位用于符号,63 位用于值,范围从 -2⁶³ 到 2⁶³-1。使用 bigint('q2i', { mode: 'number' }) 或 { mode: 'bigint' },取决于数字大小。
Serial 类型:自增整数,适合 ID。变体包括:
serial(基于 integer,别名为 serial4)smallSerial(基于 smallint,别名为 serial2)bigSerial(基于 bigint,别名为 serial8)
使用 serial('id').primaryKey() 用于自增 ID。对于 bigSerial,指定 { mode: 'number' } 或 { mode: 'bigint' }。Decimal/Numeric:用于精确小数算术的定点数字。使用 decimal('price', { precision: 7, scale: 2 }),其中 precision 是总位数,scale 是小数位。PostgreSQL 中 numeric 等同于 decimal。
real('score')。doublePrecision('field')。true 或 false(或 1/0)。使用 boolean('delivered')。text('description')。varchar('description', { length: 256 }) 或 varchar('description', 256)。char('name', { length: 10 })。例如,存储 "chair"(5 个字符)结果为 "chair "(总共 10 个字符)。json('data')。数据存储为 JSON 字符串。jsonb('data')。插入时转换为二进制,但处理更快。time('start_at', { withTimezone: true, precision: 3 })。默认可为 defaultNow()。精度控制小数秒(例如毫秒)。timestamp('date', { mode: 'date', withTimezone: true })。mode 可为 'string' 或 'date'。默认可为 defaultNow()。date('date')。interval('duration')。不能使用 defaultNow()。pgEnum('mood', ['sad', 'ok', 'happy']) 定义,然后在表中使用:mood('mood')。值限制为定义列表。在 Drizzle ORM(PostgreSQL 驱动 drizzle-orm/pg-core)中,数组类型 和 对象数组(即数组里每个元素是对象)的定义方式不同。PostgreSQL 原生支持数组,但不支持“数组里直接放复杂对象”,所以我们通常用两种方案来实现:
PostgreSQL 原生支持 text[]、integer[]、uuid[] 等,使用 .array() 方法即可。
xxxxxxxxxx171import { pgTable, text, integer, uuid, serial } from 'drizzle-orm/pg-core';23export const users = pgTable('users', {4 id: serial('id').primaryKey(),56 // 字符串数组(标签、角色等) - 最常用7 tags: text('tags').array().default([]), // text[]89 // 数字数组(分数列表、ID列表)10 scores: integer('scores').array().default([]),1112 // UUID 数组(关联多个用户ID)13 friends: uuid('friends').array().default([]),1415 // 空数组默认值(推荐用 sql 操作符,避免空数组问题)16 hobbies: text('hobbies').array().notNull().default(sql`'{}'::text[]`),17});常用附加配置(和普通列一样):
xxxxxxxxxx41tags: text('tags').array()2 .notNull() // 非空(但数组元素可以是 null)3 .default(sql`'{}'::text[]`) // 空数组默认值(PostgreSQL 语法)4 .$type<string[]>() // 类型推断(可选,增强 TS 提示)查询示例(使用数组操作符):
xxxxxxxxxx71import { arrayContains, arrayContained } from 'drizzle-orm';23await db.select().from(users)4 .where(arrayContains(users.tags, ['typescript', 'drizzle'])); // 包含这些标签56await db.select().from(users)7 .where(arrayContained(['react', 'nextjs'], users.tags)); // 数组被这些标签包含注意:PostgreSQL 数组允许元素为 null(如 {1, null, 3}),所以即使 .notNull(),类型推断仍可能是 (T | null)[]。可以用 .$type<T[]>() 强制。
PostgreSQL 没有原生“对象数组”类型,但 jsonb 非常强大、性能优秀、查询方便,是存放对象数组的最佳实践(几乎所有现代项目都这么做)。
xxxxxxxxxx141import { pgTable, jsonb, serial } from 'drizzle-orm/pg-core';2import { InferSelectModel } from 'drizzle-orm';34export const products = pgTable('products', {5 id: serial('id').primaryKey(),67 // 对象数组:用 jsonb 存储8 variants: jsonb('variants')9 .$type<Array<{ name: string; price: number; stock: number; size?: string }>>()10 .default([]),11});1213// 类型推断(可选)14export type Product = InferSelectModel<typeof products>;完整示例(带默认空数组 + 类型安全):
xxxxxxxxxx41variants: jsonb('variants')2 .$type<{ name: string; price: number; stock: number; attributes?: Record<string, string> }[]>() // 对象数组3 .notNull()4 .default([]), // 空数组默认值插入/更新示例:
xxxxxxxxxx131await db.insert(products).values({2 variants: [3 { name: 'Red - M', price: 99.99, stock: 50 },4 { name: 'Blue - L', price: 109.99, stock: 30 }5 ]6});78// 更新:追加一个新对象9await db.update(products)10 .set({11 variants: sql`${products.variants} || ${JSON.stringify(newVariant)}::jsonb`12 })13 .where(eq(products.id, productId));查询示例(强大!):
xxxxxxxxxx91// 查找有 "Red" 变体的产品2await db.select().from(products)3 .where(sql`jsonb_path_exists(${products.variants}, '$[*] ? (@.name == "Red")'`);45// 查找价格 > 100 的变体数量6await db.select({7 id: products.id,8 expensiveVariants: sql<number>`json_array_length(jsonb_path_query_array(${products.variants}, '$[*] ? (@.price > 100)'))`9}).from(products);为什么 jsonb 是对象数组的最佳选择?
->, ->> , @> 等).$type<T[]>() 提供编译时保护总结对比表(2026 年最常用方案):
| 需求 | 推荐类型 | Drizzle 定义示例 | 优点 | 缺点 |
|---|---|---|---|---|
| 简单数组(标签、ID) | text[] / integer[] | text('tags').array() | 原生、查询快 | 只能存基本类型 |
| 对象数组 | jsonb | jsonb('variants').$type<T[]>() | 灵活、支持复杂嵌套、查询强大 | 稍微多一点 overhead |
| 超严格对象数组 | 不用 | (不推荐) | - | PostgreSQL 无此类型 |
结论:
.array() 需要完整 schema 示例、查询/插入代码、或 jsonb 索引优化?随时说!🚀
.defaultNow() 或 .default('value') 设置默认值(例如 serial('id').primaryKey().defaultNow())。.notNull() 防止 null 值。.primaryKey() 定义主键(例如 serial('id').primaryKey())。视频在 test 表中使用这些类型演示,使用 Drizzle ORM 与 PostgreSQL,展示如何创建表、运行迁移,并在 Adminer 中验证结果。强调理解这些类型以有效使用 Drizzle ORM。
在 Drizzle ORM 中定义 PostgreSQL 表时(使用 drizzle-orm/pg-core),最常用的数据类型如下表所示。这些是真实项目中出现频率最高的类型(2026 年最新推荐实践,优先使用 identity 而非旧 serial)。
| 类别 | 函数名 (Drizzle) | PostgreSQL 类型 | 常见用途 | 备注 / 推荐配置示例 |
|---|---|---|---|---|
| 数值 - 整数 | integer() | integer / int / int4 | 普通整数(如年龄、计数) | integer('age') |
smallint() | smallint / int2 | 小范围整数(如状态码 0~100) | smallint('status') | |
bigint() | bigint / int8 | 大整数(如雪花ID、时间戳) | bigint('bigId', { mode: 'number' }) 或 'bigint' | |
| 自增主键 | integer().generatedAlwaysAsIdentity() | identity (推荐 2025+) | 现代标准主键(取代 serial) | integer('id').primaryKey().generatedAlwaysAsIdentity() |
serial() | serial / serial4 | 传统自增主键(仍兼容,但官方推荐 identity) | serial('id').primaryKey() | |
bigserial() | bigserial / serial8 | 大范围自增主键 | bigserial('id', { mode: 'number' }).primaryKey() | |
| 精确小数 | numeric({ precision, scale }) | numeric / decimal | 金额、汇率等需要精确小数 | numeric('price', { precision: 12, scale: 2 }) |
| 浮点数 | real() | real / float4 | 科学计算、近似浮点 | real('temperature') |
doublePrecision() | double precision / float8 | 高精度浮点 | doublePrecision('score') | |
| 布尔 | boolean() | boolean | 开关、是否已删除、激活状态 | boolean('isActive').default(false) |
| 字符串 | text() | text | 任意长度文本(如文章内容、描述) | text('content') 或 text('status', { enum: ['active', 'inactive'] }) |
varchar({ length }) | varchar(n) | 有长度限制的字符串(如用户名、邮箱) | varchar('email', { length: 255 }).unique() | |
| JSON | jsonb() | jsonb | 结构化数据、配置、扩展字段(强烈推荐) | jsonb('metadata').$type<Record<string, any>>() |
json() | json | 简单 JSON(不推荐,性能差) | json('data') | |
| 时间日期 | timestamp({ withTimezone?, precision? }) | timestamp / timestamptz | 创建/更新时间(最常用) | timestamp('createdAt').defaultNow() |
date() | date | 纯日期(如生日) | date('birthday') | |
time() | time / timetz | 纯时间(如营业时间) | time('openTime', { withTimezone: true }) | |
| UUID | uuid() | uuid | 分布式唯一ID(非常流行) | uuid('id').primaryKey().defaultRandom() |
| 枚举 | pgEnum('name', values) + enum() | custom enum | 固定状态集合(如订单状态) | const statusEnum = pgEnum('status', ['pending','paid']); statusEnum('status') |
| 二进制 | bytea() | bytea | 图片、文件等二进制数据 | bytea('avatar') |
xxxxxxxxxx131import { pgTable, integer, text, varchar, timestamp, jsonb, uuid, pgEnum } from 'drizzle-orm/pg-core';23export const users = pgTable('users', {4 id: uuid('id').primaryKey().defaultRandom(), // 现代推荐 UUID5 // id: integer('id').primaryKey().generatedAlwaysAsIdentity(), // 或用 identity 自增6 email: varchar('email', { length: 255 }).notNull().unique(),7 name: text('name').notNull(),8 role: pgEnum('user_role', ['user', 'admin', 'moderator'])('role').default('user'),9 metadata: jsonb('metadata').$type<Record<string, any>>().default({}),10 createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),11 updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),12 isActive: boolean('is_active').default(true),13});这些类型覆盖了 95%+ 的真实项目需求。
如果需要更小众的(如几何类型 point、array、interval 等),可以再告诉我,我继续补充!🚀
在 Drizzle ORM 的 PostgreSQL 列定义(使用 drizzle-orm/pg-core)中,常用的附加配置(即链式方法)非常丰富。这些方法用于添加约束、默认值、索引暗示等,是日常开发中最常使用的。
以下是最常用、最实用的附加配置表格,按使用频率和重要性排序:
| 配置方法 | 语法示例 | 作用描述 | 使用频率 | 备注 / 常见组合示例 |
|---|---|---|---|---|
.notNull() | varchar('email', { length: 255 }).notNull() | 强制列不允许 NULL 值(NOT NULL 约束) | ★★★★★ | 最常用,几乎所有非可选字段都会加 |
.default(value) | boolean('isActive').default(false)timestamp('createdAt').defaultNow() | 设置默认值(静态值或 defaultNow()) | ★★★★★ | 时间戳常用 defaultNow(),布尔常用 false/true |
.defaultRandom() | uuid('id').primaryKey().defaultRandom() | 生成随机 UUID(gen_random_uuid()) | ★★★★ | 现代项目 UUID 主键首选 |
.primaryKey() | integer('id').primaryKey()或 generatedAlwaysAsIdentity() | 将列设为主键(PRIMARY KEY) | ★★★★★ | 几乎所有表的主键都会加 |
.unique() | varchar('email').unique()或 unique('custom_name') | 添加唯一约束(UNIQUE) | ★★★★ | 邮箱、用户名、slug 等字段必加 |
.references(() => otherTable.column) | integer('userId').references(() => users.id) | 定义外键引用(FOREIGN KEY),支持 onDelete/onUpdate | ★★★★ | 关系表必备,可选加 .onDelete('cascade') |
.generatedAlwaysAsIdentity({ ... }) | integer('id').primaryKey().generatedAlwaysAsIdentity() | 现代自增主键(identity,2025+ 推荐,取代 serial) | ★★★★ | 生产级项目新标准,可指定 startWith/increment 等 |
$onUpdateFn(() => new Date()) | timestamp('updatedAt').$onUpdateFn(() => new Date()) | 更新时自动刷新值(PostgreSQL trigger 方式) | ★★★ | updatedAt 字段常用,与 defaultNow() 搭配 |
.generatedAlwaysAs(sqlexpression) | text('fullName').generatedAlwaysAs(sql{table.lastName}) | 生成列(computed column),存储或虚拟 | ★★ | 少用,但适合派生字段(如 full_name = first + last) |
.check(sqlcondition) (表级) | (table) => [check('age_check', sql${table.age} > 18)] | CHECK 约束(表级定义) | ★★ | 年龄、状态等业务规则校验 |
.default(sqlexpression) | integer('randomNum').default(sqlfloor(random() * 100)::int) | 使用 SQL 表达式作为默认值 | ★★ | 随机数、当前用户 ID 等 |
xxxxxxxxxx351import { pgTable, integer, uuid, text, varchar, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';2import { sql } from 'drizzle-orm';34export const userRole = pgEnum('user_role', ['user', 'admin']);56export const users = pgTable('users', {7 id: uuid('id')8 .primaryKey()9 .defaultRandom(), // 现代 UUID 主键 + 随机默认1011 email: varchar('email', { length: 255 })12 .notNull()13 .unique(), // 唯一 + 非空1415 name: text('name').notNull(),1617 role: userRole('role')18 .default('user'),1920 isActive: boolean('is_active')21 .notNull()22 .default(true),2324 createdAt: timestamp('created_at', { withTimezone: true })25 .notNull()26 .defaultNow(),2728 updatedAt: timestamp('updated_at', { withTimezone: true })29 .notNull()30 .defaultNow()31 .$onUpdateFn(() => new Date()), // 自动更新3233 // 自增备选(如果不喜欢 UUID)34 // id: integer('id').primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }),35});这些配置基本覆盖了 98% 的真实场景。
.notNull() + .default() + .primaryKey() + .unique() .references() defaultNow() + $onUpdateFn 需要更详细的某个配置示例(如外键的 onDelete 选项)或完整 schema 模板,再告诉我!🚀
视频焦点是使用 Drizzle ORM 编写第一个查询。通过设置全局数据库客户端处理整个应用的 CRUD 操作,避免为每个查询创建新 Drizzle 实例。
要创建全局 DB 客户端,在 src/db 目录中创建新文件 index.ts。首先,使用 pg 库实例化 PostgreSQL 客户端。客户端使用从环境变量 (process.env.DATABASE_URL) 获取的连接字符串配置。初始化客户端后,调用 connect() 方法建立连接。
接下来,使用从 drizzle-orm/node-postgres 导入的 drizzle 函数创建并导出 Drizzle 客户端。此函数需要两个参数:PostgreSQL 客户端实例和 schema。从 schema.ts 文件导入 schema(别名为 schema)并作为第二个参数传递给 drizzle 函数。
设置全局 DB 客户端后,可以在应用任何地方导入并使用它进行数据库操作。例如,在 Next.js 页面(具体是异步服务器组件)中,从 /db 导入 DB 客户端。编写一个简单查询从 users 表获取所有用户:db.select().from(users),其中 db 指导出的 Drizzle 客户端,schema.users 引用 schema 中定义的 users 表。然后使用 JSON.stringify(results) 在页面上渲染结果。
要测试查询,必须运行 PostgreSQL Docker 容器(通过 Docker Compose)。执行时,查询从 users 表检索所有记录。在示例中,数据库中有两个虚拟用户 ("John Doe" 和 "Steve Doe"),查询成功返回两个记录。
这演示了使用 Drizzle ORM 编写简单 select 查询的基本语法,强调使用全局 DB 客户端和适当 schema 集成以实现类型安全和易用性。
视频涵盖 Drizzle ORM 中的所有查询过滤器,演示如何在包含 id、fullName、phone、address 和 score 列的 users 表上应用各种条件。解释包括每个过滤器类型的关键概念、代码示例和逐步过程。
schema.ts 中定义 users 表,包含 score 列(整数,范围 0–100)。app/api/user/route.ts 创建 API 端点,使用异步 GET 函数处理查询。db.select().from(users),其中 users 从 schema 导入。EQ (Equals):过滤列等于值的用户。
xxxxxxxxxx11db.select().from(users).where(eq(users.id, 1))返回 id = 1 的用户。
NE (Not Equal):过滤列不等于值的用户。
xxxxxxxxxx11db.select().from(users).where(ne(users.id, 2))返回除 id = 2 外的所有用户。
GT (Greater Than):过滤数字列大于值的用户。
xxxxxxxxxx11db.select().from(users).where(gt(users.score, 50))返回 score > 50 的用户。
GTE (Greater Than or Equal):过滤列大于或等于值的用户。
xxxxxxxxxx11db.select().from(users).where(gte(users.score, 60))返回 score >= 60 的用户。
LT (Less Than):过滤列小于值的用户。
xxxxxxxxxx11db.select().from(users).where(lt(users.score, 60))返回 score < 60 的用户。
LTE (Less Than or Equal):过滤列小于或等于值的用户。
xxxxxxxxxx11db.select().from(users).where(lte(users.score, 60))返回 score <= 60 的用户。
isNull:过滤列为 NULL 的用户。
xxxxxxxxxx11db.select().from(users).where(isNull(users.address))返回 address IS NULL 的用户。注意:如果数据库存储空字符串而不是 NULL,结果为空数组。在数据库中手动设置值为 NULL(例如通过 Adminer)确保正确行为。
isNotNull:过滤列不为 NULL 的用户。
xxxxxxxxxx11db.select().from(users).where(isNotNull(users.address))返回除指定列中 NULL 外的所有用户。
inArray:过滤列值在指定列表中的用户。
xxxxxxxxxx11db.select().from(users).where(inArray(users.id, [1, 2, 4]))返回 id 在 [1, 2, 4] 中的用户。
对于非连续值(例如分数):
xxxxxxxxxx11db.select().from(users).where(inArray(users.score, [60, 90]))返回 score 精确为 60 或 90 的用户(不是范围)。
notInArray:过滤列值不在指定列表中的用户。
xxxxxxxxxx11db.select().from(users).where(notInArray(users.score, [60, 90]))返回 score 既不是 60 也不是 90 的用户。
between:过滤列值在两个值之间(包含)的用户。
xxxxxxxxxx11db.select().from(users).where(between(users.score, 60, 90))返回 score 在 60 和 90 之间(包含)的用户。
notBetween:过滤列值不在两个值之间的用户。
xxxxxxxxxx11db.select().from(users).where(notBetween(users.score, 20, 90))返回 score 在 20 到 90 范围外的用户。
like:基于 SQL LIKE 模式匹配过滤用户(区分大小写)。
xxxxxxxxxx11db.select().from(users).where(like(users.fullName, '%ob%'))返回 fullName 包含 "ob" 的用户。
使用 % 作为通配符:
'%ob%':包含 "ob"。'Robin%':以 "Robin" 开头。'%work':以 "work" 结尾。示例:查找名称以 "work" 结尾的用户:
xxxxxxxxxx11db.select().from(users).where(like(users.fullName, '%work'))ilike:like 的不区分大小写版本。
xxxxxxxxxx11db.select().from(users).where(ilike(users.fullName, '%allen%'))返回 fullName 包含 "allen" 的用户,不区分大小写。
notLike:like 的相反(区分大小写)。
xxxxxxxxxx11db.select().from(users).where(notLike(users.fullName, 'Alan%'))返回 fullName 不以 "Alan" 开头的用户。
notIlike:notLike 的不区分大小写版本。
xxxxxxxxxx11db.select().from(users).where(notIlike(users.fullName, 'alan%'))返回 fullName 不以 "alan" 开头的用户(不区分大小写)。
not:否定条件。
xxxxxxxxxx11db.select().from(users).where(not(eq(users.id, 1)))返回除 id = 1 外的所有用户。
可与其他运算符组合:
xxxxxxxxxx11db.select().from(users).where(not(like(users.fullName, 'Alan%')))返回 fullName 不以 "Alan" 开头的用户。
and:组合多个条件(所有必须为真)。
xxxxxxxxxx11db.select().from(users).where(and(like(users.fullName, '%a%'), gt(users.score, 50)))返回 fullName 包含 "a" 且 score > 50 的用户。
or:组合条件(任何一个为真)。
xxxxxxxxxx11db.select().from(users).where(or(like(users.fullName, 'Alan%'), gt(users.score, 90)))返回 fullName 以 "Alan" 开头 或 score > 90 的用户。
可嵌套用于复杂逻辑:
xxxxxxxxxx21db.select().from(users)2 .where(and(eq(users.id, 60), or(like(users.fullName, 'Alan%'), gt(users.score, 90))))如果 fullName 以 "Alan" 开头 或 score > 90,返回 id = 60 的用户。
eq、ne、gt、gte、lt、lte、isNull、isNotNull、inArray、notInArray、between、notBetween、like、ilike、notLike、notIlike、not、and、or) 从 drizzle-orm 导入。localhost:3000/api/users 发送请求。new Response(JSON.stringify(result)) 返回。id 列是主键,因此 id 上的平等过滤器返回最多一行。ilike。inArray 和 notInArray 匹配精确值,不是范围。这涵盖了视频中演示的 Drizzle ORM 中的所有查询过滤器。
视频解释了使用 Drizzle ORM 在两个表——users 和 profiles——之间创建一对一关系的过程。关键概念是每个用户可以有一个精确的配置文件,每个配置文件属于一个用户,建立双向一对一关系。
在现有 users 表旁边创建 profiles 表。使用 Drizzle ORM 的 pgTable 函数定义 schema。
xxxxxxxxxx51export const profiles = pgTable('profiles', {2 id: serial('id').primaryKey(),3 bio: varchar('bio', { length: 256 }),4 userId: integer('user_id').notNull().references(() => users.id),5});id:serial 类型(自增整数),设置为主键。
bio:varchar 字段,最大长度 256 个字符。
userId:引用 users 表的 id 列的外键。
integer(匹配 PostgreSQL 中 serial 的底层类型)。.notNull() 确保每个配置文件必须链接到用户。.references(() => users.id) 建立关系。注意:外键 (userId) 放置在 profiles 表中,引用 users.id 主键。这是一个设计选择——任何一方都可以持有外键,但需要一致性。
定义 schema 后,执行迁移在数据库中创建 profiles 表:
xxxxxxxxxx11npm run migrate这生成 profiles 表,包含列:id、bio 和 user_id(带有到 users.id 的外键约束)。
为了启用带有相关数据的类型安全查询,必须使用 Drizzle ORM 的 relations 函数显式定义关系。
xxxxxxxxxx81import { relations } from 'drizzle-orm';23export const userRelations = relations(users, ({ one }) => ({4 profile: one(profiles, {5 fields: [users.id],6 references: [profiles.userId],7 }),8}));relations(users, ...) 表示此关系属于 users 表。one(profiles, ...) 指定与 profiles 表的一对一关系。fields: [users.id] —— users 表中的主键。references: [profiles.userId] —— profiles 表中链接回 users.id 的外键。这告诉 Drizzle ORM,一个 user 可以有一个关联的 profile,通过 profiles 表中的 userId 外键链接。
with 查询相关数据要获取用户及其配置文件,使用查询构建器中的 with 选项。
xxxxxxxxxx61const users = await db.query.user.findMany({2 with: {3 profile: true,4 },5 where: eq(user.id, 1)6});findFirst 检索第一个匹配的用户。with: { profile: true } 在结果中包含相关 profile 数据。userRelations 后,Drizzle ORM 知道如何基于外键关系连接 users 和 profiles。返回的对象包括嵌套相关数据:
xxxxxxxxxx111{2 id: 1,3 fullName: "Kyle",4 age: 29,5 email: "kyle@example.com",6 profile: {7 id: 1,8 bio: "streamer, entrepreneur, student",9 userId: 110 }11}这演示了一对一关系成功建立和查询。
userId 在 profiles 表中,引用 users.id。integer) 必须匹配引用的主键类型 (serial → 底层 integer)。relations() 定义表链接,用于类型推断和查询。with 查询:启用相关数据的急切加载,以类型安全方式。此设置通过数据库约束确保数据完整性,并在使用 Drizzle ORM 的应用中启用高效、类型安全的检索。
视频解释了使用 Drizzle ORM 在两个表——具体是 users 表和 posts 表——之间创建一对多关系的过程。关系定义为一个用户可以有多个帖子,而每个帖子属于一个用户。
posts 表 Schema表包含以下字段:
id:使用 serial() 的主键,用于自增整数。text:使用 varchar(256) 限制为 256 个字符的可变长度文本字段。authorId:引用 users 表的外键,定义为 integer()(不是 serial()) 以避免自增行为。此字段将每个帖子链接到其作者。示例代码:
xxxxxxxxxx51export const posts = pgTable('post', {2 id: serial('id').primaryKey(),3 text: varchar('text', { length: 256 }),4 authorId: integer('author_id'),5});users 表中建立关系在 users 表的 relations 回调中,定义 many 关系将用户连接到其帖子。这表明一个用户可以有零个或多个帖子。
示例代码:
xxxxxxxxxx41export const userRelations = relations(users, ({ many }) => ({2 profile: oneToOne(profiles, { fields: [users.id], references: [profiles.userId] }),3 posts: many(posts),4}));在这里,many(posts) 从 users 到 posts 建立一对多链接。
posts 表中定义反向关系posts 表还需要关系定义,以表明每个帖子属于一个用户(作者)。这使用 one 函数完成。
示例代码:
xxxxxxxxxx61export const postRelations = relations(posts, ({ one }) => ({2 author: one(users, {3 fields: [posts.authorId],4 references: [users.id],5 }),6}));fields: [posts.authorId]:指定 posts 表中的外键。references: [users.id]:指定 authorId 引用 users 表中的 id 主键。定义 schema 和关系后:
posts 表并建立外键约束。为了强制外键,在迁移中更新 authorId 字段包括:
xxxxxxxxxx11authorId: integer('author_id').notNull().references(() => users.id),这确保 posts.authorId 和 users.id 之间的引用完整性。
将虚拟数据插入数据库以测试关系:
authorId 链接到特定用户。authorId = 1 的帖子,用户 2 有 authorId = 2 的帖子等。两种类型的查询演示关系如何工作:
a. 获取用户及其帖子
使用带有 with 子句的 db.query.users.findFirst 包括相关帖子:
xxxxxxxxxx61 const result = await db.query.users.findFirst({2 where: eq(users.id, 1),3 with: {4 posts: true,5 },6 }); 结果:返回用户及其 posts 数组,包含所有 authorId 匹配用户 ID 的帖子。
b. 获取帖子及其作者
使用 db.query.posts.findFirst 获取帖子并包括其作者:
xxxxxxxxxx51 const result = await db.query.posts.findFirst({2 with: {3 author: true,4 },5 }); 结果:返回帖子以及嵌套的 author 对象,包含完整用户数据(例如 ID、名称等)。
posts 中的 authorId 作为引用 users.id 的外键。relations() 定义,使用 one() 和 many() 函数,启用带有 with 子句的类型安全查询。.references() 来创建外键约束。此设置允许在两个方向高效查询相关数据,同时保持类型安全和数据库完整性。
视频解释了使用实际示例在 posts 表和 categories 表之间实现许多对许多关系。每个帖子可以属于多个类别,每个类别可以与多个帖子关联。这通过创建连接表并定义主表和连接表之间的一对多关系实现。
Posts 表:包含帖子(例如 Post 1、Post 2)。
Categories 表:包含类别(例如 Cat 1、Cat 2)。
示例关系:
在关系数据库中,不支持直接许多对许多关系。相反,创建 连接表(例如 post_categories)链接两个表。这将许多对许多关系分解为两个一对多关系:
posts → post_categories (一对多)categories → post_categories (一对多)在 schema.ts 中定义表
Categories 表:
xxxxxxxxxx41export const categories = pgTable('categories', {2 id: serial('id').primaryKey(),3 name: varchar('name', { length: 256 }),4});连接表 (post_categories):
xxxxxxxxxx41export const postOnCategories = pgTable('post_categories', {2 postId: integer('post_id').notNull().references(() => posts.id),3 categoryId: integer('category_id').notNull().references(() => categories.id),4});postId 是引用 posts.id 的外键。categoryId 是引用 categories.id 的外键。连接表的主键:
连接表使用组合主键结合 postId 和 categoryId:
xxxxxxxxxx61export const postOnCategories = pgTable('post_categories', {2 postId: integer('post_id').notNull().references(() => posts.id),3 categoryId: integer('category_id').notNull().references(() => categories.id),4}, (t) => ({5 pk: primaryKey({ columns: [t.postId, t.categoryId] }),6}));定义关系
Post 关系:
xxxxxxxxxx31export const postRelations = relations(posts, ({ many }) => ({2 postCategories: many(postOnCategories),3}));post_categories 连接表中有许多记录。Category 关系:
xxxxxxxxxx31export const categoryRelations = relations(categories, ({ many }) => ({2 postCategories: many(postOnCategories),3}));post_categories 连接表中有许多记录。连接表关系:
xxxxxxxxxx101export const postOnCategoriesRelations = relations(postOnCategories, ({ one }) => ({2 post: one(posts, {3 fields: [postOnCategories.postId],4 references: [posts.id],5 }),6 category: one(categories, {7 fields: [postOnCategories.categoryId],8 references: [categories.id],9 }),10}));posts 和 categories 都有许多对一关系。使用关系查询
获取帖子及其类别:
xxxxxxxxxx51const result = await db.query.posts.findFirst({2 with: {3 postCategories: true,4 },5});这检索帖子及其相关 post_categories 记录。
要包括类别细节(例如名称),使用:
xxxxxxxxxx111const result = await db.query.posts.findFirst({2 with: {3 postCategories: {4 with: {5 category: {6 columns: { name: true },7 },8 },9 },10 },11});获取类别及其帖子:
xxxxxxxxxx91const result2 = await db.query.categories.findFirst({2 with: {3 postCategories: {4 with: {5 post: true,6 },7 },8 },9});自定义选择字段:
要从连接表排除 postId 和 categoryId:
xxxxxxxxxx131with: {2 postCategories: {3 columns: {4 postId: false,5 categoryId: false,6 },7 with: {8 category: {9 columns: { id: true }, // 可选包括类别 ID10 },11 },12 },13}迁移和测试
运行迁移创建表:
xxxxxxxxxx11npm run migratecategories、post_categories 和 posts 表,带有适当外键。数据手动插入(未来集覆盖种子)。
使用数据库客户端(例如 Adminer)验证:
categories 表有如 "Sport"、"Economics" 的条目。posts 表有 ID 1 和 2 的帖子。post_categories 表链接帖子到类别(例如 Post 1 有 Category 1 和 2)。API 路由示例
在 API 路由(例如 /api/user)中,查询数据库:
xxxxxxxxxx121const post = await db.query.posts.findFirst({2 with: {3 author: true,4 postCategories: {5 with: {6 category: {7 columns: { name: true },8 },9 },10 },11 },12});relations 函数定义表之间关系。with 急切加载相关数据,包括通过连接表的嵌套关系。postId + categoryId) 确保唯一性。此实现允许灵活查询和数据检索,同时通过外键维护引用完整性。未来集将覆盖插入表中各种关系类型的数据(一对一、一对多、许多对许多)。
视频演示了通过替换 Prisma 将 Drizzle ORM 与 NestJS 项目集成。过程涉及:
移除 Prisma:从 package.json 删除 prisma(依赖和 devDependencies),并移除 prisma.service.ts 文件。
安装依赖:通过 npm 安装 drizzle-orm、better-sqlite3(SQLite 驱动)和 drizzle-kit(dev 依赖)。
创建 Drizzle 模块:使用 nest g m drizzle 生成 drizzle 模块。移除现有 prisma.service.ts。
定义 Schema:在 drizzle 目录中创建 schema.ts。使用 Drizzle 的 SQLite 表函数定义 users 表:
xxxxxxxxxx71import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';2export const users = sqliteTable('users', {3 id: integer('id').primaryKey({ autoIncrement: true }).notNull(),4 name: text('name'),5 email: text('email'),6 password: text('password')7});使用从 drizzle-orm/sqlite-core 的 integer 和 text,不是 pg-core。
创建 Drizzle Provider:在 drizzle.provider.ts 中,使用 NestJS 的工厂模式定义异步 provider:
xxxxxxxxxx161import { Provider } from '@nestjs/common';2import Database from 'better-sqlite3';3import { drizzle } from 'drizzle-orm/better-sqlite3';4import * as schema from './schema';56export const DRIZZLE_ASYNC_PROVIDER = 'DRIZZLE_PROVIDER';78export const drizzleProvider: Provider = {9 provide: DRIZZLE_ASYNC_PROVIDER,10 useFactory: async () => {11 const sqlite = new Database(process.env.DATABASE_URL);12 const db = drizzle(sqlite, { schema });13 return db;14 },15 exports: [DRIZZLE_ASYNC_PROVIDER]16};从 better-sqlite3 导入 Database(不是 sqlite3)以避免运行时错误。
注册 Provider:在 drizzle.module.ts 中,在 providers 数组中展开 drizzleProvider。
在服务中注入 DB:在 user.service.ts 中,使用 @Inject 注入 Drizzle DB 客户端:
xxxxxxxxxx111import { Inject } from '@nestjs/common';2import { DRIZZLE_ASYNC_PROVIDER } from '../drizzle/drizzle.provider';3import * as schema from '../drizzle/schema';4import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';56@Injectable()7export class UserService {8 constructor(9 @Inject(DRIZZLE_ASYNC_PROVIDER) private db: BetterSQLite3Database<typeof schema>10 ) {}11}重构 CRUD 操作:
创建:使用 db.select().from(users).where(eq(users.email, dto.email)) 检查重复电子邮件。插入使用:
xxxxxxxxxx11const newUsers = await this.db.insert(users).values({ dto, password: await hash(dto.password, 10) }).returning();按电子邮件/ID 查找:使用 db.select().from(users).where(eq(users.email, email)) 或 eq(users.id, id)。
在用户模块中注册:在 user.module.ts 中用 drizzleProvider 替换 Prisma provider。
运行迁移:
创建 drizzle.config.ts:
xxxxxxxxxx71import { Config } from 'drizzle-kit';2export default {3 schema: './src/drizzle/schema.ts',4 out: './drizzle',5 driver: 'better-sqlite',6 dbCredentials: { url: './sqlite.db' }7} as Config;运行 npx drizzle-kit generate:sqlite 创建迁移。
运行 npx drizzle-kit push:sqlite --db-path ./sqlite.db 应用它(注意:config 中 env 变量可能不加载;临时硬编码路径)。
测试:使用 Insomnia 测试 /auth/register 和 /auth/login。应用创建用户、检查重复并返回令牌。
schema 传递给 drizzle() 以启用 db.query。better-sqlite3 用于 SQLite;正确导入 Database。视频解释了使用 Drizzle ORM 将新数据插入 PostgreSQL 数据库,焦点是处理表之间关系:一对一、一对多和许多对许多。schema 包括 users、posts、categories 和 profiles 表,带有以下关系:
users 与 profiles 有一对一关系。users 与 posts 有一对多关系。posts 通过连接表 post_categories 与 categories 有许多对许多关系。逐步过程:
api/create/insert 目录中创建 POST 路由处理程序。db.insert(users).values({...}) 插入新用户。address、fullName、phone 和 score 的字段。id 由数据库自增。.returning() 获取插入的用户对象,或 .execute() 如果不需要返回数据。代码示例:
xxxxxxxxxx101export async function POST() {2 const newUser = await db.insert(users).values({3 address: "Street One",4 fullName: "User One",5 phone: "1234567890",6 score: 95,7 }).returning();89 return new Response(JSON.stringify(newUser));10}逐步过程:
.returning() 捕获生成的 user.id。user.id 在 profiles 表中插入相应配置文件,通过 userId 链接。profiles 表行通过外键引用 users 表。代码示例:
xxxxxxxxxx231export async function POST() {2 const newUser = await db.insert(users).values({3 address: "Street One",4 fullName: "User One",5 phone: "1234567890",6 score: 95,7 }).returning({ userId: users.id });89 const userId = newUser[0].userId;1011 await db.insert(profiles).values({12 userId: userId,13 bio: "I am a programmer",14 });1516 // 获取并返回用户及其配置文件17 const result = await db.query.users.findFirst({18 where: eq(users.id, userId),19 with: { profile: true },20 });2122 return new Response(JSON.stringify(result));23}userId 链接到用户。with: { profile: true } 获取用户及其配置文件。逐步过程:
id。authorId(新用户 ID)。forEach)单独或批量插入帖子。posts 关系的查询获取用户及其帖子。代码示例:
xxxxxxxxxx231export async function POST() {2 const newUser = await db.insert(users).values({3 fullName: "User3",4 // ... 其他字段5 }).returning({ userId: users.id });67 const userId = newUser[0].userId;89 const posts = ["Post One", "Post Two"];10 posts.forEach(async (text) => {11 await db.insert(posts).values({12 authorId: userId,13 text: text,14 });15 });1617 const result = await db.query.users.findFirst({18 where: eq(users.id, userId),19 with: { posts: true },20 });2122 return new Response(JSON.stringify(result));23}authorId 设置为新用户 ID。with: { posts: true } 加载相关帖子。逐步过程:
插入用户并捕获其 id。
插入多个类别。
插入多个帖子,每个通过 authorId 链接到用户。
使用连接表 post_categories 建立帖子和类别之间关系。
在 post_categories 中插入行连接:
使用嵌套 with 子句获取用户及其帖子和类别。
代码示例:
xxxxxxxxxx441export async function POST() {2 const newUser = await db.insert(users).values({ }).returning({ userId: users.id });3 const userId = newUser[0].userId;45 // 插入类别6 const newCats = await db.insert(categories).values([7 { name: "Cat1" },8 { name: "Cat2" },9 ]).returning({ catId: categories.id });1011 // 插入帖子12 const newPosts = await db.insert(posts).values([13 { authorId: userId, text: "Post One" },14 { authorId: userId, text: "Post Two" },15 ]).returning({ postId: posts.id });1617 // 插入许多对许多的连接行18 await db.insert(post_categories).values([19 { postId: newPosts[0].postId, categoryId: newCats[0].catId },20 { postId: newPosts[0].postId, categoryId: newCats[1].catId },21 { postId: newPosts[1].postId, categoryId: newCats[0].catId },22 { postId: newPosts[1].postId, categoryId: newCats[1].catId }23 ]);2425 // 获取用户及其帖子和类别26 const result = await db.query.users.findFirst({27 where: eq(users.id, userId),28 with: {29 posts: {30 with: {31 post_categories: {32 with: {33 category: {34 columns: { name: true },35 },36 },37 },38 },39 },40 },41 });4243 return new Response(JSON.stringify(result));44}post_categories 包含到 posts 和 categories 的外键。with 子句加载帖子及其关联类别名称。.returning() 捕获 ID 以链接相关记录。.returning() 与特定字段链式提取仅需要的数据(例如 users.id)。with 高效获取相关数据。此方法确保跨相关表的数据完整性,同时利用 Drizzle ORM 的类型安全查询能力。
种子指的是为测试目的自动将虚拟数据插入数据库的过程。这消除了每次测试时手动输入数据的需要。通过使用种子文件,开发者可以通过终端中的单个命令用一致的测试数据初始化数据库。
在实现种子之前,视频演示了如何创建 Drizzle ORM schema 的视觉图以理解结构和表之间关系。schema 包括:
id(serial,主键)、fullName(text)、phone(text)、address(text)、score(integer)。id(serial,主键)、bio(text)和 userId 外键引用 users 表。这建立 一对一关系 ——每个用户有一个配置文件,每个配置文件属于一个用户。id(serial,主键)、text(text)和 authorId(到 users 的外键)。这创建 许多对一关系 ——一个用户可以作者多个帖子,但每个帖子有一个作者。id(serial,主键)和 name(text)。postId(integer)和 categoryId(integer)。这启用 posts 和 categories 之间 许多对许多关系,其中一个帖子可以属于多个类别,一个类别可以应用于多个帖子。图使用其关系视觉连接这些表:
profiles.userId → users.id (1:1)posts.authorId → users.id (1:N)postsOnCategories.postId → posts.id (N:1)postsOnCategories.categoryId → categories.id (N:1)此结构确保 PostgreSQL 中的引用完整性和适当数据建模。
要实现种子,在 db 目录中创建 seed.ts 文件。过程涉及:
设置数据库连接
因为种子文件独立于主应用运行,需要单独数据库连接。
使用 pg 库创建 PostgreSQL 连接池:
xxxxxxxxxx41import { Pool } from 'pg';2const pool = new Pool({3 connectionString: process.env.DATABASE_URL,4});使用此池初始化 drizzle 客户端:
xxxxxxxxxx21import { drizzle } from 'drizzle-orm/node-postgres';2const db = drizzle(pool);使用 dotenv/config 访问环境变量:
xxxxxxxxxx11import 'dotenv/config';创建主种子函数
定义 async main() 函数处理数据库操作:
xxxxxxxxxx61async function main() {2 console.log('Seeding started');3 // 插入逻辑在这里4 console.log('Seeding finished');5 process.exit(0);6}调用函数并处理错误:
xxxxxxxxxx41main().catch(err => {2 console.error(err);3 process.exit(1);4});插入虚拟数据
Users 和 Profiles:
for 循环插入 10 个用户:
xxxxxxxxxx141for (let i = 0; i < 10; i++) {2 const user = await db.insert(users).values({3 address: 'First Street',4 fullName: 'My Name',5 phone: '123456789',6 score: 95,7 }).returning();8 const userId = user[0].id;910 await db.insert(profiles).values({11 userId: userId,12 bio: 'Web developer',13 });14}每个用户获取相应配置文件,通过 userId 链接。
Categories:
批量插入两个类别:
xxxxxxxxxx41const cats = await db.insert(categories).values([2 { name: 'Sports' },3 { name: 'Economics' }4]).returning();Posts:
插入两个帖子,链接到最后一个插入的用户:
xxxxxxxxxx101const insertedPosts = await db.insert(posts).values([2 { 3 authorId: userId, 4 text: 'Test text' 5 },6 { 7 authorId: userId, 8 text: 'Text two' 9 }10]).returning();Pivot 表 (PostsOnCategories):
填充连接表链接帖子和类别:
xxxxxxxxxx61await db.insert(postsOnCategories).values([2 { categoryId: cats[0].id, postId: insertedPosts[0].id },3 { categoryId: cats[1].id, postId: insertedPosts[0].id },4 { categoryId: cats[0].id, postId: insertedPosts[1].id },5 { categoryId: cats[1].id, postId: insertedPosts[1].id }6]);为了避免硬编码重复数据,使用 Faker.js 库生成随机、真实值:
安装 Faker:npm install @faker-js/faker
替换硬编码值:
faker.location.streetAddress()faker.person.fullName()faker.phone.number()faker.number.int({ min: 0, max: 100 })faker.person.bio()faker.lorem.sentence() 或 faker.lorem.text()示例:
xxxxxxxxxx61address: faker.location.streetAddress(),2fullName: faker.person.fullName(),3phone: faker.phone.number(),4score: faker.number.int({ min: 0, max: 100 }),5bio: faker.person.bio(),6text: faker.lorem.sentence()这确保每个种子记录有独特、可信数据。
在 package.json 中添加脚本:
xxxxxxxxxx31"scripts": {2 "db:seed": "node -r esbuild-register src/db/seed.ts"3}运行种子命令:
xxxxxxxxxx11npm run db:seed终端输出:
xxxxxxxxxx21Seeding started2Seeding finished
Drizzle Studio(GUI 工具)显示填充的表带有虚拟数据。
到最后,开发者可以使用单个命令用真实、相关数据种子 Drizzle ORM 数据库。
系列总结建议
这个系列是 YouTube 上最完整的 Drizzle 实战课之一,跟着 repo 一边敲代码一边看,学完能直接上手项目!
如果想针对某集要更详细的代码示例或 2026 年更新适配,告诉我~ 🚀