Prisma + Webpack + Docker 踩坑筆記
Preface
前陣子為了其他系列的部落格文章的 lab,在練習一個簡單的 REST-ful API 的專案
途中遇到不少的困難,想說寫起來紀錄一下
用到的 tech stack 如題所述,稍微簡介一下
Prisma 是一款專為 Node.js 而誕生的 ORM 套件,透過 ORM 工具可以讓你輕鬆的序列化資料,不用自己硬刻轉換資料的部份
Webpack 則是負責將所有的檔案 “打包” 的一項工具
當然還有我們的老朋友 Docker
Environment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ npx prisma version
prisma : 4.16.2
@prisma/client : 4.16.2
Current platform : debian-openssl-3.0.x
Query Engine (Node-API) : libquery-engine 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 (at node_modules/@prisma/engines/libquery_engine-debian-openssl-3.0.x.so.node)
Migration Engine : migration-engine-cli 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 (at node_modules/@prisma/engines/migration-engine-debian-openssl-3.0.x)
Format Wasm : @prisma/prisma-fmt-wasm 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
Default Engines Hash : 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
Studio : 0.484.0
$ npx webpack version
System:
OS: Linux 5.19 Ubuntu 22.04.2 LTS 22.04.2 LTS (Jammy Jellyfish)
CPU: (12) x64 AMD Ryzen 5 2600 Six-Core Processor
Memory: 22.04 GB / 31.28 GB
Binaries:
Node: 18.15.0 - /usr/local/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 9.5.0 - /usr/local/bin/npm
Browsers:
Chrome: 114.0.5735.133
Packages:
copy-webpack-plugin: ^11.0.0 => 11.0.0
dotenv-webpack: 8.0.1 => 8.0.1
node-polyfill-webpack-plugin: ^2.0.1 => 2.0.1
ts-loader: ^9.4.4 => 9.4.4
webpack: ^5.88.1 => 5.88.1
webpack-cli: ^5.1.4 => 5.1.4
webpack-obfuscator: ^3.5.1 => 3.5.1
$ docker -v
Docker version 24.0.2, build cb74dfc
How does Prisma Work
開發者透過定義 schema.prisma 檔案,定義資料庫的 table 結構,內容大概會長這樣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id
username String
created_at DateTime @default(now())
last_login_at DateTime @updatedAt
RoomMember RoomMember[]
Message Message[]
}
...
主要就是三個部份
- datasource
- 定義你用哪一種資料庫(e.g.
mysql
,postgresql
),然後他的 URL
- 定義你用哪一種資料庫(e.g.
- generator
- 為了要生成對應的 typing(for TypeScript)
- 畫個重點,這裡很重要,跟 Webpack 有關
- model
- 最後這裡就是定義 Table schema, 你可以定義多個 model
這時候,資料庫並沒有這些定義
所以要想辦法同步進去
由於 Prisma 本身是透過 JavaScript client 進行操作的(詳見 Prisma Architecture)
所以要先將客戶端生成出來
1
$ npx prisma generate --schema schema.prisma
generate 會在
npm i
的時候自動執行
然後就要同步 schema 了
1
$ npx prisma migrate dev --name init --schema schema.prisma
注意到一點,當你 migration 完成之後
migration 的 history 檔案也務必要加入版控裡面
預設定義檔路徑是 ./schema.prisma
如果你放在別的地方要指過去
到這裡,基本上你就設定完成了
然而,事情才剛剛開始
Prisma Architecture
在眾多 Node.js ORM 框架裡,Prisma 是稍微年輕的後起之秀
而他的架構,是由 client 以及 server 所組成的,如下所示
Prisma 透過 JavaScript client 與 Query Engine 進行溝通,然後才到資料庫進行查詢
npx prisma generate 這行指令,上一節才看到,它會負責生成 client 以及 engine
注意到,query engine 是 binary(aka. 執行檔)
它會根據你目前的系統,自動下載相對應的 binary 到 node_modules/@prisma/engines
以及 node_modules/.prisma/client
裡頭
@prisma/client prisma module 本體,下載後就不會改動了
.prisma/client 根據你的 schema.prisma 動態生成的
檔案名稱的規則為
Prefix | Platform | Postfix | Image |
---|---|---|---|
libquery_engine- |
Platform list | .so.node |
|
query-engine- |
Platform list |
Bundle all Source Code
當你完成你的程式碼,並且將他們打包的時候
你會意識到一個問題,在 Prisma Architecture 裡面有提到,Prisma 有一個 query engine 的 binary
而一般情況下,webpack 不會處理它,你需要手動將 binary 打包起來
這時候你需要 copy-webpack-plugin
copy-webpack-plugin
透過 npm 安裝
1
$ npm i -D copy-webpack-plugin
webpack.config.js 裡面加入 plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: "./src/database/prisma/schema.prisma",
to: "./schema.prisma",
},
{
from: path.join(
__dirname,
"./node_modules/.prisma/client/query-engine-linux-musl-openssl-3.0.x"
),
to: "./query-engine-linux-musl-openssl-3.0.x",
},
{
from: path.join(
__dirname,
"./node_modules/.prisma/client/query-engine-debian-openssl-3.0.x"
),
to: "./query-engine-debian-openssl-3.0.x",
},
],
}),
]
}
copy-webpack-plugin 基本用法就是這樣,複製某個檔案到某個位置
prisma 初始化的時候會跑兩個指令(generate
以及 migrate
)
所以 schema 的定義檔也必須要複製進去,再來就是 query engine 的 binary 檔案
細心的你發現到,怎麼這裡複製的 binary 位置跟 Prisma Architecture 裡面講的不一樣?
prisma 預設會幫你下載跟系統一致的 binary(e.g. libquery_engine-debian-openssl-3.0.x.so.node
) 要用它自然也是沒問題的
只不過我的例子是開發環境跟正式環境所使用的系統不一樣(ubuntu
以及 alpine
)
prisma 提供了一個選項,可以指定你要用的 binary 有哪些
所以設定檔可以改成這樣寫
1
2
3
4
5
generator client {
provider = "prisma-client-js"
binaryTargets = ["linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
engineType = "binary"
}
其中,binaryTargets 可以在 Platform list 找到
然後切記 engineType 也要設定成 binary, 不然它會抓不到
這裡我有兩個 binary targets, 分別對應到
ubuntu 22.04
以及alpine
你可以指定多個 binary, 它會自己抓合適的使用
而你指定的 binary, 會被下載到 node_modules/.prisma/client
裡面,如圖所示
Module not found: Error: Can’t resolve ‘fs’
webpack 5 以上,當你在打包的時候可能噴一堆 error 說它找不到 fs
, http
, crypto
… etc.
一個簡單的作法是使用 node-polyfill-webpack-plugin plugin
你的 webpack.config.js 就會變成以下
1
2
3
4
5
6
7
const NodePolyfillPlugin = const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
module.export = {
plugins: [
new NodePolyfillPlugin()
]
}
但是它會造成一點問題,可以參考 TypeError: argument entity must be string, Buffer, or fs.Stats
有一個更簡單的方法,只要將 target
設為 node
即可
1
2
3
4
5
// webpack.config.js
module.export = {
target: 'node',
}
TypeError: argument entity must be string, Buffer, or fs.Stats
如果你跑起來,有遇到
1
2
3
4
5
6
TypeError: argument entity must be string, Buffer, or fs.Stats
at etag (/[...]/node_modules/etag/index.js:83:11)
at generateETag ([...]/node_modules/express/lib/utils.js:280:12)
at ServerResponse.send ([...]/node_modules/express/lib/response.js:200:17)
at ServerResponse.json ([...]/node_modules/express/lib/response.js:267:15)
at api.post (/the/code/above)
這個問題,是因為你在 webpack.config.js 裡面用了 node-polyfill-webpack-plugin plugin
把它移除之後就可以了
1
2
3
4
5
module.export = {
plugins: [
// new NodePolyfillPlugin()
]
}
Prisma Migration Inside Docker Container
將 application 容器化算是一個好習慣吧,至少對我來說這樣可以很方便的測試
但是碰上 Prisma 整體會稍微麻煩一點,且聽我娓娓道來
$ npx prisma migrate
的功用還記得嗎? 就是將 table schema 同步到資料庫裡面
可是這行指令必須在你的 application container 裡面執行才可以(因為它需要 @prisma/client
以及你的 schema)
現今主流 container 的作法是把 app 跟 database 拆開
問題來了,你要怎麼同步 schema?
還有一點是,我們在 schema.prisma
裡面定義 URL 的時候是採環境變數的方式
1
2
3
4
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
要怎麼處理這塊也是一個問題
關於第二點其實如果你不用 environment variable 的寫法也不是不行
可以維護兩份不一樣的 schema 定義,在下 command 的時候指到不同的檔案
也可以 work 但顯然這樣有點蠢
Prisma Migration History
每一次你執行 $ npx prisma migrate
的時候,它會生成一個 sql
檔,存放於 ./migrations
裡面紀錄了每一次 migration 的 sql 檔
既然我有 .sql
那不用手動執行 $ npx prisma migrate
也沒差,還更省事
假設你用的 image 是 mariadb
有一個貼心的功能是,當資料庫 boot 的時候,它會自動執行所有放在 /docker-entrypoint-initdb.d
底下的檔案們(e.g. .sql
, 可參考 Initializing a fresh instance)
因此你可以這樣做
- 複製最新的 migration sql 檔案
- 客製化 docker image, 將 sql 檔案塞入
/docker-entrypoint-initdb.d
如此一來,不需要手動執行指令,也可以初始化資料庫 table 了
1
2
3
4
5
6
7
8
9
10
11
USE restdb;
-- CreateTable
CREATE TABLE `User` (
`id` VARCHAR(191) NOT NULL,
`username` VARCHAR(191) NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `User_username_key`(`username`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
注意到 prisma 生成的 sql 裡面沒有指定資料庫
你需要手動指定(USE xxx
)
1
2
FROM mariadb:latest
COPY ./init.sql /docker-entrypoint-initdb.d/
另外如果你需要初始化資料 .csv 之類的檔案
記得放在 /var/lib/mysql 以外 的地方,由於權限問題,docker 沒辦法複製到這裡面
process.env undefined in Node Docker Image
同樣也是跟 prisma 有點相關的慘劇
1
2
3
4
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
除了 prisma 的環境變數,你可能有其他的環境變數設定要載入
不知道為啥,即使在 docker-compose.yaml 當中有設定環境變數,進去 container 裡面也看得到
但 application 就是啥都沒有
我原本用的 dotenv 它只會從檔案裡面讀取 .env
研判是這個導致的問題,因為在打包的過程中我並沒有將設定檔一併帶入,取而代之的是在 docker-compose.yaml 裡面定義
dotenv-webpack
後來我找到一款第三方的 webpack plugin dotenv-webpack
它可以讀取系統層級的環境變數,並載入使用
安裝也如同先前
1
$ npm i -D dotenv-webpack
webpack.config.js 可以改成以下
1
2
3
4
5
6
7
8
9
10
const Dotenv = require("dotenv-webpack");
module.export = {
plugins: [
new Dotenv({
systemvars: true,
path: path.join(__dirname, "./.env"),
}),
]
}
除了載入 .env
之外,也將系統層級的環境變數寫入 process.env
裡面
這樣就可以 work 了
Example
有關上述所有的程式碼實作,你可以在 ambersun1234/blog-labs/cursor-based-pagination 找到
Leave a comment