SafeQL 與 Prisma Client
概觀
本頁面說明如何改善在 Prisma ORM 中編寫原始 SQL 的體驗。它使用Prisma Client 擴充功能和SafeQL來建立自訂、類型安全的 Prisma Client 查詢,這些查詢抽象化您的應用程式可能需要的自訂 SQL(使用 $queryRaw
)。
此範例將使用PostGIS和 PostgreSQL,但適用於您應用程式中可能需要的任何原始 SQL 查詢。
什麼是 SafeQL?
SafeQL允許在原始 SQL 查詢中進行進階的程式碼檢查和類型安全。設定完成後,SafeQL 會與 Prisma Client 的 $queryRaw
和 $executeRaw
搭配運作,在需要原始查詢時提供類型安全。
SafeQL 作為 ESLint 外掛程式執行,並使用 ESLint 規則進行設定。本指南不涵蓋設定 ESLint,我們假設您已經在專案中執行它。
先決條件
若要繼續操作,您需要具備
- 已安裝 PostGIS 的 PostgreSQL 資料庫
- 在您的專案中設定 Prisma ORM
- 在您的專案中設定 ESLint
Prisma ORM 中的地理資料支援
在撰寫本文時,Prisma ORM 尚不支援處理地理資料,特別是使用 PostGIS。
具有地理資料欄位的模型將使用Unsupported
資料類型儲存。具有 Unsupported
類型的欄位會出現在產生的 Prisma Client 中,並且會被鍵入為 any
。具有必要 Unsupported
類型的模型不會公開寫入操作,例如 create
和 update
。
Prisma Client 支援使用 $queryRaw
和 $executeRaw
在具有必要 Unsupported
欄位的模型上執行寫入操作。您可以使用 Prisma Client 擴充功能和 SafeQL 來提高在原始查詢中使用地理資料時的類型安全性。
1. 設定 Prisma ORM 以搭配 PostGIS 使用
如果您尚未啟用,請啟用 postgresqlExtensions
預覽功能,並在您的 Prisma schema 中新增 postgis
PostgreSQL 擴充功能
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [postgis]
}
如果您未使用託管資料庫供應商,您可能需要安裝 postgis
擴充功能。請參閱 PostGIS 的文件,以了解更多關於如何開始使用 PostGIS 的資訊。如果您使用 Docker Compose,您可以使用以下程式碼片段來設定已安裝 PostGIS 的 PostgreSQL 資料庫
version: '3.6'
services:
pgDB:
image: postgis/postgis:13-3.1-alpine
restart: always
ports:
- '5432:5432'
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: geoexample
volumes:
db_data:
接下來,建立遷移並執行遷移以啟用擴充功能
npx prisma migrate dev --name add-postgis
作為參考,遷移檔案的輸出應如下所示
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "postgis";
您可以執行 prisma migrate status
來再次檢查遷移是否已套用。
2. 建立使用地理資料欄位的新模型
一旦遷移套用,新增具有 geography
資料類型欄位的新模型。在本指南中,我們將使用名為 PointOfInterest
的模型。
model PointOfInterest {
id Int @id @default(autoincrement())
name String
location Unsupported("geography(Point, 4326)")
}
您會注意到 location
欄位使用Unsupported
類型。這表示當使用 PointOfInterest
時,我們會失去 Prisma ORM 的許多優勢。我們將使用SafeQL來修正此問題。
與之前一樣,使用 prisma migrate dev
命令建立並執行遷移,以在您的資料庫中建立 PointOfInterest
表格
npx prisma migrate dev --name add-poi
作為參考,以下是 Prisma Migrate 產生的 SQL 遷移檔案的輸出
-- CreateTable
CREATE TABLE "PointOfInterest" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"location" geography(Point, 4326) NOT NULL,
CONSTRAINT "PointOfInterest_pkey" PRIMARY KEY ("id")
);
3. 整合 SafeQL
SafeQL 可以輕鬆地與 Prisma ORM 整合,以便檢查 $queryRaw
和 $executeRaw
Prisma 操作。您可以參考 SafeQL 的整合指南 或按照以下步驟操作。
3.1. 安裝 @ts-safeql/eslint-plugin
npm 套件
npm install -D @ts-safeql/eslint-plugin
此 ESLint 外掛程式將允許檢查查詢。
3.2. 將 @ts-safeql/eslint-plugin
新增至您的 ESLint 外掛程式
接下來,將 @ts-safeql/eslint-plugin
新增至您的 ESLint 外掛程式清單。在我們的範例中,我們使用 .eslintrc.js
檔案,但這可以應用於您設定 ESLint的任何方式。
/** @type {import('eslint').Linter.Config} */
module.exports = {
"plugins": [..., "@ts-safeql/eslint-plugin"],
...
}
3.3 新增 @ts-safeql/check-sql
規則
現在,設定規則,讓 SafeQL 能夠將無效的 SQL 查詢標記為 ESLint 錯誤。
/** @type {import('eslint').Linter.Config} */
module.exports = {
plugins: [..., '@ts-safeql/eslint-plugin'],
rules: {
'@ts-safeql/check-sql': [
'error',
{
connections: [
{
// The migrations path:
migrationsDir: './prisma/migrations',
targets: [
// This makes `prisma.$queryRaw` and `prisma.$executeRaw` commands linted
{ tag: 'prisma.+($queryRaw|$executeRaw)', transform: '{type}[]' },
],
},
],
},
],
},
}
注意:如果您的
PrismaClient
實例名稱與prisma
不同,您需要相應地調整tag
的值。例如,如果它被稱為db
,則tag
的值應為'db.+($queryRaw|$executeRaw)'
。
3.4. 連線到您的資料庫
最後,為 SafeQL 設定 connectionUrl
,以便它可以內省您的資料庫並檢索您在 schema 中使用的表格和欄位名稱。然後 SafeQL 使用此資訊來檢查程式碼並突出顯示原始 SQL 語句中的問題。
我們的範例依賴 dotenv
套件來取得 Prisma ORM 使用的相同連線字串。我們建議這樣做,以便將您的資料庫 URL 保留在版本控制之外。
如果您尚未安裝 dotenv
,您可以按如下方式安裝它
npm install dotenv
然後按如下方式更新您的 ESLint 設定
require('dotenv').config()
/** @type {import('eslint').Linter.Config} */
module.exports = {
plugins: ['@ts-safeql/eslint-plugin'],
// exclude `parserOptions` if you are not using TypeScript
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@ts-safeql/check-sql': [
'error',
{
connections: [
{
connectionUrl: process.env.DATABASE_URL,
// The migrations path:
migrationsDir: './prisma/migrations',
targets: [
// what you would like SafeQL to lint. This makes `prisma.$queryRaw` and `prisma.$executeRaw`
// commands linted
{ tag: 'prisma.+($queryRaw|$executeRaw)', transform: '{type}[]' },
],
},
],
},
],
},
}
SafeQL 現在已完全設定,可協助您使用 Prisma Client 編寫更好的原始 SQL。
4. 建立擴充功能以使原始 SQL 查詢類型安全
在本節中,我們將建立兩個model
擴充功能,其中包含自訂查詢,以便能夠方便地使用 PointOfInterest
模型
- 一個
create
查詢,允許我們在資料庫中建立新的PointOfInterest
記錄 - 一個
findClosestPoints
查詢,傳回最接近給定座標的PointOfInterest
記錄
4.1. 新增擴充功能以建立 PointOfInterest
記錄
Prisma schema 中的 PointOfInterest
模型使用 Unsupported
類型。因此,Prisma Client 中產生的 PointOfInterest
類型不能用於攜帶緯度和經度的值。
我們將透過定義兩個自訂類型來解決此問題,這些類型可以更好地表示 TypeScript 中的模型
type MyPoint = {
latitude: number
longitude: number
}
type MyPointOfInterest = {
name: string
location: MyPoint
}
接下來,您可以將 create
查詢新增至 Prisma Client 的 pointOfInterest
屬性
const prisma = new PrismaClient().$extends({
model: {
pointOfInterest: {
async create(data: {
name: string
latitude: number
longitude: number
}) {
// Create an object using the custom types from above
const poi: MyPointOfInterest = {
name: data.name,
location: {
latitude: data.latitude,
longitude: data.longitude,
},
}
// Insert the object into the database
const point = `POINT(${poi.location.longitude} ${poi.location.latitude})`
await prisma.$queryRaw`
INSERT INTO "PointOfInterest" (name, location) VALUES (${poi.name}, ST_GeomFromText(${point}, 4326));
`
// Return the object
return poi
},
},
},
})
請注意,程式碼片段中突出顯示的行中的 SQL 會由 SafeQL 檢查!例如,如果您將表格名稱從 "PointOfInterest"
變更為 "PointOfInterest2"
,則會出現以下錯誤
error Invalid Query: relation "PointOfInterest2" does not exist @ts-safeql/check-sql
這也適用於欄位名稱 name
和 location
。
您現在可以按如下方式在程式碼中建立新的 PointOfInterest
記錄
const poi = await prisma.pointOfInterest.create({
name: 'Berlin',
latitude: 52.52,
longitude: 13.405,
})
4.2. 新增擴充功能以查詢最接近的 PointOfInterest
記錄
現在讓我們建立一個 Prisma Client 擴充功能,以便查詢此模型。我們將建立一個擴充功能,用於尋找最接近給定經度和緯度的興趣點。
const prisma = new PrismaClient().$extends({
model: {
pointOfInterest: {
async create(data: {
name: string
latitude: number
longitude: number
}) {
// ... same code as before
},
async findClosestPoints(latitude: number, longitude: number) {
// Query for clostest points of interests
const result = await prisma.$queryRaw<
{
id: number | null
name: string | null
st_x: number | null
st_y: number | null
}[]
>`SELECT id, name, ST_X(location::geometry), ST_Y(location::geometry)
FROM "PointOfInterest"
ORDER BY ST_DistanceSphere(location::geometry, ST_MakePoint(${longitude}, ${latitude})) DESC`
// Transform to our custom type
const pois: MyPointOfInterest[] = result.map((data) => {
return {
name: data.name,
location: {
latitude: data.st_x || 0,
longitude: data.st_y || 0,
},
}
})
// Return data
return pois
},
},
},
})
現在,您可以像平常一樣使用我們的 Prisma Client,以使用在 PointOfInterest
模型上建立的自訂方法,尋找接近給定經度和緯度的興趣點。
const closestPointOfInterest = await prisma.pointOfInterest.findClosestPoints(
53.5488,
9.9872
)
與之前類似,我們再次獲得 SafeQL 的好處,為我們的原始查詢新增額外的類型安全。例如,如果我們移除 location
的 geometry 轉換,將 location::geometry
變更為僅 location
,我們將分別在 ST_X
、ST_Y
或 ST_DistanceSphere
函數中收到程式碼檢查錯誤。
error Invalid Query: function st_distancesphere(geography, geometry) does not exist @ts-safeql/check-sql
結論
雖然有時您可能需要在使用 Prisma ORM 時降級到原始 SQL,但您可以使用各種技術來改善使用 Prisma ORM 編寫原始 SQL 查詢的體驗。
在本文中,您已使用 SafeQL 和 Prisma Client 擴充功能來建立自訂、類型安全的 Prisma Client 查詢,以抽象化目前 Prisma ORM 本身不支援的 PostGIS 操作。