メインコンテンツへスキップ

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型を持つモデルは、createupdateのような書き込み操作を公開しません。

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

参考までに、マイグレーションファイルの出力は次のようになります。

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は、$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の設定方法に応じて適用できます。

.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を設定して、データベースをイントロスペクトし、スキーマで使用するテーブル名とカラム名を取得できるようにします。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クエリを型安全にするための拡張機能を作成する

このセクションでは、PointOfInterestモデルを便利に操作できるように、カスタムクエリを持つ2つのmodel拡張機能を作成します。

  1. データベースに新しいPointOfInterestレコードを作成できるcreateクエリ
  2. 指定された座標に最も近い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に変更してlocationgeometryへのキャストを削除した場合、それぞれST_XST_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クエリを作成しました。

© . All rights reserved.