跳到主要內容

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 查詢中進行高階 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 型別的模型不公開寫操作,例如 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 操作進行 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 的任何方式。

.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 語句進行 Linting 和問題高亮。

我們的示例依賴於 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
}

接下來,您可以向 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

這也適用於列名 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 的型別轉換從 location::geometry 更改為僅 location,那麼在 ST_XST_YST_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 操作。

© . This site is unofficial and not affiliated with Prisma Data, Inc.