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传递给所有子组件,避免在每个页面/组件重复获取会话和创建实例。
- 创建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 };
- 在根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> ); }
- 在子组件/页面中使用:
// 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
相关产品推荐
相关产品推荐

