🧩
Hono × Supabase 複数テーブルを1回のクエリでレスポンス!
はじめに
Supabase SDKと軽量WebフレームワークHonoを組み合わせてAPIを作る中で、「複数テーブルをJOINして1回のクエリでレスポンスしたい」というニーズにぶつかりました。
この記事では、Supabaseの .select()
におけるJOINの使い方と、RelationalRepository
を活用して再利用性高く設計する方法を紹介します。
.select()
でJOINする方法
Supabaseの Supabaseでは、RDBの外部キーを活用して .select()
に ネスト構文を渡すことでJOINを実現できます。
const { data, error } = await supabase
.from('staffs')
.select('id, name, place(id, name)');
このように書くと、staffs
テーブルに関連する place
のデータも同時に取得できます。
裏側ではPostgRESTが外部キーを解釈して LEFT JOIN
を実行してくれます。
実践
前提 : TableRepository との関係
本記事の内容は、既に構築済みの TableRepository
を前提にしています。
これは、単一テーブルに対するCRUD(取得・更新・削除など)操作を共通化したベースクラスです。
abstract class TableRepository<T extends BaseEntity> {
protected abstract readonly TABLE_NAME: string;
protected abstract readonly ID_FIELD: string;
protected abstract readonly UPDATED_AT_FIELD: string;
constructor(protected readonly client: SupabaseClient) {}
// 共通の取得・更新・削除メソッドを提供
}
RelationalRepository を作って共通化
今回紹介する RelationalRepository
は上記の TableRepository
を継承して、JOINを含むクエリ構文を扱えるようにした拡張版です。
export abstract class RelationalRepository<T extends BaseEntity> extends TableRepository<T> {
protected abstract readonly BASE_TABLE: string;
protected abstract readonly SELECT_QUERY: string; // e.g., '*, place(name)'
protected abstract readonly ID_FIELD: string;
protected readonly UPDATED_AT_FIELD: string = 'updated_at';
async getByIdentifier(identifier: string): Promise<T> {
this.validateIdentifier();
this.validateField(identifier);
const query = this.client
.from(this.BASE_TABLE)
.select(this.SELECT_QUERY)
.eq(this.IDENTIFIER_FIELD!, identifier)
.single();
return this.executeQuery<T>(query, 'getByIdentifier');
}
}
StaffWithPlaceRepository
使用例:export class StaffWithPlaceRepository extends RelationalRepository<StaffWithPlace> {
protected readonly BASE_TABLE = StaffFields.TABLE_NAME;
protected readonly SELECT_QUERY = `*, ${PlaceFields.TABLE_NAME}(*)`;
protected readonly TABLE_NAME = StaffFields.TABLE_NAME;
protected readonly ID_FIELD = StaffFields.ID;
protected readonly IDENTIFIER_FIELD = StaffFields.STAFF_IDENTIFIER;
protected readonly UPDATED_AT_FIELD = StaffFields.UPDATED_AT;
/// [GET]
async getStaffWithPlaceByIdentifier(staff_identifier: string): Promise<StaffWithPlace> {
try {
return await this.getByIdentifier(staff_identifier);
} catch (e) {
console.error('getUserBySupabaseId エラー:', e);
throw e;
}
}
HonoでAPIを組み合わせるとこうなる
export const getStaff = async (
{ staff_identifier }: { staff_identifier: string },
c: any
) => {
try {
const supabase = createSupabaseClient(c);
const staffWithPlaceRepo = new StaffWithPlaceRepository(supabase);
const staffWithPlaceData = await staffWithPlaceRepo.getStaffWithPlaceByIdentifier(staff_identifier);
return {
status: 'fetched',
message: 'スタッフ情報を取得しました',
data: staffWithPlaceData,
};
} catch (e) {
console.error('エラー発生:', e);
throw e;
}
};
因みに変更前の同じ関数はこんな感じです!
よりシンプルになったことが分かるかと思います。
export const getStaff = async (
{ staff_identifier }: { staff_identifier: string },
c: any
) => {
try {
const supabase = createSupabaseClient(c);
const staffRepo = new StaffRepository(supabase);
const staffData = await staffRepo.getStaffByIdentifier(staff_identifier);
const placeRepo = new PlaceRepository(supabase);
const placeData = await placeRepo.getPlaceById(staffData.place_id);
return {
status: 'fetched',
message: 'スタッフ情報を取得しました',
data: {
...staffData,
place: placeData,
},
};
} catch (e) {
console.error('エラー発生:', e);
throw e;
}
};
まとめ
複数回のリクエストを Supabase に投げるコードから脱却することで、パフォーマンスの向上とコードの見通しの良さを両方得ることができてスッキリしました!
うれしい!
おまけ:ネストJOINの応用
.select('id, gift(name), user(id, name), staff(id, name, place(id, name))')
このように、2段階、3段階のネストJOINも対応可能です。
SupabaseはPostgreSQLの力を使ってレスポンスを生成しているので、正しくリレーションが貼られていれば強力なJOINが使えます。
Discussion