メインコンテンツまでスキップ

· 約3分
ogumaru

概要

Prisma を利用して、 1:N のリレーションを持つ単純なレコードの値を配列として取得する。

結論

Proxyを利用する。

実行環境

実行環境バージョン
nodev16.15.0
prisma4.2.1
@prisma/client4.2.1

やりたいこと

LeavesとBranchesのER図

schema.prisma
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model Branches {
id Int @id @default(autoincrement())
leaves Leaves[]
}

model Leaves {
id Int @id @default(autoincrement())
branch Branches @relation(fields: [branchId], references: [id])
branchId Int
value String
}

上図の単純な Branches:Leaves = 1:N となるテーブルに対して、Prisma で取得したオブジェクトに対し、

console.log(branches.leaves)
> ["hoge", "huga", ...]

のようにプロパティアクセスで子となるレコードの値のみを取得したい。

実際の挙動

Prisma ではレコードはオブジェクトとなる。

const select = async () => {
const branch = await prisma.branches.findFirstOrThrow({
include: { leaves: true },
});
return branch;
};

とすると下記のような形でレコードが返される。

{
"id": 1,
"leaves": [
{
"id": 1,
"branchId": 1,
"value": "hoge"
},
{
"id": 2,
"branchId": 1,
"value": "huga"
}
]
}

Proxy を利用したルーティング

leavesに対するアクセスを下記のように処理すると値の配列として取得できる。

interface ILeaf {
id: number;
value: string;
}

interface IBranch {
id: number;
leaves: Array<ILeaf>;
}

const select = async () => {
const proxy: ProxyHandler<IBranch> = {
get: (obj, prop) => {
if (prop === "leaves") {
return obj.leaves.map((leaf) => leaf.value);
} else {
if (Object.hasOwn(obj, prop)) {
return obj[prop as keyof IBranch];
} else {
return undefined;
}
}
},
};
const branch = await prisma.branches.findFirstOrThrow({
include: { leaves: true },
});
return new Proxy(branch, proxy);
};

leavesは単純な値の配列となる。

{
"id": 1,
"leaves": ["hoge", "huga"]
}

留意点

プロパティをプライベートにするものではないため、値の更新をする場合には注意が必要。

const records = {
private: [
{ id: 0, value: "secret" },
{ id: 1, value: "keys" },
],
};

const proxy = {
get: (obj, prop) => {
if (prop === "private") {
return obj[prop].map((record) => record.value);
} else {
return obj[prop];
}
},
};
const proxied = new Proxy(records, proxy);

console.log(proxied.private);
// > [ { id: 0, value: 'secret' }, { id: 1, value: 'keys' } ]

proxied.private = ["hoge", "huga"];

console.log(proxied.private);
// > [ undefined, undefined ]

· 約5分
ogumaru

概要

TypeScript にて、タプル風([T, T])の配列のコレクションがあり、これの中身をユニークにしたい場面があった。

Setを経由すれば重複排除できると思ったが、見た目上同じオブジェクトの重複は排除されなかった。

結論

JavaScript における配列の同値比較がfalseになるため。

厳密にユニークなコレクションを作成する場合は、deepEqual 相当の確認が必要そう。

実行環境

実行環境バージョン
nodev16.15.0
python3Python 3.10.4

JavaScript の挙動と Python との比較

タプル風の配列リテラルをaddすると、重複した値がそれぞれが追加されてしまう。

const unique = new Set();
unique.add(["hoge", "huga"]);
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(["hoge", "huga"]);
// Set(2) { [ 'hoge', 'huga' ], [ 'hoge', 'huga' ] }

Python ではできた気がしたので確認する。

(なお["hoge", "huga"]Hashableでないためsetには追加できない)

unique = set()
unique.add(("hoge", "huga"))
# {('hoge', 'huga')}
unique.add(("hoge", "huga"))
# {('hoge', 'huga')}

リテラルのタプルに対して重複排除ができている。

JavaScript でも、オブジェクトが同一になるため、下記では意図した通りに重複が排除される。

const unique = new Set();
const tuple = ["hoge", "huga"];
unique.add(tuple);
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(tuple);
// Set(1) { [ 'hoge', 'huga' ] }

JavaScript では Python におけるタプル相当のデータ型がないため、上記コードでは実際にはミュータブルな配列となる。

Object.freeze()でイミュータブルにしてみたが、結果は変わらなかった。

const unique = new Set();
unique.add(Object.freeze(["hoge", "huga"]));
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(Object.freeze(["hoge", "huga"]));
// Set(2) { [ 'hoge', 'huga' ], [ 'hoge', 'huga' ] }

仕様の確認

MDN のSetのページを見ると、-0+0について触れられている。

See "Key equality for -0 and 0" in the browser compatibility table for details.

Set - JavaScript | MDN (developer.mozilla.org)

等価比較のページを見ると、

SameValueZero: used by %TypedArray% and ArrayBuffer constructors, as well as Map and Set operations, and also String.prototype.includes and Array.prototype.includes since ES2016

Equality comparisons and sameness - JavaScript | MDN (developer.mozilla.org)

SameValueZeroの TC39 へリンクされていたのでこちらも確認。

SameValueZero differs from SameValue only in that it treats +0𝔽 and -0𝔽 as equivalent.

SameValueZero | ECMAScript® 2023 Language Specification (tc39.es)

SameValue+0-0の比較結果のみ異なるとあるため、SameValueを確認すると、

  1. If Type(x) is different from Type(y), return false.

  2. If Type(x) is Number, then

    a. Return Number::sameValue(x, y).

  3. If Type(x) is BigInt, then

    a. Return BigInt::sameValue(x, y).

  4. Return SameValueNonNumeric(x, y).

とのことなのでSameValueNonNumericを確認。

  1. Assert: Type(x) is the same as Type(y).

  2. If Type(x) is Undefined, return true.

  3. If Type(x) is Null, return true.

  4. If Type(x) is String, then

    a. If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false.

  5. If Type(x) is Boolean, then

    a. If x and y are both true or both false, return true; otherwise, return false.

  6. If Type(x) is Symbol, then

    a. If x and y are both the same Symbol value, return true; otherwise, return false.

  7. If x and y are the same Object value, return true. Otherwise, return false.

タプルの場合7にて評価されると思われ、same Object valueではないということになるようだ。

same Object value とは

TC39にはこれ以上のリンクがなかったが、deepEqual相当の比較を行うことで重複の確認はできそう。

修正 PR いただけるとありがたいです。