Firebase に Cloud SQL を接続できるようになった、という話を聞いてから「いつか試そう」と先送りにしているうちに、気がつけばFirebase Data Connectは正式リリースから1年以上が経過していました。RealTime Database でも Firestore でもない、GraphQL ベースのリレーショナルデータベース連携——これが既存のFirebaseプロジェクトに何をもたらすのか、正直なところまだ掴みきれていない方も多いのではないでしょうか。
私も最初はそうでした。しかし Antigravity IDE でスキーマ定義ファイルを開いた瞬間、AI が「この構造なら @table ディレクティブでこう設計するとN+1クエリが消えます」と即座に提案してきたとき、このツールが従来の Firebase 開発を根本から変えることを実感しました。
ここで扱うのはFirebase Data Connect を Antigravity と組み合わせて使う際に得られた知見を、実際に動くコードとともに共有します。公式ドキュメントには書かれていない「実際に詰まるポイント」に特に注力しました。
Firebase Data Connect とは何か:Firestoreとの根本的な違いを理解する
Firebase Data Connect は、Google Cloud SQL(PostgreSQL)を Firebase のエコシステムから直接操作できるようにする GraphQL ベースのサービスです。2024年に GA(正式リリース)となり、従来の Firebase Realtime Database や Firestore とは設計思想が大きく異なります。
最も重要な違いは「スキーマファースト」という設計アプローチです。Firestore はドキュメント指向でスキーマレスな柔軟性が売りでしたが、Data Connect では .gql ファイルでスキーマを先に定義し、そこからクライアント SDK、型定義、GraphQL リゾルバーが自動生成されます。
Data Connect が Firestore より適しているケース
ユーザー・投稿・コメント・いいねのような複雑なリレーションがある
集計クエリ(SUM・AVG・GROUP BY)を多用する分析系の機能がある
既存の PostgreSQL スキーマを Firebase に移植したい
型安全な GraphQL API を素早く生成したい
Firestore が引き続き適しているケース
リアルタイムリスニング(onSnapshot)を多用する
ドキュメントの柔軟な構造変更が頻繁に必要
スモールスタートで後からスキーマを固めたい
Antigravity でこの判断を AI に聞くと、既存のドメインモデルを分析して「このエンティティ群は Data Connect の方が適切です、理由は〜」と根拠付きで回答してくれます。これが Antigravity を使う最初のメリットです。
Antigravity での開発環境セットアップ:正しい初期化手順
Firebase Data Connect の開発環境は、セットアップ手順を間違えると後から修正が難しい落とし穴があります。特に Antigravity でアシストを受けながら進める場合、以下の順序を守る点が肝心です。
まず、Firebase CLI と Data Connect Emulator を正しいバージョンで用意します。
# Firebase CLI を最新版に更新(12.0.0 以降が必要)
npm install -g firebase-tools@latest
firebase --version # 13.x.x であることを確認
# プロジェクトの初期化
mkdir my-app && cd my-app
firebase init dataconnect
# エミュレーターのセットアップ(Data Connect + Auth を一緒に起動)
firebase init emulators
# dataconnect, auth, hosting を選択
初期化後、Antigravity でプロジェクトを開くと .antigravity/rules ファイルに Data Connect 用のコンテキストを追加するよう提案されます。これを受け入れることで、スキーマファイルを開いたときに AI が GraphQL ディレクティブの補完を正確に行えるようになります。
# .antigravity/rules に追加(Antigravity が自動提案する内容)
You are working on a Firebase Data Connect project.
Schema files use .gql extension with @table, @col, @ref directives.
Connector files define queries and mutations.
Always generate TypeScript SDK imports from the auto-generated client SDK.
次に、プロジェクト構造を確認します。
dataconnect/
schema/
schema.gql ← エンティティ定義
connector/
connector.yaml ← コネクター設定
queries.gql ← クエリ定義
mutations.gql ← ミューテーション定義
dataconnect.yaml ← サービス設定
ここでよくあるミスは、schema.gql と queries.gql を1つのファイルにまとめてしまうことです。Data Connect のビルドは分割されたファイルを前提としており、混在させるとエミュレーターが正しく起動しません。Antigravity の AI に「スキーマと操作を1ファイルにまとめたい」と伝えると、なぜ分割すべきかを根拠付きで説明してくれます。
スキーマ設計の核心:GraphQLディレクティブを使いこなす
Data Connect のスキーマ設計で最初に理解すべきは、@table・@col・@ref の3つのコアディレクティブです。Antigravity に「ブログサービスのスキーマを作って」と頼むと、以下のような型安全なスキーマを即座に生成してくれます。
# dataconnect/schema/schema.gql
type User @table {
id : UUID \! @col ( name : "user_id" ) @default ( expr : "uuidV4()" )
email : String \! @col ( dataType : "varchar(255)" ) @unique
displayName : String \! @col ( name : "display_name" , dataType : "varchar(100)" )
createdAt : Date \! @col ( name : "created_at" ) @default ( expr : "request.time" )
posts : [ Post \!]\! @hasMany ( foreignKey : "author_id" )
}
type Post @table {
id : UUID \! @col ( name : "post_id" ) @default ( expr : "uuidV4()" )
title : String \! @col ( dataType : "varchar(500)" )
content : String \! @col ( dataType : "text" )
published : Boolean \! @default ( value : false )
authorId : UUID \! @col ( name : "author_id" )
author : User \! @belongsTo ( foreignKey : "author_id" )
tags : [ Tag \!]\! @manyToMany ( joinTable : "PostTag" )
createdAt : Date \! @col ( name : "created_at" ) @default ( expr : "request.time" )
updatedAt : Date @col ( name : "updated_at" )
}
type Tag @table {
id : UUID \! @col ( name : "tag_id" ) @default ( expr : "uuidV4()" )
name : String \! @col ( dataType : "varchar(50)" ) @unique
posts : [ Post \!]\! @manyToMany ( joinTable : "PostTag" )
}
このスキーマで重要なポイントを解説します。
@default(expr: "uuidV4()") について : Data Connect はサーバーサイドで UUID を自動生成できます。クライアントから ID を送る必要がなくなり、UUID の衝突リスクを排除できます。
@manyToMany(joinTable: "PostTag") について : 中間テーブルを明示的に定義せずに多対多のリレーションを表現できます。Data Connect が PostTag テーブルを自動生成します。ただし、中間テーブルに追加属性(例: 並び順)を持たせたい場合は、明示的なエンティティ定義が必要です。これは Antigravity に「中間テーブルに order カラムを追加したい」と伝えると正しい実装方法を教えてくれます。
@col(dataType: "varchar(255)") について : PostgreSQL の型を直接指定できます。デフォルトでは String は text 型になりますが、インデックス効率を考えると varchar の方が適切なケースが多いです。
クエリとミューテーションの実装:型安全APIの自動生成
スキーマを定義したら、次はクエリとミューテーションを定義します。ここが Data Connect の最大の魅力で、定義した GraphQL がそのまま型安全な TypeScript SDK に変換されます。
# dataconnect/connector/queries.gql
# 公開済み投稿の一覧(ページネーション付き)
query ListPublishedPosts ( $limit : Int , $offset : Int ) @auth ( level : PUBLIC ) {
posts (
where : { published : { eq : true } }
orderBy : [{ createdAt : DESC }]
limit : $limit
offset : $offset
) {
id
title
createdAt
author {
id
displayName
}
tags {
name
}
}
}
# ユーザーの全投稿(認証必須)
query GetUserPosts ( $userId : UUID \!) @auth ( level : USER ) {
posts (
where : {
authorId : { eq : $userId }
# 自分の投稿のみアクセス可能
_and : [{ authorId : { eq_expr : "auth.uid" } }]
}
orderBy : [{ createdAt : DESC }]
) {
id
title
published
createdAt
updatedAt
}
}
# 投稿詳細(公開済みのみ、または自分の投稿)
query GetPostDetail ( $postId : UUID \!) @auth ( level : PUBLIC ) {
post (
id : $postId
where : {
_or : [
{ published : { eq : true } }
{ authorId : { eq_expr : "auth.uid" } }
]
}
) {
id
title
content
published
createdAt
updatedAt
author {
id
displayName
}
tags {
id
name
}
}
}
# dataconnect/connector/mutations.gql
# 投稿の新規作成(認証必須)
mutation CreatePost ( $title : String \!, $content : String \!, $tagIds : [ UUID \!]) @auth ( level : USER ) {
post_insert ( data : {
title : $title
content : $content
authorId_expr : "auth.uid"
tags : { connect : $tagIds }
}) {
id
title
createdAt
}
}
# 投稿の更新(オーナーのみ)
mutation UpdatePost ( $postId : UUID \!, $title : String , $content : String , $published : Boolean ) @auth ( level : USER ) {
post_update (
id : $postId
data : {
title : $title
content : $content
published : $published
updatedAt_expr : "request.time"
}
where : { authorId : { eq_expr : "auth.uid" } }
) {
id
title
published
updatedAt
}
}
# 投稿の削除(オーナーのみ)
mutation DeletePost ( $postId : UUID \!) @auth ( level : USER ) {
post_delete (
id : $postId
where : { authorId : { eq_expr : "auth.uid" } }
)
}
これらの定義から、Antigravity 上でエミュレーターを起動すると TypeScript SDK が自動生成されます。
// src/lib/dataconnect.ts
import {
connectDataConnectEmulator,
getDataConnect,
} from "firebase/data-connect" ;
import { connectorConfig } from "@firebasegen/my-app-connector" ;
// 開発環境ではエミュレーターに接続
const dc = getDataConnect (connectorConfig);
if (process.env. NODE_ENV === "development" ) {
connectDataConnectEmulator (dc, "localhost" , 9399 );
}
export { dc };
// src/app/posts/page.tsx(Next.js App Router の例)
import {
executeQuery,
listPublishedPostsRef,
} from "@firebasegen/my-app-connector" ;
import { dc } from "@/lib/dataconnect" ;
async function getPosts ( limit = 10 , offset = 0 ) {
try {
const result = await executeQuery ( listPublishedPostsRef (dc, { limit, offset }));
return result.data.posts;
} catch (error) {
// Data Connect のエラーは FirebaseError 型
if (error instanceof Error && error.message. includes ( "permission" )) {
console. error ( "アクセス権限エラー:" , error.message);
return [];
}
throw error;
}
}
export default async function PostsPage () {
const posts = await getPosts ();
return (
< ul >
{ posts . map (( post ) => (
< li key = {post.id} >
< a href = { `/posts/${ post . id }` } > {post.title} </ a >
< span >{post.author.displayName} </ span >
</ li >
))}
</ ul >
);
}
この型安全性が Data Connect の強みです。posts.map(post => post.title) と書いたとき、post.title が string であることを TypeScript が保証してくれます。Firestore の DocumentData では実現できなかった体験です。
Firebase Auth との認証統合:@auth ディレクティブの設計パターン
Data Connect の認証は、クエリ・ミューテーションに @auth ディレクティブを付けることで制御します。ここでの設計ミスがセキュリティホールに直結するため、慎重に理解する必要があります。
@auth レベルの種類と使い分け
@auth(level: PUBLIC): 未認証ユーザーも実行可能。公開コンテンツの読み取りに使います
@auth(level: USER): Firebase Auth で認証済みのユーザーのみ実行可能。auth.uid が使えます
@auth(level: ADMIN): 管理者(Firebase Admin SDK)からのみ実行可能。バックエンドのバッチ処理などに使います
@auth(level: USER_ANON): 匿名認証ユーザーを含む認証済みユーザー
Antigravity でスキーマを設計していると、AI が「このミューテーションは level: USER だけでなく where: { authorId: { eq_expr: "auth.uid" } } も必ず追加してください」と警告してくれます。@auth(level: USER) だけでは「ログイン済みユーザーなら誰でも他人のデータを変更できる」という危険な状態になるためです。
# ❌ 危険:ログイン済みなら誰でも他人の投稿を削除できる
mutation DeletePost ( $postId : UUID \!) @auth ( level : USER ) {
post_delete ( id : $postId )
}
# ✅ 安全:自分の投稿しか削除できない
mutation DeletePost ( $postId : UUID \!) @auth ( level : USER ) {
post_delete (
id : $postId
where : { authorId : { eq_expr : "auth.uid" } }
)
}
クライアント側での Firebase Auth との連携は以下の通りです。
// src/lib/auth-dataconnect.ts
import { getAuth, onAuthStateChanged } from "firebase/auth" ;
import { getDataConnect, setInitialUserToken } from "firebase/data-connect" ;
import { connectorConfig } from "@firebasegen/my-app-connector" ;
const auth = getAuth ();
const dc = getDataConnect (connectorConfig);
// Firebase Auth の状態変化を Data Connect に反映する
onAuthStateChanged (auth, async ( user ) => {
if (user) {
const token = await user. getIdToken ();
await setInitialUserToken (dc, token);
} else {
// ログアウト時はトークンをクリア
await setInitialUserToken (dc, undefined );
}
});
ここで注意すべき点は、setInitialUserToken は非同期処理であり、呼び出し直後に Data Connect クエリを実行すると認証状態が反映されていない場合があります。Antigravity に「ログイン直後にユーザーデータを取得したい」と相談すると、onAuthStateChanged のコールバック内でクエリを実行するパターンを提案してくれます。
AntigravityのAIによるスキーマ最適化:実際のプロンプトパターン
Antigravity を使う最大のメリットは、スキーマ設計の相談相手として AI を使えることです。以下は実際に効果があったプロンプトパターンです。
パターン1: N+1クエリの検出と修正
既存の schema.gql と queries.gql を分析して、N+1クエリが発生しているクエリを全て特定し、
@join や nested selection を使って修正してください。
各修正について、なぜN+1が発生していたかと、どう解決したかを説明してください。
このプロンプトで Antigravity が「ListPublishedPosts クエリでは author を取得するたびに個別クエリが走っています。Data Connect の nested selection は自動的に JOIN に最適化されるため、以下のように書き直すと1クエリで完結します」という具体的な修正を提案してくれます。
パターン2: インデックス設計の提案
現在の schema.gql と queries.gql を分析して、Cloud SQL (PostgreSQL) で
必要なインデックスを特定してください。
クエリのWHERE句とORDER BY句を基に、CREATE INDEX文のSQLも生成してください。
Data Connect はインデックスを自動管理しますが、複合インデックスや部分インデックスは手動で追加した方が効率的なケースがあります。AI がクエリパターンを分析して最適なインデックス戦略を提案してくれます。
パターン3: ページネーション戦略の選択
現在 offset ベースのページネーションを使っています。
データ量が増えた場合のカーソルベースへの移行方法と、
Data Connect でのカーソルページネーションの実装例を教えてください。
よくある落とし穴:実際に詰まった5つのポイント
Firebase Data Connect を使い始めて実際に遭遇した問題を、解決策とともに共有します。
1. エミュレーターと本番環境でのスキーマ差異
エミュレーターは dataconnect.yaml の schemaValidation: COMPATIBLE 設定を無視して常に厳格検証を行います。本番デプロイ時に「エミュレーターでは動いたのに」というエラーが出た場合、ほとんどのケースで @col(dataType) の指定がないカラムが PostgreSQL のデフォルト型(text)として作成されているのに対し、クライアントが varchar を期待している不整合が原因です。
解決策 : スキーマ変更のたびに firebase dataconnect:sql:migrate を実行し、Cloud SQL への適用内容を確認します。
2. ミューテーション後のキャッシュ不整合
Next.js App Router と組み合わせた場合、ミューテーション後に router.refresh() を呼び出しても古いデータが表示されることがあります。Data Connect のクライアント SDK はデフォルトでキャッシュを持たないため、これは Next.js の RSC(React Server Components)キャッシュの問題です。
解決策 : ミューテーション後に revalidatePath('/posts') を Server Action 内で呼び出します。
// src/app/actions/posts.ts
"use server" ;
import { revalidatePath } from "next/cache" ;
import { executeMutation, createPostRef } from "@firebasegen/my-app-connector" ;
import { dc } from "@/lib/dataconnect" ;
export async function createPost ( title : string , content : string ) {
try {
const result = await executeMutation (
createPostRef (dc, { title, content })
);
// Next.js のキャッシュを無効化
revalidatePath ( "/posts" );
return { success: true , post: result.data.post_insert };
} catch (error) {
console. error ( "投稿作成エラー:" , error);
return { success: false , error: "投稿の作成に失敗しました" };
}
}
3. UUID の型不一致
TypeScript SDK では UUID が string 型として扱われますが、フォームから受け取った postId をそのまま渡すと実行時エラーになることがあります。これは UUID のフォーマット検証を Data Connect がサーバーサイドで行うためです。
解決策 : ユーザー入力から受け取った ID には必ずバリデーションを追加します。
import { z } from "zod" ;
const uuidSchema = z. string (). uuid ( "無効なIDフォーマットです" );
export async function getPost ( postId : string ) {
// バリデーション
const validatedId = uuidSchema. parse (postId);
try {
const result = await executeQuery (
getPostDetailRef (dc, { postId: validatedId })
);
return result.data.post;
} catch (error) {
if (error instanceof z . ZodError ) {
throw new Error ( "無効なリクエストです" );
}
throw error;
}
}
4. @default(expr: "request.time") のタイムゾーン
request.time は UTC で記録されます。日本のユーザー向けに JST で表示する場合、クライアントサイドで変換が必要です。これを忘れると「9時間ずれた日時が表示される」という定番バグが発生します。
解決策 : 日時変換には Intl.DateTimeFormat を使います。
function formatJST ( dateString : string ) : string {
return new Intl. DateTimeFormat ( "ja-JP" , {
timeZone: "Asia/Tokyo" ,
year: "numeric" ,
month: "2-digit" ,
day: "2-digit" ,
hour: "2-digit" ,
minute: "2-digit" ,
}). format ( new Date (dateString));
}
5. 大量データでの offset ページネーションの劣化
10万件を超えるデータで offset: 50000 のようなクエリを実行すると、PostgreSQL が先頭から50,000件を読み飛ばすため極めて遅くなります。Data Connect でのカーソルベースページネーションは現時点(2026年4月)で実験的サポートのため、回避策として createdAt < [最後の記事の日時] での絞り込みが現実的です。
本番環境デプロイと運用のベストプラクティス
本番環境への初回デプロイで最も重要なのは、Cloud SQL インスタンスの選択です。Development エディションと Enterprise エディションでは価格と機能が大きく異なります。
個人開発・スタートアップ段階では Development エディション(約 $7/月〜)から始め、DAU が 1,000 を超えたあたりで Enterprise への移行を検討するのが現実的です。
# 本番環境へのデプロイ
firebase deploy --only dataconnect
# スキーママイグレーション(破壊的変更がある場合)
firebase dataconnect:sql:migrate --force
# デプロイ前のドライラン(推奨)
firebase dataconnect:sql:migrate --dry-run
接続数管理も重要です。Cloud SQL はデフォルトで接続数に上限があります。Next.js の Serverless 環境(Cloudflare Workers, Vercel Edge)では各リクエストが新しい接続を作るため、接続プールの設定が必須です。
Data Connect SDK はコネクションプールを内部管理していますが、Antigravity の AI に「Cloudflare Workers と Data Connect の組み合わせで接続プールを最適化したい」と相談すると、@google-cloud/cloud-sql-connector を使った明示的な設定方法を提案してくれます。詳細は Antigravity × Cloudflare Workers AIエッジアプリ構築ガイド もご参照ください。
コスト管理については Antigravity AIクレジット最適化ガイド に詳しくまとめています。Data Connect の Cloud SQL コストは読み取りより書き込みの方が高く、バッチ挿入時には post_insertMany を使うことで API 呼び出し回数を削減できます。
MCP との統合:AIエージェントからデータベースを操作する
Antigravity の真の力は、MCP(Model Context Protocol)サーバーと組み合わせたときに発揮されます。Antigravity × MCP エコシステム 2026 完全活用ガイド で紹介されている Firebase MCP サーバーを使うと、Antigravity の AI が Data Connect のクエリを自然言語で実行できるようになります。
// カスタム MCP サーバー:Data Connect をAIエージェントに公開する例
// mcp-server/src/tools/dataconnect.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js" ;
import { executeQuery, listPublishedPostsRef } from "@firebasegen/my-app-connector" ;
import { dc } from "../lib/dataconnect.js" ;
export function registerDataConnectTools ( server : Server ) {
server. setRequestHandler ( "tools/call" , async ( request ) => {
if (request.params.name === "get_recent_posts" ) {
const { limit = 5 } = request.params.arguments as { limit ?: number };
try {
const result = await executeQuery (
listPublishedPostsRef (dc, { limit })
);
return {
content: [{
type: "text" ,
text: JSON . stringify (result.data.posts, null , 2 )
}]
};
} catch (error) {
return {
content: [{
type: "text" ,
text: `エラー: ${ error instanceof Error ? error . message : "不明なエラー"}`
}],
isError: true
};
}
}
});
}
この MCP サーバーを Antigravity に接続すると、「最新5件の投稿を取得して要約を作って」という自然言語の指示で、AI が get_recent_posts ツールを呼び出してデータを取得・処理します。詳細なマルチエージェント設計については Antigravity × Agent Manager フレームワーク をご参照ください。
個人開発者の視点から(実体験メモ)
全体を振り返って:Firebase Data Connectで変わる開発の現場
Firebase Data Connect は「Firestore の代替」ではなく「リレーショナルデータが必要なアプリのための Firebase ネイティブな選択肢」です。GraphQL スキーマから型安全な SDK が自動生成され、Firebase Auth との統合がディレクティブ一行で完結するという体験は、バックエンド API を自分でゼロから実装する手間を大幅に削減してくれます。
Antigravity と組み合わせた場合、スキーマの N+1 問題検出、インデックス最適化提案、セキュリティルールのレビューを AI がリアルタイムでサポートしてくれるため、一人開発でも高品質なデータ層を短期間で構築できます。
次のステップとして、手元のプロジェクトで firebase init dataconnect を実行し、エミュレーター環境で実際にスキーマを定義してみてください。最初の型安全なクエリが動いた瞬間、Firestore とは異なる確かな手応えを感じていただけると思います。