🗺️

おれおれ流 PrismaでGeometryを扱う方法

2024/05/02に公開

最近 Prisma を使ってバックエンド開発をしてるのだ.Geometry 型を取り扱うために色々調べたからまとめるのだ.
結論は これ なのだ

1. 公式が紹介している方法

ここのページで公式が紹介してる方法なのだ.

// ↑のページより引用
const prisma = new PrismaClient().$extends({
  model: {
    // エンティティ名
    pointOfInterest: {
      // prisma.pointOfInterest.create() がこれに置き換わる
      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
      },
    },
  },
})

PrismaClient を拡張して実装する方法なんだけど,これしたら prisma.foo.create() とかするときに IDE のサジェストが動かなくなっちゃうのだ.
たぶん原因は $extends() で全エンティティの create()update()なんかのメソッドが型システム上は上書きされてるからだと思うのだ.

却下なのだ.

2. Supabase Client Library を使う

この記事 とかで紹介されてるやつなのだ.
Prisma を使わないならこの方法で良いと思うのだ.
でも筆者はアプリケーションをデータベースに依存させたくなかったのと,既に Prisma を使ってまあまあな量のコードを書いてたからやめたのだ.

却下なのだ.

3. 各 Repository 内で prisma.$queryRaw を使う

結論これなのだ.
筆者はクリーンアーキテクチャを採用してるんだけど,そのうちの Repository (データベースとのやりとりを行うやつ) の中で $queryRaw を使うことにしたのだ.
$extend がまともに使えなかったから仕方ないのだ.

実装にあたって,できるだけ型安全にするためにユーティリティをいくつか作ったから紹介していくのだ.
コメントが雑なのは許してほしいのだ.

export type Geometry = {
    longitude: number,
    latitude: number
}

/// Geometry を Geography の StPoint(緯度, 経度) に変換する
export function geometryToStPoint(geometry: Geometry | null) {
    if (geometry === null) return 'NULL';
    return `st_point(${geometry.longitude} ${geometry.latitude})`;
}

/**
 * object を (foo, bar) VALUES (fuga, hoge) の文字列に変換する
 * @param object SQL文に変換するオブジェクト
 * @param joint INSERT 文のときは VALUES, UPDATE 文のときは = を指定する
 */
export function toSqlValues(object: {[k in string]: unknown}, joint: 'VALUES' | '=' = 'VALUES') {
    const keys = Object.keys(object);

    return `(${keys.join(', ')}) ${joint} (${keys.map((k) => {
        const val = object[k];
        if (val instanceof string) return `'${val}'`;
        return val;
    }).join(', ')})`
}

export type Primitive = string | number | symbol | bigint | boolean | null | undefined;

/// オブジェクトの value を Primitive 型のみに制限する
export type PrimitiveObject<T> = {
    [k in keyof T]: Primitive
}

これらを次のような形で使うのだ.

type Foo = {
  location: Geometry | null,
  // some other fields...
}

export class FooRepositoryImpl implements FooRepository {
    async create(foo: ForCreateWithId<Foo>): Promise<Foo> {
        try {
            const primitiveFoo: PrimitiveObject<Foo> = {
                ...foo,
                location: geometryToStPoint(deliverer.location)
            }
            return await prisma.$queryRaw<Foo>`
            INSERT INTO "Foo" ${toSqlValues(primitiveFoo)});
            `;
        } catch (e) {
            return Promise.reject(e);
        }
    }

    // ...

終わりに

読んでくれてありがとうなのだ.
間違いあったら指摘してほしいのだ.

Discussion