在 Next.js 中集成 Knex 查询构建器操作 PostgreSQL:with-knex 官方示例全解析
导读:本篇文章以 next.js 官方仓库
examples/with-knex为蓝本,完整演示如何让 Knex 查询构建器与 Next.js 协同工作——从环境变量加载、连接配置、数据库迁移,到通过 API Route 查询 PostgreSQL 数据并渲染到页面。阅读完成后,你将掌握一套可复制的「Next.js + Knex + Postgres」实战搭建流程,并理解其背后的源码细节(含开发模式热重载下的连接池复用技巧),且同一套连接代码可平滑迁移到 Knex 支持的其他数据库。
示例概览:Knex 在 Next.js 中的定位
Knex 是一个功能强大的 SQL 查询构建器,支持 Postgres、MySQL、SQLite、MSSQL、Oracle 等主流数据库。示例 README.md 的定位很清晰:它本身不强制绑定某一种数据库,而是演示如何用 Knex 作为统一的数据访问层去连接并查询 Postgres,其余 Knex 所支持的数据库也都可以按同样方式接入。
官方这个示例(位于 examples/with-knex)是一个完整可运行的最小应用,目录结构非常精简:
examples/with-knex/
├── .env.local.example # 环境变量样例(含 PG_URI 占位值)
├── .gitignore # 已忽略 .env*.local 等本地敏感文件
├── knexfile.js # Knex CLI 配置文件(读取 PG_URI)
├── knex/
│ ├── index.js # 连接实例模块(带全局缓存)
│ └── migrations/
│ └── 20201015140127_initial.js # 示例迁移:创建 todos 表并写入种子数据
├── pages/
│ ├── api/
│ │ └── todos.js # API Route:查询并返回 todos 列表
│ └── index.js # 首页:客户端 fetch 并渲染 todos
└── package.json # 脚本与依赖声明
从依赖清单(见 package.json)可以看出示例的技术栈组合:knex 负责查询构建,pg 作为 PostgreSQL 驱动,@next/env 用于在 Node 侧加载 Next.js 环境变量,再加上 next、react、react-dom。
快速启动:用 create-next-app 拉取并运行示例
官方提供了两种体验方式,最快捷的是直接使用 create-next-app 以 with-knex 作为模板引导一个全新项目(npm / Yarn / pnpm 命令任选其一):
npx create-next-app --example with-knex with-knex-app
yarn create next-app --example with-knex with-knex-app
pnpm create next-app --example with-knex with-knex-app
命令执行后,create-next-app 会把整个示例模板复制到新目录 with-knex-app 中。随后进入项目并安装依赖:
npm install
# 或
yarn
# 或
pnpm install
由于示例依赖清单中 next 与 @next/env 均声明为 latest,安装时会拉取当前最新版本,而 knex@0.95.15、pg@8.4.1 则为示例编写时锁定的稳定版本——如果你在自己的项目中使用,可以按需升级。
准备一个 Postgres 数据库
接下来需要准备数据库连接。既可以在本地安装并启动一个 Postgres 实例,也可以直接使用 AWS RDS、DigitalOcean Managed Database 等 DBaaS 云服务。无论哪种方式,最终你都需要得到一个形如 postgres://<user>:<password>@<host>:<port>/<dbname> 的连接串。
配置环境变量
环境变量是示例中数据库配置的唯一入口。示例附带了环境变量样例 .env.local.example,内容如下:
PG_URI=postgres://admin:pass@localhost:5432/mydb
将它复制为本地环境文件 .env.local(该文件已被 .gitignore 中的 .env*.local 规则忽略,不会提交到版本库):
cp .env.local.example .env.local
然后把 .env.local 里的 PG_URI 改成你真实数据库的连接串,例如:
PG_URI=postgres://myuser:mypassword@myhost.example.com:5432/mydb
连接串的组成部分含义如下:
| 部分 | 示例值 | 说明 |
|---|---|---|
user |
myuser |
数据库用户名 |
password |
mypassword |
对应用户密码 |
host |
myhost.example.com |
数据库主机地址,本地可为 localhost |
port |
5432 |
Postgres 默认端口 |
dbname |
mydb |
要连接的数据库名 |
为什么 knexfile 能直接读到 Next.js 的环境变量
这里有一个值得注意的实现细节:Knex CLI(如 knex migrate:latest)本身并不认识 Next.js 的 .env.local 约定,因此示例的 knexfile.js 在文件最顶部手动调用了 @next/env 来加载环境变量:
const { loadEnvConfig } = require("@next/env");
const dev = process.env.NODE_ENV !== "production";
const { PG_URI } = loadEnvConfig("./", dev).combinedEnv;
module.exports = {
client: "pg",
connection: PG_URI,
migrations: {
directory: "./knex/migrations",
},
seeds: {
directory: "./knex/seeds",
},
};
各字段作用如下:
loadEnvConfig("./", dev):让 Knex 运行时也能解析 Next.js 项目根目录下的.env.local(dev为真时还会一并加载.env.development.local等开发环境文件),combinedEnv是合并后的环境对象;client: "pg":声明使用 PostgreSQL 驱动,若改用 MySQL、SQLite 等,仅需更换client值并安装对应驱动即可,这是 Knex 多数据库支持的关键;connection: PG_URI:直接以连接串形式传入连接配置;migrations.directory:指向迁移文件所在目录;seeds.directory:指向种子数据目录。
同时注意,这个配置文件以 CommonJS(require/module.exports)导出,因此可以被 knex CLI 以及 ESM 代码两种模式共同消费。
数据库迁移与种子数据:理解示例的 todos 表结构
迁移脚本做了什么
Knex 的迁移机制用于以可版本化的方式演进数据库结构。示例迁移文件 knex/migrations/20201015140127_initial.js 完成两件事:建表 + 插入初始数据。
up 方法先通过 knex.schema.createTable 创建名为 todos 的表,定义了三个字段:
exports.up = async function (knex) {
await knex.schema.createTable("todos", (table) => {
table.increments("id"); // 自增主键
table.string("text").notNullable(); // 待办内容,非空字符串
table.boolean("done").notNullable(); // 是否完成,非空布尔
});
await knex("todos").insert([
{ text: "Buy milk", done: true },
{ text: "Wash car", done: false },
]);
};
接着用查询构建器的 .insert() 接口直接写入两条演示数据(Buy milk / Wash car),让应用一启动就有内容可展示。down 方法则定义了回滚逻辑——用 knex.raw 执行原生 SQL DROP TABLE todos CASCADE 将表彻底删除,CASCADE 会一并清除依赖该表的外键约束等对象。
从这份迁移可以看出 Knex 的双层能力:knex.schema 负责 Schema 级 DDL(建表、改列),而 knex("表名") 负责面向数据行的 CRUD;当构建器语法无法覆盖需求时,还可退回到 knex.raw 写原生 SQL。
迁移相关命令一览
示例在 package.json 中预置了完整的迁移命令,覆盖了「创建、应用、回滚、查看状态」全生命周期:
| 命令 | 底层执行 | 用途 |
|---|---|---|
npm run migrate:make |
knex migrate:make |
新建一个空的迁移文件 |
npm run migrate:latest |
knex migrate:latest |
应用所有尚未执行的迁移 |
npm run migrate:up |
knex migrate:up |
只执行下一个待执行的迁移 |
npm run migrate:down |
knex migrate:down |
回滚最近一次执行的迁移 |
npm run migrate:status |
knex migrate:status |
查看迁移执行状态 |
npm run seed:create |
knex seed:make |
创建种子文件 |
npm run seed:run |
knex seed:run |
执行全部种子文件 |
对当前示例,只需应用一次即可完成建表与数据初始化:
npm run migrate:latest
# 或
yarn migrate:latest
# 或
pnpm migrate:latest
需要说明:种子目录 ./knex/seeds 在迁移脚本中虽已被配置,但示例当前并未提供实际的种子文件(examples/with-knex 目录下只有 migrations 目录),因此日常演示中用不到 seed:run;它属于预置好、方便你扩展的脚手架能力。
代码深读:连接实例缓存与 API Route 查询链路
knex/index.js:解决开发热重载下连接泄漏的关键
在 Next.js 开发模式下,代码会随着编辑而被反复重新执行(Fast Refresh)。如果每次模块重新加载都新建一个 Knex 连接实例而不做清理,就会造成连接不断累积,最终达到数据库连接上限。示例在 knex/index.js 中通过挂在 global 对象上做缓存来规避这一经典问题:
import knex from "knex";
import config from "../knexfile.js";
/**
* Global is used here to ensure the connection
* is cached across hot-reloads in development
*/
let cached = global.pg;
if (!cached) cached = global.pg = {};
export function getKnex() {
if (!cached.instance) cached.instance = knex(config);
return cached.instance;
}
理解这段代码的关键在于模块作用域与全局作用域的区别:
- 普通模块级变量在每次模块热替换/重新求值后都会重新初始化,导致连接被反复重建;
- 而
global是 Node.js 的进程级全局对象,其内容不会因模块重载而丢失; - 因此把连接实例挂在
global.pg(代码注释甚至给出了相关的 Next.js discussion 链接作参考)上,就能保证整个开发进程生命周期内只创建一次 Knex 实例,后续任何模块、任何请求都复用它。
getKnex() 采用惰性初始化模式:首次调用时才执行 knex(config),之后直接返回缓存实例。从源码结构可以推断,这种「模块只导出函数、连接由函数按需提供」的写法,同时兼顾了两点:一是在生产环境避免在模块加载阶段就建立数据库连接(此时环境变量可能尚未就绪或不需要连接),二是让所有调用方共享同一连接池,从而复用 Knex 内置的连接池能力。在你自己的 Next.js + 数据库项目(无论 Knex、Prisma 还是其他客户端)中,这一「global 缓存」写法都值得直接借鉴。
API Route:一条极简的查询链
数据出口是 Next.js Pages Router 的 API Route pages/api/todos.js,它只有短短几行:
import { getKnex } from "../../knex";
export default async function handler(req, res) {
const knex = getKnex();
const todos = await knex("todos");
res.status(200).json(todos);
}
执行流程解读:
- 从
../../knex(即 knex/index.js)导入getKnex; - 调用
getKnex()获取(首次创建后缓存的)Knex 实例; await knex("todos")等价于SELECT * FROM todos,Knex 会基于迁移文件里定义的表自动映射出行数据;- 通过
res.status(200).json(todos)以 JSON 返回查询结果。
由于 API Route 在服务端执行,pg 驱动直连数据库的过程对浏览器完全不可见,浏览器最终拿到的只是干净的 JSON 数据——这正是「服务端持有数据库凭证、客户端零暴露」的安全边界。
前端页面:客户端数据获取
首页 pages/index.js 是一个经典的客户端渲染取数模式:组件挂载后(useEffect 且依赖数组为空,仅执行一次)向 /api/todos 发起 fetch 请求,把返回的数组存入 state 后再渲染:
const [todos, setTodos] = useState();
useEffect(() => {
async function loadTodos() {
const resp = await fetch("/api/todos");
const data = await resp.json();
setTodos(data);
}
loadTodos();
}, []);
渲染逻辑利用 state 的「未定义」状态做加载提示,收到数据后逐条渲染待办文本,done 字段为真时追加 (complete) 后缀:
{!todos && <p className="todos-loading">Todos loading...</p>}
{todos &&
todos.map((todo) => {
return (
<p className="todos-item" key={todo.id}>
{todo.text} {todo.done && "(complete)"}
</p>
);
})}
这里的 key={todo.id} 正是对应迁移中定义的 id 自增主键。从示例可见一条完整的「数据库 → API Route → 浏览器」数据流:迁移建好的数据,经 Knex 查询、API Route 序列化,最终以列表形式呈现在页面上。
本地运行与预期结果
一切配置就绪后启动开发服务器:
npm run dev
# 或
yarn dev
# 或
pnpm dev
应用默认运行在 http://localhost:3000。正常工作时,页面标题下方会展示 "Todos" 区块,依次列出两条来自数据库的数据:
Buy milk (complete)
Wash car
(第二行 done 为 false,故无 (complete) 标记。)这正是 API Route 从 Postgres 取回并返回的数据;作为验证,你也可以直接在浏览器访问 http://localhost:3000/api/todos 查看接口返回的原始 JSON。
如果页面加载异常,请优先按「数据库连接是否正确可达 →
PG_URI是否已写入.env.local→ 迁移是否已成功执行」的顺序排查。
部署到 Vercel:环境变量的正确姿势
方式一:本地项目推送后导入
先在本地把项目推送至 GitHub / GitLab / Bitbucket 任一 Git 托管平台,再在 Vercel 上「Import Project」导入该仓库,触发自动化部署。
关键一步:导入项目时,务必在 Vercel 的 Environment Variables 面板中手动添加与本地 .env.local 中完全一致的变量(即 PG_URI),否则云端无法获知数据库连接信息,应用会因缺少配置而无法工作。注意 .env.local 中的本地值若指向 localhost,部署后必须替换为云数据库实例可公网访问的连接串(并确保数据库的网络访问策略放行了 Vercel 的出口 IP / 服务)。
方式二:直接使用模板一键部署
with-knex 示例还配置了「一键部署」按钮,点击后会引导你基于该模板在 Vercel 创建新项目,并在创建向导中直接填写 PG_URI(官方按钮的 env 参数里同时携带了 envDescription 提示该变量用于连接 Postgres)。
由于本仓库当前仅提供源码级示例,以上部署均要求你自己持有可访问的 PostgreSQL 实例;Vercel 本身不托管 Postgres 数据,数据库仍需依赖外部 DBaaS 或自建服务。
进阶延展:把示例改造成生产可用的注意点
结合 knexfile.js、knex/index.js 与 迁移脚本 的实现,从示例走向生产环境时有几点值得关注:
- 连接池与并发安全:示例演示用的是默认连接池行为。
global.pg缓存保证了单实例,但在 Serverless 部署(如 Vercel 的 Serverless Function)下,每个并发实例都是独立进程,各自持有一份连接,因此生产部署时应关注数据库的最大连接数,并按需配置 Knex 连接池参数(pool.min/pool.max)。 - 凭证安全:数据库连接串属于敏感信息,务必遵循示例的
.env*.local忽略规则,绝不提交到 Git;云端则通过平台的环境变量能力注入。 - 迁移纳入发布流程:生产环境部署前,应把
knex migrate:latest纳入 CI/CD 发布流程,保证数据库 Schema 与应用版本同步演进(示例中迁移目录与脚本已就绪)。 - 统一的数据访问层:示例把
getKnex收敛在 knex/index.js 单点导出,所有页面与 API 都经由它取连接。这种「单一入口」便于统一管理查询与事务,适合作为你项目数据访问层的起点。 - 替换数据库只需改两处:得益于 Knex 的多方言设计,要换到 MySQL 等数据库,仅需修改 knexfile.js 中的
client值、安装对应驱动、并把PG_URI换成相应连接串(可能需要微调迁移中的字段类型),业务代码基本不用变动。
从源码结构看,with-knex 示例刻意保持了最小化:它没有引入 ORM 模型层、没有封装 Repository,而是把 Knex 查询构建器最原生的用法直接摆在 pages/api/todos.js 中,让你在理解全链路的同时,可以按需叠加更复杂的抽象。以此为基础,你可以继续阅读仓库内其他数据库相关示例(例如 with-mongodb、with-prisma 同类生态)做横向对比,选择最适合自己数据模型与团队习惯的方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00