diff --git a/article/tech/sveltekit-db-build-error.md b/article/tech/sveltekit-db-build-error.md new file mode 100644 index 0000000..3ae1cfd --- /dev/null +++ b/article/tech/sveltekit-db-build-error.md @@ -0,0 +1,119 @@ +--- +title: SvelteKitでDB(Postgres)を使ったプロジェクトをビルドするとエラーが出る +released_at: 2024-09-03T17:58:00+09:00 +updated_at: +tags: + - Svelte + - SvelteKit + - Web +image: /uploads/error.png +publish: public +--- + + +# ビルド時にエラー +--- +devでは問題ないのに、buildして確認するとエラーが出る..という問題の解決方法です。 +Postgres以外にもこの手のエラーはデータベースを使っていれば発生する可能性がありそうです。 +## 症状 +ビルド時に、DBに接続できなかったときによく見るエラーが発生する症状です。 +リクエスト時に出るならわかるけど、なんでビルド時...?ってなる。 +``` +AggregateError [ECONNREFUSED]: + at /home/hare/Documents/blog.hareworks.net/node_modules/pg-pool/index.js:45:11 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + at async Postgres.new (file:///home/hare/Documents/blog.hareworks.net/.svelte-kit/output/server/chunks/database.js:153:18) + at async file:///home/hare/Documents/blog.hareworks.net/.svelte-kit/output/server/chunks/database.js:175:12 { + code: 'ECONNREFUSED', + [errors]: [ + Error: connect ECONNREFUSED ::1:5432 + at createConnectionError (node:net:1646:14) + at afterConnectMultiple (node:net:1676:16), + Error: connect ECONNREFUSED 127.0.0.1:5432 + at createConnectionError (node:net:1646:14) + at afterConnectMultiple (node:net:1676:16) + ] +} +``` + +# 原因 +--- +SvelteKitはページをプリレンダリングするために、ビルド時にページのコードを実行します。 +リクエスト時に初めてモジュールが評価される場合でも、ページの`+page.server.ts`からモジュールを利用していると、モジュールの初期化によって接続が要求されエラーとなります。 + +## だめな実装 +```ts: database.ts +import { PG_USER, PG_PASS, PG_HOST, PG_PORT, PG_DB, } from '$env/static/private' const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`; const pool = new Pool({ connectionString }); + +export default async () => { return await pool.connect();} +``` + +```ts: +page.server.ts +import PG from '$lib/server/database'; +const db = await PG() +await db.query("hogehoge...") +``` + +# 解決策 +--- +`hook.server.ts`を利用します。 +Hooksを使うことで、リクエストに対してカスタムデータをリクエストに追加し、ページのload関数に渡すことができます。 +https://kit.svelte.dev/docs/hooks + +ここでは、プールを使った実装で、プールを取得するモジュールを定義し、そのプールを返す関数としてexportします。 +hookを用いてリクエスト時にデータを渡してやることで、load関数から`locals.db`としてpoolにアクセスすることができます。 +```ts: lib/server/database/get_connection.ts +import pg from 'pg'; +const { Pool } = pg; + +import { PG_USER, PG_PASS, PG_HOST, PG_PORT, PG_DB, } from '$env/static/private' const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`; +const pool = new Pool({ connectionString }); + +export const getConnection = () => pool; + +// DB初期化の処理 +``` + +```ts: hook.server.ts +import type { Handle } from '@sveltejs/kit' +import { getConnection } from '$lib/server/database/get_connection' + +export const handle: Handle = async ({ event, resolve }) => { + // dbを渡してやる + const pg = getConnection(); + event.locals.db = pg; + + const result = await resolve(event); + return result; +} +``` + +```ts: +page.server.ts +export const load: PageServerLoad = async ({ params, locals }) => { + // locals.dbでアクセス + const client = locals.db.connect(); + // ... +} +``` + +これによって、リクエスト時にはじめてgetConnection関数が叩かれ、初期化諸々が走るということになります。 +パフォーマンスについても、初回アクセス時にモジュールが評価されてからPoolオブジェクトをそのまま返す関数が毎回走るだけなので、全く問題無いと考えています。 + +## 別解: ビルド時か否かの判定をする +`$app/environment`にある`building`フラグを用いて判定することができます。 +```ts +import { building } from '$app/environment'; +let client: PoolClient | null = null; +if (!building) { + client = pool.connect(); + // ... +} +``` +こんな感じでビルド中ではなければ実行するみたいなことができます。 +しかし、DBの初期化時に一回だけ実行されるなら問題にはなりませんが、様々なページからDBにアクセスしようとするたびにこんな調子では面倒ですし、適当なクラスでラッパーしようにも結局場合分け等が煩わしいことになります。 + +# まとめ +--- +devでは期待通りに動くのに、いざbuildとなるとエラーを出されて悲しくなりました。 +フレームワークの仕組みとしては合理的なので、文句も言えないのですが。 \ No newline at end of file