跳到主要內容

SafeQL 與 Prisma Client

概觀

本頁面說明如何改善在 Prisma ORM 中編寫原始 SQL 的體驗。它使用Prisma Client 擴充功能SafeQL來建立自訂、類型安全的 Prisma Client 查詢,這些查詢抽象化您的應用程式可能需要的自訂 SQL(使用 $queryRaw)。

此範例將使用PostGIS和 PostgreSQL,但適用於您應用程式中可能需要的任何原始 SQL 查詢。

請注意

本頁面建立在 Prisma Client 中可用的舊版原始查詢方法之上。雖然 Prisma Client 中原始 SQL 的許多用例都由TypedSQL涵蓋,但對於處理 Unsupported 欄位,使用這些舊版方法仍然是建議的方法。

什麼是 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 類型的模型不會公開寫入操作,例如 createupdate

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

作為參考,遷移檔案的輸出應如下所示

migrations/TIMESTAMP_add_postgis/migration.sql
-- 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 遷移檔案的輸出

migrations/TIMESTAMP_add_poi/migration.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的任何方式。

.eslintrc.js
/** @type {import('eslint').Linter.Config} */
module.exports = {
"plugins": [..., "@ts-safeql/eslint-plugin"],
...
}

3.3 新增 @ts-safeql/check-sql 規則

現在,設定規則,讓 SafeQL 能夠將無效的 SQL 查詢標記為 ESLint 錯誤。

.eslintrc.js
/** @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 設定

.eslintrc.js
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 模型

  1. 一個 create 查詢,允許我們在資料庫中建立新的 PointOfInterest 記錄
  2. 一個 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

這也適用於欄位名稱 namelocation

您現在可以按如下方式在程式碼中建立新的 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_XST_YST_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 操作。