SafeQLとPrisma Client
概要
このページでは、Prisma ORMで生のSQLを記述する体験を向上させる方法について説明します。Prisma ClientエクステンションとSafeQLを使用して、アプリで必要となるカスタムSQL($queryRaw
を使用)を抽象化する、カスタムの型安全なPrisma Clientクエリを作成します。
この例ではPostGISとPostgreSQLを使用しますが、アプリケーションで必要となるあらゆる生SQLクエリに適用可能です。
このページは、Prisma Clientで利用可能なレガシーな生クエリメソッドに基づいています。Prisma Clientにおける生SQLの多くのユースケースはTypedSQLでカバーされていますが、Unsupported
フィールドを扱う場合は、これらのレガシーメソッドを使用することが依然として推奨されるアプローチです。
SafeQLとは?
SafeQLは、生SQLクエリ内で高度なリンティングと型安全性を提供します。セットアップ後、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. 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操作をLintするために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にconnectionUrl
を設定して、データベースをイントロスペクトし、スキーマで使用するテーブル名とカラム名を取得できるようにします。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クエリを型安全にするための拡張機能を作成する
このセクションでは、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
},
},
},
})
これで、PointOfInterest
モデルに作成されたカスタムメソッドを使用して、指定された経度と緯度に近接する関心地点を通常のPrisma Clientとして検索できます。
const closestPointOfInterest = await prisma.pointOfInterest.findClosestPoints(
53.5488,
9.9872
)
以前と同様に、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を使用する際に生SQLに頼る必要がある場合でも、様々な手法を用いることで、Prisma ORMで生SQLクエリを記述する体験を向上させることができます。
この記事では、SafeQLとPrisma Clientエクステンションを使用して、現在Prisma ORMでネイティブにサポートされていないPostGIS操作を抽象化する、カスタムの型安全なPrisma Clientクエリを作成しました。