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

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

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

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

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操作をリントするために、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がデータベースをイントロスペクトし、スキーマで使用するテーブル名と列名を取得できるように、SafeQLのconnectionUrlを設定します。SafeQLは、この情報を使用して、raw SQLステートメントの問題をリントおよび強調表示します。

この例では、Prisma ORMで使用されているのと同じ接続文字列を取得するために、dotenvパッケージに依存しています。データベース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を使用してより適切なraw SQLを作成するのに役立ちます。

4. raw 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
},
},
},
})

これで、Prisma Clientを通常どおり使用して、PointOfInterestモデルで作成されたカスタムメソッドを使用して、指定された経度と緯度に最も近い関心のあるポイントを見つけることができます。

const closestPointOfInterest = await prisma.pointOfInterest.findClosestPoints(
53.5488,
9.9872
)

以前と同様に、rawクエリに型安全性を追加する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を使用しているときにraw SQLにドロップダウンする必要がある場合がありますが、さまざまな手法を使用して、Prisma ORMでraw SQLクエリを作成するエクスペリエンスを向上させることができます。

この記事では、SafeQLとPrisma Client拡張機能を使用して、現在Prisma ORMでネイティブにサポートされていないPostGIS操作を抽象化するための、カスタムの型安全なPrisma Clientクエリを作成しました。