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

Next.js14多租户架构:search_path与数据库连接池问题求助

解决Next.js 14 + Kysely + node-postgres多租户schema切换问题

问题根源

你遇到的问题核心是数据库连接池的连接复用机制:onCreateConnection仅在连接首次从池中创建时执行一次,后续复用该连接时不会重新执行SET search_path。如果全局复用同一个Kysely实例,切换租户后,连接仍保留之前设置的schema,导致查询指向错误的租户数据。

解决方案

以下是几种优雅且可靠的解决方式,可根据你的场景选择:

方案1:查询拦截器(推荐)

通过Kysely的查询拦截器,在每次执行查询前动态设置search_path,完全规避连接复用带来的schema残留问题。

import { Pool } from 'pg';
import { PostgresDialect, Kysely, QueryInterceptor } from 'kysely';
import { Database, TenantSchema } from './types/db';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// 创建带租户schema拦截的dialect
const createTenantDialect = (tenantId?: number): PostgresDialect => {
  const baseDialect = new PostgresDialect({ pool });

  const schemaInterceptor: QueryInterceptor = {
    async transformQuery(query) {
      if (!tenantId) return query;

      // 在查询前注入SET search_path语句
      return {
        kind: 'ModifyQueryNode',
        query: {
          kind: 'RawNode',
          sql: `SET search_path TO tenant_${tenantId}, public; ${query.sql}`,
          parameters: [...query.parameters],
        },
      };
    },
  };

  return baseDialect.withQueryInterceptor(schemaInterceptor);
};

// 封装自动获取租户ID的DB实例生成函数
export const getTenantDB = async () => {
  // 从当前会话获取租户ID
  const session = await getServerSession(authOptions);
  const tenantId = session?.user?.tenant_id;

  return new Kysely<Database & TenantSchema>({
    dialect: createTenantDialect(tenantId),
    log: ['query', 'error'],
  });
};

方案2:连接池生命周期管理

通过连接池的afterRelease钩子重置schema,结合onCreateConnection在连接分配时设置租户schema,确保连接复用前状态干净。

import { Pool } from 'pg';
import { PostgresDialect, Kysely } from 'kysely';
import { Database, TenantSchema } from './types/db';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  // 连接放回池前重置schema为默认值
  afterRelease: async (client) => {
    await client.query('SET search_path TO public');
  },
});

export const createPostgresDialect = (tenantId?: number) => {
  return new PostgresDialect({
    pool,
    onCreateConnection: async (connection) => {
      if (tenantId) {
        await connection.executeQuery({
          sql: `SET search_path TO tenant_${tenantId}, public`,
          query: { kind: 'RawNode', sqlFragments: [], parameters: [] },
          parameters: []
        });
      }
    }
  });
};

export const getTenantDB = async () => {
  const session = await getServerSession(authOptions);
  const tenantId = session?.user?.tenant_id;

  return new Kysely<Database & TenantSchema>({
    dialect: createPostgresDialect(tenantId),
    log: ['query', 'error'],
  });
};

方案3:请求级上下文传递(优雅简化代码)

在Next.js App Router的根Layout中获取会话并创建DB实例,通过React Context传递给所有子组件,避免在每个页面/组件重复获取会话和创建实例。

  1. 创建DB Context:
// lib/db-context.ts
import { createContext, useContext } from 'react';
import { Kysely } from 'kysely';
import { Database, TenantSchema } from './types/db';

const DBContext = createContext<Kysely<Database & TenantSchema> | null>(null);

export const useDB = () => {
  const db = useContext(DBContext);
  if (!db) throw new Error('useDB must be used within a DBContext provider');
  return db;
};

export { DBContext };
  1. 在根Layout中注入DB实例:
// app/layout.tsx
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { getTenantDB } from '@/lib/db';
import { DBContext } from '@/lib/db-context';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const db = await getTenantDB();

  return (
    <html lang="en">
      <body>
        <DBContext.Provider value={db}>{children}</DBContext.Provider>
      </body>
    </html>
  );
}
  1. 在子组件/页面中使用:
// app/dashboard/page.tsx
import { useDB } from '@/lib/db-context';

export default async function DashboardPage() {
  const db = useDB();
  const tenantData = await db.selectFrom('tenant_table').selectAll().execute();

  return <div>{JSON.stringify(tenantData)}</div>;
}

关键注意事项

  • 避免全局复用同一个Kysely实例:每个请求/租户应该使用独立的Kysely实例(Kysely实例本身轻量,性能开销可忽略)。
  • 会话获取要确保在Server Components中执行:客户端组件无法直接获取服务器会话,需通过Server Components传递或API路由获取。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 13:22:16