Vercel Postgres 搭配本地数据库
在 Learn Next.js 教程中数据库链接采用的是 Vercel Postgres
,本地开发会遇到一些网络问题,导致体验并不是很好。
因此,在本地开发时我期望能将本地数据库与 Vercel Postgres 一起使用,但目前支持的并不是很好。才有了下面这篇文章介绍。
安装 Postgres 数据库
选择你熟悉的方式搭建本地数据库,以下使用 Docker 命令:
docker run --name myPostgresDb -p 5432:5432 -e POSTGRES_USER=postgresUser -e POSTGRES_PASSWORD=postgresPW -e POSTGRES_DB=postgresDB -d postgres
遇到的问题
替换 .env
中的数据库配置为本地数据库信息:
POSTGRES_URL="postgres://postgresUser:postgresPW@127.0.0.1:5432/postgresDB"
POSTGRES_PRISMA_URL="postgres://postgresUser:postgresPW@127.0.0.1:5432/postgresDB?pgbouncer=true&connect_timeout=15"
POSTGRES_URL_NON_POOLING="postgres://postgresUser:postgresPW@127.0.0.1:5432/postgresDB"
POSTGRES_USER="postgresUser"
POSTGRES_HOST="127.0.0.1"
POSTGRES_PASSWORD="postgresPW"
POSTGRES_DATABASE="postgresDB"
执行 Examples 的 yarn seed
命令,起初会得到如下错误:
An error occurred while attempting to seed the database: VercelPostgresError: VercelPostgresError - 'invalid_connection_string': This connection string is meant to be used with a direct connection. Make sure to use a pooled connection string or try `createClient()` instead.
这是因为 Vercel 对 URL 有一些硬编码的校验,这一块很难饶过,详情参见 ISSUE#123 (opens in a new tab)。
但根据上面的错误提示,可以导入 createClient()
方法进行尝试,于是修改代码 scripts/seed.js
如下所示:
const { db, createClient } = require('@vercel/postgres');
async function main() {
const client = await createClient({ connectionString: process.env.POSTGRES_URL })
await client.connect();
// ...
}
尝试更改之后又报错了,这报错信息让人也很不理解,就本地连接个数据库,为什么还需要链接 443 端口?
Error: connect ECONNREFUSED 127.0.0.1:443
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1570:16) Emitted 'error' event on WebSocket instance at:
at ClientRequest.emit (node:events:511:28)
at TLSSocket.socketErrorListener (node:_http_client:495:9)
at TLSSocket.emit (node:events:511:28)
at emitErrorNT (node:internal/streams/destroy:151:8)
at emitErrorCloseNT (node:internal/streams/destroy:116:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 443
}
这是因为在底层,Vercel Postgres 连接器使用 WebSocket 连接。createClient()
返回的 client 实例是来自 node-postgres (opens in a new tab)
模块,但是 PostgreSQL 本身并不支持 WebSocket。
除了运行本地数据库还要运行一个代理,这里有一篇文章介绍 https://gal.hagever.com/posts/running-vercel-postgres-locally (opens in a new tab) 。 但这种方式对本地开发不是太友好,没有一个清晰的步骤来介绍怎么使用。
在这些问题上浪费了不少时间。最后,决定采用 pg 库,按照 Learn Next.js 教程的使用示例,做了一些修改。
seed 脚本中使用本地数据库 Postgres
安装 pg 模块:yarn add pg
。
创建 /scripts/pg-local.js
文件。
注意:因为 Vercel Postgres 并没有提供 "sql``" 这样模版字符串的方式来根据 SQL 内容查询数据,因此,我们这里也需要做些修改,来适配 Learn Next.js 教程示例中的写法。
const { Client } = require('pg');
const client = new Client(process.env.POSTGRES_URL || "postgres://postgresUser:postgresPW@127.0.0.1:5432/postgresDB");
exports.getClient = async () => {
if (!client._connected) {
await client.connect();
}
// 适配这样的语句查询数据:client.sql`SHOW TIME ZONE;`
client.sql = async (strings, ...values) => {
if (!strings) {
throw new ('sql is required')
}
const [query, params] = sqlTemplate(strings, ...values)
const res = await client.query(query, params);
return res;
}
return client;
}
function sqlTemplate(strings, ...values) {
if (!isTemplateStringsArray(strings) || !Array.isArray(values)) {
throw new Error(
'incorrect_tagged_template_call',
"It looks like you tried to call `sql` as a function. Make sure to use it as a tagged template.\n\tExample: sql`SELECT * FROM users`, not sql('SELECT * FROM users')",
);
}
let result = strings[0] ?? '';
for (let i = 1; i < strings.length; i++) {
result += `$${i}${strings[i] ?? ''}`;
}
return [result, values];
}
function isTemplateStringsArray(strings) {
return (
Array.isArray(strings) && 'raw' in strings && Array.isArray(strings.raw)
);
}
// (async () => {
// // Test script
// try {
// const clientInstance = await exports.getClient();
// const res = await clientInstance.sql`SHOW TIME ZONE;`
// console.log(res.rows[0].TimeZone) // 'Etc/UTC'
// } catch (err) {
// console.error(err);
// } finally {
// await client.end()
// }
// })();
在 seed 脚本文件 /scripts/seed.js
中新增环境变量 LOCAL_VERCEL_POSTGRES 判断逻辑,如果是本地 postgres 数据库
调用我们刚写的 getClient()
方法获取 client 实例,否则还是使用 Vercel Postgres
提供的 client 实例。
const { db } = require('@vercel/postgres');
const { getClient } = require('./pg-local');
// ...
async function main() {
const client = process.env.LOCAL_VERCEL_POSTGRES ? await getClient() : await db.connect();
await seedUsers(client);
await seedCustomers(client);
await seedInvoices(client);
await seedRevenue(client);
await client.end();
}
业务代码中使用本地数据库 Postgres
与 seed 脚本不同,Learn Next.js 教程中的其余代码都采用的 TypeScript 写法,因此我们还需要在写一个 TS 版本。
这里要使用的链接池,这里使用 pg
模块的 Pool
类创建链接池实例,详情参见 Pooling (opens in a new tab)
创建 /app/lib/pg-local.ts
文件。
import { Pool } from 'pg';
import type {
QueryResult,
QueryResultRow,
} from '@neondatabase/serverless';
const connectionString = process.env.POSTGRES_URL;
const pool = new Pool({
connectionString,
})
export async function sql<O extends QueryResultRow>(
strings: TemplateStringsArray,
...values: Primitive[]
): Promise<QueryResult<O>> {
const [query, params] = sqlTemplate(strings, ...values);
// @ts-ignore
const res = await pool.query(query, params);
// @ts-ignore
return res as unknown as Promise<QueryResult<O>>;
}
export type Primitive = string | number | boolean | undefined | null;
export function sqlTemplate(
strings: TemplateStringsArray,
...values: Primitive[]
): [string, Primitive[]] {
if (!isTemplateStringsArray(strings) || !Array.isArray(values)) {
throw new Error("It looks like you tried to call `sql` as a function. Make sure to use it as a tagged template.\n\tExample: sql`SELECT * FROM users`, not sql('SELECT * FROM users')");
}
let result = strings[0] ?? '';
for (let i = 1; i < strings.length; i++) {
result += `$${i}${strings[i] ?? ''}`;
}
return [result, values];
}
function isTemplateStringsArray(
strings: unknown,
): strings is TemplateStringsArray {
return (
Array.isArray(strings) && 'raw' in strings && Array.isArray(strings.raw)
);
}
创建 /app/lib/sql-hack.ts
文件。根据环境变量做区分,本地开发时使用本地的 postgres 数据库。
import { sql as vercelSql } from '@vercel/postgres';
import { sql as pgLocalSql } from './pg-local';
export const sql = process.env.LOCAL_VERCEL_POSTGRES ? pgLocalSql : vercelSql
修改 /app/lib/data.ts
文件。
import { sql } from './sql-hack';
请注意:以上是一个 hack 的解决方案,只适用解决本教程示例中遇到的问题。如果选用 Next.js 做开发时,推荐关注一些 ORM 框架,例如 Prisma 还是很好用的,这不是这篇教程的重点,这里不会展开介绍。
声明
之前,有网友表示,这个项目挺好的,对他有帮助,来表示感谢!
在此声明下,本项目只是一个中文翻译版本,纯粹的 “为爱发电了”,原版权仍归 Next.js 官方教程所有,本翻译项目无任何商业行为,也不接受任何金钱上的捐赠哈。 如果真的感觉有帮助,就点个 Star 吧,关注下 “编程界” 公众号,上面平常也在分享 Next.js 相关内容。
扫码备注「nextjs」
加入 Next.js 中文技术交流群
关注公众号「编程界」
获取最新 Next.js 开发资讯