SafeQL & Prisma Client
概述
本頁解釋瞭如何改善在 Prisma ORM 中編寫原始 SQL 的體驗。它使用Prisma Client 擴充套件和SafeQL來建立自定義的、型別安全的 Prisma Client 查詢,這些查詢能夠抽象應用程式可能需要的自定義 SQL(使用 $queryRaw)。
本示例將使用PostGIS和 PostgreSQL,但適用於應用程式中可能需要的任何原始 SQL 查詢。
什麼是 SafeQL?
SafeQL允許在原始 SQL 查詢中進行高階 Linting 和型別安全檢查。設定完成後,SafeQL 可與 Prisma Client 的 $queryRaw 和 $executeRaw 配合使用,在需要原始查詢時提供型別安全。
SafeQL 作為 ESLint 外掛執行,並使用 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 操作進行 Linting。您可以參考 SafeQL 的整合指南 或按照以下步驟操作。
3.1. 安裝 @ts-safeql/eslint-plugin npm 包
npm install -D @ts-safeql/eslint-plugin libpg-query
這個 ESLint 外掛將允許對查詢進行 Linting。
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 語句進行 Linting 和問題高亮。
我們的示例依賴於 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
}
接下來,您可以向 Prisma Client 的 pointOfInterest 屬性新增一個 create 查詢。
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 的型別轉換從 location::geometry 更改為僅 location,那麼在 ST_X、ST_Y 或 ST_DistanceSphere 函式中就會出現 Linting 錯誤。
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 操作。