You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14服务器缓存导致SSG预渲染MongoDB连接异常问题

问题:Next.js SSG构建时MongoDB连接缓存导致的MongoNotConnectedError

我正在开发一个需要国际化的厨师网站,使用SSG为每个语言环境预渲染所有页面。构建阶段遇到了问题:只有当后端数据被缓存时才会报错,禁用缓存、不使用SSG或开发模式下一切正常。缓存会阻塞数据库请求,抛出MongoNotConnectedError: Client must be connected before running operations。

已经确认不是数据库本身的问题,禁用缓存后运行正常,但这样就失去了SSG的意义——页面会每次都从数据库动态获取数据重新生成。

SSG语言环境在主布局级别设置:

import { getAllProducts } from '@/database/dbProducts';
import XDraggableList from '../XDraggableList';
import MenuItem from './MenuItem';
import { IMenuItem } from '@/interfaces/IMenuItem';

const Menu = async () => {
  
const products: IMenuItem[] = await getAllProducts();

  // ... 剩余代码
};

export default Menu;

数据库数据获取代码

import { db } from '.';
import { IMenuItem } from '@/interfaces/IMenuItem';
import Product from '@/models/Product';
import { unstable_noStore } from 'next/cache';

export const getAllProducts = async (): Promise<IMenuItem[]> => {
  // TODO: 为什么不禁用缓存时构建会报错
  // unstable_noStore();
  // 如果不使用unstable_noStore(),构建时就会出现MongoNotConnectedError
  // 但禁用缓存后,请求会一直访问数据库,失去缓存作用

  await db.connect();
  const products = await Product.find().sort({ name: 1 }).lean();
  await db.disconnect();

  return JSON.parse(JSON.stringify(products));
};

构建错误信息

$ yarn build
yarn run v1.22.19
$ next build
   ▲ Next.js 14.0.3
   - Environments: .env

 ✓ Creating an optimized production build
 ✓ Compiled successfully
 ✓ Linting and checking validity of types    
 ✓ Collecting page data    
   Generating static pages (24/32) [  ==]
Connected to MongoDb:
MongoNotConnectedError: Client must be connected before running operations
    at executeOperationAsync (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:23:19)
    at C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:12:45
    at maybeCallback (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\utils.js:269:21)
    at executeOperation (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\operations\execute_operation.js:12:38)
    at FindCursor._initialize (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\find_cursor.js:55:73)
    at [kInit] (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:450:38)
    at next (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:510:32)
    at FindCursor.next (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:226:16)
    at [Symbol.asyncIterator] (C:\Users\pcjos\Desktop\Proyectos\Ingrid\shadcn-ingrid-webpage\node_modules\mongodb\lib\cursor\abstract_cursor.js:160:45)
    at AsyncGenerator.next (<anonymous>) {
  [Symbol(errorLabels)]: Set(0) {}
}

错误重复7次后,各语言页面的预渲染都会报错。禁用缓存能解决问题,但会让SSG失效,请问该如何处理?


解决方案

问题根源

Next.js在SSG构建时会缓存数据获取函数的结果,当缓存命中时,不会重新执行函数体内的代码——包括await db.connect()和await db.disconnect()。第一次执行函数时,连接建立后又被断开,后续缓存复用结果时,不会重新建立连接,但MongoDB驱动会尝试复用之前的断开连接,导致报错。

简单说:缓存让getAllProducts只执行一次,断开连接后,后续复用缓存的场景下(多语言页面预渲染),没有重新建立连接就直接执行查询,触发错误。

修复方案

方案1:修改MongoDB连接逻辑,避免重复断开连接

不要在每次数据请求后都断开连接,而是复用全局连接。MongoDB驱动本身会维护连接池,频繁断开/重连反而影响性能。

修改db模块的连接逻辑:

// db/index.ts
import mongoose from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI!;

if (!MONGODB_URI) {
  throw new Error('Please define the MONGODB_URI environment variable');
}

// 全局缓存连接实例
let cached = global.mongoose;

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

export async function connect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      bufferCommands: false,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose;
    });
  }

  cached.conn = await cached.promise;
  return cached.conn;
}

// 移除disconnect逻辑,不需要每次请求后断开

然后修改getAllProducts:

export const getAllProducts = async (): Promise<IMenuItem[]> => {
  await db.connect();
  const products = await Product.find().sort({ name: 1 }).lean();
  // 不再调用disconnect
  return JSON.parse(JSON.stringify(products));
};

这样连接会被全局复用,即使Next.js缓存数据获取函数,后续执行时也能使用已建立的连接(或者自动重新连接),不会出现断开的情况。

方案2:使用revalidate控制缓存,而非禁用缓存

如果必须保持断开连接的逻辑,可以在页面或数据函数中指定revalidate,让Next.js在构建时正确处理多语言场景的缓存,同时保留SSG能力。

在Menu页面添加generateStaticParams和revalidate:

import { getAllProducts } from '@/database/dbProducts';
// ... 其他导入

export async function generateStaticParams() {
  // 返回所有语言环境参数,比如['en', 'es', 'fr']
  return [{ lang: 'en' }, { lang: 'es' }, /* ...其他语言 */];
}

// 设置缓存重新验证时间,这里设置为永久(SSG),或者按需设置
export const revalidate = false; // 等同于SSG,永久缓存

const Menu = async ({ params }: { params: { lang: string } }) => {
  const products: IMenuItem[] = await getAllProducts();
  // ... 剩余代码
};

export default Menu;

同时修改getAllProducts,确保连接逻辑在缓存时也能正确执行:

export const getAllProducts = async (): Promise<IMenuItem[]> => {
  // 不需要unstable_noStore
  await db.connect();
  try {
    const products = await Product.find().sort({ name: 1 }).lean();
    return JSON.parse(JSON.stringify(products));
  } finally {
    // 使用finally确保即使查询出错也能断开连接
    await db.disconnect();
  }
};

不过这种方案还是不如方案1高效,因为频繁断开重连会增加数据库负担。

方案3:在构建时预取所有数据,避免多语言页面重复请求

在generateStaticParams中预取数据,然后传递给页面组件,这样所有语言页面复用同一份数据,避免多次调用getAllProducts:

export async function generateStaticParams() {
  const products = await getAllProducts();
  return ['en', 'es', 'fr'].map(lang => ({
    lang,
    props: { products }, // 传递预取的数据
  }));
}

const Menu = async ({ params, props }: { params: { lang: string }, props: { products: IMenuItem[] } }) => {
  const { products } = props;
  // ... 剩余代码
};

export default Menu;

这种方式下,getAllProducts只执行一次,构建时所有语言页面复用这份数据,既保留SSG缓存,又避免多次连接数据库。


内容的提问来源于stack exchange,提问作者jm3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:50:57