Node.js: SQLite 与 Prisma
最后更新:2026-08-26
Charlie 正在开发一款桌面笔记应用,需要本地存储数据,但不想让用户额外安装 MySQL 服务。SQLite 是零配置的嵌入式数据库,一个文件就是整个数据库;再搭配 Prisma ORM,让 SQL 操作拥有完整的类型安全。
1. 1 better-sqlite3:同步 API 的嵌入式数据库
(1)为什么选 better-sqlite3
Node.js 生态中有多个 SQLite 库,better-sqlite3 以同步 API著称——调用即返回,无需 await 或回调嵌套。它在底层用 C++ 编译 SQLite,性能远超异步封装库。
| 特点 | 说明 |
|---|---|
| 同步 API | 无回调地狱,代码线性可读 |
| 零配置 | 无需安装数据库服务,npm install 即用 |
| 单文件存储 | 整个数据库就是一个 .db 文件 |
| 事务支持 | 嵌套事务、预编译语句一应俱全 |
| 高性能 | C++ 绑定,比 node-sqlite3 快 2-3 倍 |
| 跨平台 | Windows / macOS / Linux 均可编译 |
(2)安装与基本连接
npm init -y
npm install better-sqlite3
const Database = require('better-sqlite3');
const db = new Database('myapp.db');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
console.log('SQLite 版本:', db.prepare('SELECT sqlite_version()').get());
(3)常用方法速查
| 方法 | 用途 | 示例 |
|---|---|---|
db.prepare(sql) |
创建预编译语句 | const stmt = db.prepare('SELECT * FROM users WHERE id = ?') |
stmt.run(...params) |
执行 INSERT/UPDATE/DELETE | stmt.run(1, 'Charlie') |
stmt.get(...params) |
返回单行对象 | stmt.get(1) |
stmt.all(...params) |
返回所有行数组 | stmt.all() |
stmt.values(...params) |
返回值数组(无键名) | stmt.values() |
db.exec(sql) |
执行多条 SQL | db.exec(schemaSql) |
db.transaction(fn) |
创建事务函数 | const insertMany = db.transaction((items) => {...}) |
db.pragma(cmd) |
设置/查询 PRAGMA | db.pragma('journal_mode = WAL') |
▶ 示例:better-sqlite3 CRUD 完整流程
const Database = require('better-sqlite3');
const db = new Database('notes.db');
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)
`);
const insert = db.prepare('INSERT INTO notes (title, body) VALUES (?, ?)');
const result = insert.run('第一篇笔记', 'Hello SQLite!');
console.log('插入行 ID:', result.lastInsertRowid);
const find = db.prepare('SELECT * FROM notes WHERE id = ?');
console.log('查询结果:', find.get(1));
const update = db.prepare('UPDATE notes SET title = ? WHERE id = ?');
update.run('修改后的标题', 1);
const remove = db.prepare('DELETE FROM notes WHERE id = ?');
remove.run(1);
const listAll = db.prepare('SELECT * FROM notes ORDER BY created_at DESC');
console.log('全部笔记:', listAll.all());
db.close();
(4)事务用法
const insertMany = db.transaction((notes) => {
for (const n of notes) {
insert.run(n.title, n.body);
}
});
insertMany([
{ title: '笔记 A', body: '内容 A' },
{ title: '笔记 B', body: '内容 B' },
]);
2. 2 SQL 语句基础回顾
(1)四大操作 CRUD
| 操作 | SQL | 关键字 |
|---|---|---|
| Create | INSERT | INSERT INTO 表 (列) VALUES (值) |
| Read | SELECT | SELECT 列 FROM 表 WHERE 条件 |
| Update | UPDATE | UPDATE 表 SET 列=值 WHERE 条件 |
| Delete | DELETE | DELETE FROM 表 WHERE 条件 |
(2)常用查询子句
SELECT 列名
FROM 表名
WHERE 条件
GROUP BY 分组列
HAVING 分组条件
ORDER BY 排序列 ASC|DESC
LIMIT 数量 OFFSET 偏移
(3)连接查询
db.exec(`
CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author_id INTEGER REFERENCES authors(id)
)
`);
const joinQuery = db.prepare(`
SELECT books.title, authors.name AS author
FROM books
JOIN authors ON books.author_id = authors.id
`);
3. 3 Prisma 安装与初始化
(1)Prisma 是什么
Prisma 是下一代 Node.js / TypeScript ORM,核心流程:
flowchart LR
A["schema.prisma"] -->|"prisma migrate dev"| B["迁移 SQL"]
A -->|"prisma generate"| C["Prisma Client"]
C -->|"类型安全查询"| D[("数据库")]
B --> D
- schema.prisma:声明式定义数据模型
- Prisma Migrate:自动生成并执行迁移 SQL
- Prisma Client:自动生成的类型安全查询客户端
(2)项目初始化
mkdir prisma-notes && cd prisma-notes
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider sqlite
初始化后生成:
prisma-notes/
├── prisma/
│ └── schema.prisma
├── .env
└── package.json
.env 文件内容:
DATABASE_URL="file:./dev.db"
(3)Prisma 命令速查
| 命令 | 用途 |
|---|---|
npx prisma init |
初始化 Prisma 项目 |
npx prisma migrate dev |
创建并应用开发迁移 |
npx prisma migrate deploy |
生产环境应用迁移 |
npx prisma generate |
生成 Prisma Client |
npx prisma studio |
打开可视化管理界面 |
npx prisma db push |
原型阶段直接推送 schema(不生成迁移文件) |
npx prisma db seed |
运行种子数据脚本 |
4. 4 schema.prisma 定义模型
(1)基础结构
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Note {
id Int @id @default(autoincrement())
title String
body String @default("")
pinned Boolean @default(false)
tags String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
(2)字段类型速查
| Prisma 类型 | SQLite 映射 | 说明 |
|---|---|---|
String |
TEXT | 字符串 |
Int |
INTEGER | 32 位整数 |
BigInt |
INTEGER | 64 位整数 |
Float |
REAL | 浮点数 |
Boolean |
INTEGER | 0 或 1 |
DateTime |
TEXT | ISO 8601 字符串 |
Json |
TEXT | JSON 字符串 |
Bytes |
BLOB | 二进制数据 |
Decimal |
TEXT | 高精度小数 |
注意:SQLite 类型系统与 PostgreSQL 不同,Prisma 在底层做适配。切换
provider时字段类型映射会自动调整。
(3)属性与修饰符
| 修饰符 | 用途 | 示例 |
|---|---|---|
@id |
主键 | id Int @id |
@default |
默认值 | @default(autoincrement()) / @default(now()) / @default("active") |
@unique |
唯一约束 | email String @unique |
@relation |
关系定义 | @relation(fields: [authorId], references: [id]) |
@map / @@map |
列/表名映射 | @map("created_at") |
@@unique |
复合唯一 | @@unique([firstName, lastName]) |
@@index |
复合索引 | @@index([categoryId, createdAt]) |
? |
可选字段 | bio String? |
(4)关系定义
model User {
id Int @id @default(autoincrement())
email String @unique
name String
notes Note[]
}
model Note {
id Int @id @default(autoincrement())
title String
body String @default("")
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
}
▶ 示例:定义多模型关系
// prisma/schema.prisma — 用户、笔记和标签模型之间的关系
model User {
id Int @id @default(autoincrement())
email String @unique
name String
notes Note[]
createdAt DateTime @default(now())
}
model Note {
id Int @id @default(autoincrement())
title String
body String @default("")
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags NoteTag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
notes NoteTag[]
}
model NoteTag {
noteId Int
tagId Int
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([noteId, tagId])
@@index([tagId])
}
本例定义了三种模型和两类关系:
- User → Note:一对多,级联删除
- Note ↔ Tag:通过中间表
NoteTag实现多对多,使用复合主键@@id和索引@@index
5. 5 Prisma Migrate 迁移
(1)创建并应用迁移
npx prisma migrate dev --name init
执行后:
prisma/
├── schema.prisma
└── migrations/
└── 20260703_init/
└── migration.sql
生成的 migration.sql:
CREATE TABLE "Note" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL DEFAULT '',
"pinned" BOOLEAN NOT NULL DEFAULT false,
"tags" TEXT NOT NULL DEFAULT '',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
(2)迁移工作流
开发阶段: schema 修改 → npx prisma migrate dev --name 描述
测试阶段: npx prisma migrate deploy(仅应用,不创建新迁移)
原型阶段: npx prisma db push(跳过迁移文件,快速迭代)
重置数据: npx prisma migrate reset(清空数据库并重放所有迁移)
(3)生产部署
npx prisma migrate deploy
migrate deploy只执行未应用的迁移,不会创建新迁移,也不会重置数据——适合 CI/CD 流水线。
6. 6 Prisma Client CRUD
(1)初始化 Client
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
// CRUD 操作写在这里
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
(2)Create — 创建
const note = await prisma.note.create({
data: {
title: '学习 Prisma',
body: 'Prisma 让数据库操作类型安全',
pinned: true,
},
});
const notes = await prisma.note.createMany({
data: [
{ title: '笔记 A', body: '内容 A' },
{ title: '笔记 B', body: '内容 B', pinned: true },
],
});
(3)Read — 查询
const one = await prisma.note.findUnique({ where: { id: 1 } });
const first = await prisma.note.findFirst({
where: { pinned: true },
orderBy: { createdAt: 'desc' },
});
const all = await prisma.note.findMany();
const filtered = await prisma.note.findMany({
where: {
pinned: true,
title: { contains: 'Prisma' },
},
});
(4)Update — 更新
const updated = await prisma.note.update({
where: { id: 1 },
data: { title: '更新后的标题', pinned: false },
});
const count = await prisma.note.updateMany({
where: { pinned: false },
data: { tags: 'archived' },
});
(5)Delete — 删除
const deleted = await prisma.note.delete({ where: { id: 1 } });
const deleteCount = await prisma.note.deleteMany({
where: { pinned: false },
});
(6)查询过滤器一览
| 过滤器 | 含义 | 示例 |
|---|---|---|
equals |
等于 | { title: { equals: 'Hello' } } |
not |
不等于 | { id: { not: 1 } } |
contains |
包含 | { title: { contains: 'Prisma' } } |
startsWith |
前缀 | { title: { startsWith: '学习' } } |
endsWith |
后缀 | { email: { endsWith: '@test.com' } } |
in |
在列表中 | { id: { in: [1, 2, 3] } } |
notIn |
不在列表中 | { id: { notIn: [4, 5] } } |
lt / lte |
小于 / 小于等于 | { id: { lte: 10 } } |
gt / gte |
大于 / 大于等于 | { id: { gte: 5 } } |
AND |
与 | { AND: [{ pinned: true }, { title: { contains: 'A' } }] } |
OR |
或 | { OR: [{ pinned: true }, { pinned: false }] } |
NOT |
非 | { NOT: { title: 'Hello' } } |
▶ 示例:关联数据查询与过滤
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function queryNotes() {
// 查询已发布的笔记,同时加载作者和标签
const notes = await prisma.note.findMany({
where: {
published: true,
title: { contains: 'Prisma' },
},
include: {
author: {
select: { id: true, name: true, email: true },
},
tags: {
include: {
tag: { select: { id: true, name: true } },
},
},
},
orderBy: { createdAt: 'desc' },
take: 10,
});
console.log(`找到 ${notes.length} 条笔记`);
for (const note of notes) {
const tagNames = note.tags.map((nt) => nt.tag.name).join(', ');
console.log(`${note.title} — ${note.author.name} [${tagNames}]`);
}
}
queryNotes()
.catch(console.error)
.finally(() => prisma.$disconnect());
本示例使用 include 预加载关联的 author 和 tags,再用 select 限制返回字段。结果中包含嵌套数据,无需额外查询。
7. 7 排序与分页
(1)排序
const sorted = await prisma.note.findMany({
orderBy: [
{ pinned: 'desc' },
{ createdAt: 'desc' },
],
});
(2)分页
const PAGE_SIZE = 10;
const page1 = await prisma.note.findMany({
skip: 0,
take: PAGE_SIZE,
orderBy: { createdAt: 'desc' },
});
const page2 = await prisma.note.findMany({
skip: PAGE_SIZE,
take: PAGE_SIZE,
orderBy: { createdAt: 'desc' },
});
(3)游标分页(大数据集推荐)
const first = await prisma.note.findMany({
take: 10,
orderBy: { id: 'asc' },
});
const cursor = first[first.length - 1].id;
const next = await prisma.note.findMany({
take: 10,
skip: 1,
cursor: { id: cursor },
orderBy: { id: 'asc' },
});
▶ 示例:搜索与游标分页
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function searchNotes(searchTerm, cursorId = null) {
const PAGE_SIZE = 5;
const results = await prisma.note.findMany({
take: PAGE_SIZE + 1,
skip: cursorId ? 1 : 0,
cursor: cursorId ? { id: cursorId } : undefined,
where: {
OR: [
{ title: { contains: searchTerm } },
{ body: { contains: searchTerm } },
],
},
include: { author: { select: { name: true } } },
orderBy: { id: 'asc' },
});
const hasMore = results.length > PAGE_SIZE;
if (hasMore) results.pop();
return {
items: results,
nextCursor: hasMore ? results[results.length - 1].id : null,
hasMore,
};
}
async function main() {
let cursor = null;
for (let page = 1; page <= 3; page++) {
const { items, nextCursor, hasMore } = await searchNotes('Prisma', cursor);
console.log(`第 ${page} 页: ${items.length} 条结果`);
items.forEach((n) => console.log(` ${n.id}: ${n.title}`));
if (!hasMore) break;
cursor = nextCursor;
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
本例在 title 和 body 中实现全文搜索,并结合游标分页。每次多取一条来判断是否有下一页,返回前移除多余记录。
8. 8 数据库选型对比
(1)SQLite vs MySQL vs PostgreSQL vs MongoDB
| 维度 | SQLite | MySQL | PostgreSQL | MongoDB |
|---|---|---|---|---|
| 类型 | 嵌入式 | 客户端-服务器 | 客户端-服务器 | 文档型 |
| 安装 | 零安装(随 npm 包) | 需安装服务 | 需安装服务 | 需安装服务 |
| 并发写入 | 单写入者 | 多写入者 | 多写入者 | 多写入者 |
| 数据规模 | 中小(GB 级) | 大(TB 级) | 大(TB 级) | 大(TB 级) |
| 适用场景 | 桌面应用 / 原型 / 测试 | Web 应用 / 中型项目 | 复杂查询 / 地理数据 | 灵活 Schema / 日志 |
| JSON 支持 | 有限(JSON1 扩展) | 支持 | 原生 JSONB | 原生文档 |
| 全文搜索 | FTS5 扩展 | 全文索引 | tsvector | 文本索引 |
| 事务 | 完整 ACID | 完整 ACID | 完整 ACID | 4.0+ 多文档事务 |
| 许可证 | 公有领域 | GPL / 商业 | PostgreSQL | SSPL |
(2)ORM 框架对比
| 维度 | Mongoose | Prisma | Sequelize | TypeORM |
|---|---|---|---|---|
| 语言 | JavaScript | TypeScript 优先 | JavaScript | TypeScript 优先 |
| 数据库 | MongoDB only | SQLite/MySQL/PostgreSQL/MongoDB | MySQL/PostgreSQL/SQLite/MSSQL | MySQL/PostgreSQL/SQLite/MSSQL |
| Schema 定义 | JS 对象 | .prisma 声明式文件 |
JS 模型定义 | 装饰器 / 实体类 |
| 类型安全 | 弱(需手动) | 强(自动生成) | 弱 | 中(装饰器类型) |
| 迁移工具 | 无内置 | prisma migrate |
sequelize-cli |
内置 |
| 查询方式 | 链式 API | 链式对象 | 链式 / 原生 SQL | QueryBuilder / 原生 |
| N+1 问题 | 需 populate | 自动 includes | 需 eager/lazy | 需 relations |
| 社区规模 | 大 | 快速增长 | 大 | 大 |
| 适合项目 | MongoDB 项目 | 全栈 TypeScript | 传统 Node.js | NestJS 生态 |
9. 9 综合示例:笔记管理数据层
用 Prisma + SQLite 构建完整的笔记管理 CRUD 数据层。
▶ 示例:Prisma + SQLite 笔记管理
Step 1 — Schema 定义
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Note {
id Int @id @default(autoincrement())
title String
body String @default("")
pinned Boolean @default(false)
tags String @default("")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("notes")
}
Step 2 — 迁移
npx prisma migrate dev --name notes_init
Step 3 — 数据访问层
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function createNote(data) {
return prisma.note.create({ data });
}
async function getNoteById(id) {
return prisma.note.findUnique({ where: { id } });
}
async function updateNote(id, data) {
return prisma.note.update({ where: { id }, data });
}
async function deleteNote(id) {
return prisma.note.delete({ where: { id } });
}
async function listNotes({ page = 1, pageSize = 10, pinned, keyword } = {}) {
const where = {};
if (pinned !== undefined) where.pinned = pinned;
if (keyword) where.title = { contains: keyword };
const [items, total] = await Promise.all([
prisma.note.findMany({
where,
orderBy: [{ pinned: 'desc' }, { createdAt: 'desc' }],
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.note.count({ where }),
]);
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
}
async function togglePin(id) {
const note = await prisma.note.findUnique({ where: { id } });
if (!note) throw new Error('笔记不存在');
return prisma.note.update({
where: { id },
data: { pinned: !note.pinned },
});
}
module.exports = {
createNote,
getNoteById,
updateNote,
deleteNote,
listNotes,
togglePin,
};
Step 4 — 使用示例
const db = require('./note-service');
async function main() {
const n1 = await db.createNote({ title: '学习 Prisma', body: '类型安全 ORM', pinned: true });
const n2 = await db.createNote({ title: 'SQLite 要点', body: '零配置嵌入式数据库' });
const n3 = await db.createNote({ title: 'Prisma 迁移', body: 'migrate dev 驱动' });
console.log('单条查询:', await db.getNoteById(n1.id));
await db.updateNote(n2.id, { body: '更新内容' });
await db.togglePin(n3.id);
const result = await db.listNotes({ page: 1, pageSize: 10, keyword: 'Prisma' });
console.log('搜索结果:', result);
await db.deleteNote(n2.id);
const all = await db.listNotes({ page: 1, pageSize: 10 });
console.log('剩余笔记:', all);
}
main()
.catch(console.error)
.finally(() => require('@prisma/client').PrismaClient &&
require('./node_modules/.prisma/client').$disconnect?.());
运行:
node index.js
Step 5 — 可视化管理
npx prisma studio
浏览器打开 http://localhost:5555,即可可视化浏览和编辑数据。
10. 10 本课小结
- SQLite 是零配置嵌入式数据库,
better-sqlite3提供高性能同步 API - Prisma 通过
schema.prisma声明式定义模型,自动生成迁移和类型安全 Client prisma migrate dev驱动开发迁移,prisma migrate deploy用于生产部署- Prisma Client 支持
findMany、create、update、delete及丰富的过滤/排序/分页 - 数据库选型:桌面/原型选 SQLite,Web 应用选 MySQL/PostgreSQL,灵活 Schema 选 MongoDB
- ORM 选型:MongoDB 用 Mongoose,TypeScript 全栈用 Prisma,NestJS 用 TypeORM
❓ 常见问题
- Q: SQLite 能支持并发吗? A: 支持多读单写,WAL 模式下可并发读取,但写入仍串行;高并发写入场景需换 MySQL/PostgreSQL。
- Q: Prisma 和 Sequelize 哪个好? A: Prisma 类型安全更强、迁移体验更好、查询无 N+1 隐患;Sequelize 生态成熟、支持更多数据库方言;新项目推荐 Prisma。
- Q: better-sqlite3 为什么是同步的? A: SQLite 本身是进程内库,I/O 耗时微秒级,异步反而增加事件循环开销;同步 API 消除回调嵌套,代码更简洁。
- Q: SQLite 最大数据库多大? A: 理论上限 281 TB,实际受操作系统单文件大小限制;常见场景几十 GB 完全没问题。
- Q: 什么时候该从 SQLite 迁移到 PostgreSQL? A: 需要多进程并发写入、原生 JSONB 查询、全文搜索、地理数据、行级安全策略或超过单机存储时迁移。
- Q: Prisma 支持 MongoDB 吗? A: 2.0+ 支持 MongoDB,但功能覆盖不如关系数据库完整;复杂聚合建议用 Mongoose。
- Q: 生产环境如何管理 Prisma Client 实例? A: 全局单例,避免每次请求新建;Next.js 等热重载环境需用
globalThis缓存防止连接泄漏。
📖 小节
- 1 better-sqlite3:同步 API 的嵌入式数据库的核心概念与使用方法
- 2 SQL 语句基础回顾的核心概念与使用方法
- 3 Prisma 安装与初始化的核心概念与使用方法
- 4 schema.prisma 定义模型的核心概念与使用方法
- 5 Prisma Migrate 迁移的核心概念与使用方法
- 6 Prisma Client CRUD的核心概念与使用方法
- 7 排序与分页的核心概念与使用方法
- 8 数据库选型对比的核心概念与使用方法
📝 作业
- 用
better-sqlite3创建一个users表,实现插入、按 email 查询、更新和删除操作,并用事务保证原子性。 - 初始化一个 Prisma 项目,定义
User和Post两个模型(一对多关系),执行迁移后用 Prisma Client 完成创建用户、发布文章、查询用户及其所有文章。 - 在笔记管理示例基础上,添加
Category模型,实现笔记按分类过滤,并在listNotes中支持按分类 ID 筛选。 - 对比
better-sqlite3原生 SQL 和 Prisma Client 实现同一组 CRUD 操作,记录代码行数和可读性差异。 - 写一个脚本,用
prisma.note.findMany实现关键词搜索 + 置顶优先 + 分页,对比 offset 分页和游标分页在 10000 条数据下的性能差异。