SafeQLとPrisma Client
概要
このページでは、Prisma ORMで生のSQLを記述するエクスペリエンスを向上させる方法について説明します。ここでは、Prisma Client拡張機能とSafeQLを使用して、アプリケーションが必要とする可能性のあるカスタムSQL($queryRaw
を使用)を抽象化する、カスタムの型安全なPrisma Clientクエリを作成します。
この例では、PostGISとPostgreSQLを使用しますが、アプリケーションで必要となる可能性のある生のSQLクエリに適用できます。
このページは、Prisma Clientで利用可能な従来のrawクエリメソッドを基に構築されています。Prisma Clientでのraw SQLの多くのユースケースはTypedSQLでカバーされていますが、これらのレガシーメソッドを使用することは、Unsupported
フィールドを扱うための推奨されるアプローチです。
SafeQLとは?
SafeQLを使用すると、raw SQLクエリ内で高度なリントと型安全性を実現できます。セットアップ後、SafeQLはPrisma Clientの$queryRaw
および$executeRaw
と連携して、rawクエリが必要な場合に型安全性を提供します。
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を使用して、rawクエリで地理データを扱う際の型安全性を向上させることができます。
1. PostGISで使用するためのPrisma ORMのセットアップ
まだ行っていない場合は、postgresqlExtensions
プレビュー機能を有効にし、Prismaスキーマに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は、$queryRaw
および$executeRaw
Prisma操作をリントするために、Prisma ORMと簡単に統合できます。SafeQLの統合ガイドを参照するか、以下の手順に従ってください。
3.1. @ts-safeql/eslint-plugin
npmパッケージのインストール
npm install -D @ts-safeql/eslint-plugin libpg-query
このESLintプラグインは、クエリのリントを可能にするものです。
3.2. ESLintプラグインへの@ts-safeql/eslint-plugin
の追加
次に、ESLintプラグインのリストに@ts-safeql/eslint-plugin
を追加します。この例では、.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がデータベースをイントロスペクトし、スキーマで使用するテーブル名と列名を取得できるように、SafeQLのconnectionUrl
を設定します。SafeQLは、この情報を使用して、raw SQLステートメントの問題をリントおよび強調表示します。
この例では、Prisma ORMで使用されているのと同じ接続文字列を取得するために、dotenv
パッケージに依存しています。データベース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を使用してより適切なraw SQLを作成するのに役立ちます。
4. raw SQLクエリを型安全にするための拡張機能の作成
このセクションでは、PointOfInterest
モデルを便利に操作できるように、カスタムクエリを備えた2つのmodel
拡張機能を作成します。
- データベースに新しい
PointOfInterest
レコードを作成できるcreate
クエリ - 指定された座標に最も近い
PointOfInterest
レコードを返すfindClosestPoints
クエリ
4.1. PointOfInterest
レコードを作成するための拡張機能の追加
PrismaスキーマのPointOfInterest
モデルは、Unsupported
型を使用しています。その結果、Prisma Clientで生成されたPointOfInterest
型は、緯度と経度の値を保持するために使用できません。
TypeScriptでモデルをより適切に表現する2つのカスタム型を定義することで、これを解決します。
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
)
以前と同様に、rawクエリに型安全性を追加するSafeQLの利点が再び得られます。たとえば、location::geometry
を単にlocation
に変更して、location
のgeometry
へのキャストを削除した場合、ST_X
、ST_Y
、またはST_DistanceSphere
関数でそれぞれリントエラーが発生します。
error Invalid Query: function st_distancesphere(geography, geometry) does not exist @ts-safeql/check-sql
結論
Prisma ORMを使用しているときにraw SQLにドロップダウンする必要がある場合がありますが、さまざまな手法を使用して、Prisma ORMでraw SQLクエリを作成するエクスペリエンスを向上させることができます。
この記事では、SafeQLとPrisma Client拡張機能を使用して、現在Prisma ORMでネイティブにサポートされていないPostGIS操作を抽象化するための、カスタムの型安全なPrisma Clientクエリを作成しました。