FlutterのDBをFirestoreからサーバーレスRDBに置き換えてみた
はじめに
今回はFlutterのデータベースにRDBを使ってみたいと思い、Firebase Cloud Functions / PlanetScale / Prisma の組み合わせを試してみました。
普段Firebase(Firestore)を使っている場合はあまり意識することはないと思いますが、本来自分でデータベース(DB)を用意する場合、↓のようにAPI/DBのサーバーを用意する必要があります。
ここでFirebase Cloud FunctionsとPlanetScaleを使うと、それぞれ自分でサーバーを管理することなく(サーバーレス)運用できるというわけです。
さらにPrismaを使うことで、SQLではなくTypeScript(JavaScriptも可)でRDBの読み書きをしていきます。
各リンク
Prisma: https://www.prisma.io
PlanetScale: https://app.planetscale.com
セットアップ
Firebase Cloud Functions
特に難しい手順はないので割愛します。
Firebaseのプロジェクトを作成し、firebase init
まで済ませておくと良いです。
PlanetScale
こちらも特に難しい手順はないので割愛します。
databaseを作って、発行されるパスワードをメモしておいてください。
(次の手順で使用します)
Prisma
初期設定のため以下コマンドを実行します。
// firebase initで作られるfunctions/に移動
$ cd functions
$ npm install prisma -D
$ npx prisma init
PlanetScaleでConnect
> Connect with: Prisma
を選択すると、.env
とschema.prisma
の例が表示されるので、そのままコピペします。
.envの************
部分は、発行したパスワードに置き換える必要があるので注意してください。
Scheme作成
PrismaのModel定義
今回は投稿(POST)を扱います。
Firestoreとの連携のため、id
と別にユニークなpostId
を持たせています。
/functions/prisma/schema.prisma
generator client {
// ↑でPlanetScaleからコピペした内容
}
datasource db {
// ↑でPlanetScaleからコピペした内容
}
model Post {
id Int @id @default(autoincrement())
authorId String
imageUrl String
postId String @unique
isDeleted Boolean @default(false)
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
@@map(name: "posts")
}
PlanetScaleに反映
$ npx prisma db push
上記コマンドでPlanetScaleに反映します。
PlanetScaleのコンソールから反映内容を可能です。
Cloud Functions実装
ここまででデータベースの用意はできたので、データの読み書きをするためのFunctionsを実装していきます。
今回はとりあえず、新規登録を試してみます。
Prismaを通してデータの読み書きをするので、こちらを参考にしました。
/functions/src/index.ts
↓
import * as admin from 'firebase-admin';
export const firebaseAdmin = admin.initializeApp();
import { addPost } from './methods/addPost';
module.exports = {
addPost,
};
/functions/src/methods/addPost.ts
↓
import * as functions from 'firebase-functions';
import { successResponse, throwError } from '../../services/httpService';
import { setPostToPrisma } from '../../services/prismaHandler';
export const addPost = functions
.region('asia-northeast1')
.https.onCall(async (data, context) => {
const auth = context.auth;
if (auth == undefined) {
throwError('unauthenticated', 'auth is undefined');
return;
}
const authorId = auth.uid;
const imageUrl: string | undefined = data.imageUrl;
const postId: string | undefined = data.postId;
if (postId == undefined || imageUrl == undefined) {
throwError('invalid-argument', 'parameter is invalid');
return;
}
await setPostToPrisma({ authorId, imageUrl, postId });
console.log('Addition Complete ', postId);
return successResponse();
});
/functions/services/httpService.ts
↓
import { HttpsError } from 'firebase-functions/v1/auth';
import { FunctionsErrorCode } from 'firebase-functions/v1/https';
export const successResponse = () => {
return { result: 'ok' };
};
export const throwError = (code: FunctionsErrorCode, message: string) => {
console.log(message);
throw new HttpsError(code, message);
};
/functions/services/prismaHandler.ts
↓
import { PrismaClient } from '@prisma/client';
import { Post, PrismaPost } from '../models/Post';
export const setPostToPrisma = async (postData: PrismaPost) => {
const prisma = new PrismaClient();
const result = await prisma.post.create({
data: postData,
});
return Post.fromJson(result);
};
/functions/models/Post.ts
↓
export class Post {
public readonly authorId: string;
public readonly imageUrl: string;
public readonly postId: string;
public readonly isDeleted: boolean;
public readonly createdAt: Date;
public readonly updatedAt: Date;
constructor(
authorId: string,
imageUrl: string,
postId: string,
isDeleted: boolean,
createdAt: Date,
updatedAt: Date
) {
this.authorId = authorId;
this.imageUrl = imageUrl;
this.postId = postId;
this.isDeleted = isDeleted;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public static fromJson = (json: any) => {
const {
authorId,
imageUrl,
postId,
isDeleted,
createdAt,
updatedAt,
} = Object.assign({}, json);
return new Post(
authorId,
imageUrl,
postId,
isDeleted,
createdAt,
updatedAt
);
};
}
export type PrismaPost = {
authorId: string;
imageUrl: string;
postId: string;
};
Functionsデプロイ
実装が済んだのでデプロイしたところ、↓のようにエラーが大量発生してデプロイできませんでした。
node_modules/@types/express-serve-static-core/index.d.ts:1187:33 - error TS1005: ';' expected.
1187 * application (which is a `Function`) as its
~~~~~~~~
node_modules/@types/express-serve-static-core/index.d.ts:1255:1 - error TS1160: Unterminated template literal.
1255
Found 127 errors.
express-serve-static-core
とか特に使ってない認識でしたが、firebase関連のパッケージが依存しているのでは?と思い、最新のバージョンに更新してみたところ解決しました。
参考までにpackage.json
は↓こんな感じです。
"dependencies": {
"@prisma/client": "^4.6.1",
"firebase-admin": "^11.3.0", // 最新に更新
"firebase-functions": "^4.1.0" // 最新に更新
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.9.1",
"@typescript-eslint/parser": "^3.8.0",
"eslint": "^7.6.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.22.0",
"firebase-functions-test": "^0.2.0",
"prisma": "^4.6.1",
"typescript": "^4.9.3" // 最新に更新
},
アプリ(Flutter)側の実装
Cloud Functionsのデプロイができたので、あとはアプリ側でこの関数を呼び出してあげればOKです。
今回はパッケージcloud_functionsを使います。
画像のアップロード等絡んで長くなるので詳細は割愛しますが、関数の呼び出し自体はこんな↓感じです。
Future<void> addPost({
required String imageUrl,
required String postId,
}) async {
try {
final callable = FirebaseFunctions.instanceFor(region: 'asia-northeast1')
.httpsCallable('addPost');
await callable.call({
'imageUrl': imageUrl,
'postId': postId,
});
} on Exception catch (_) {
rethrow;
}
}
Cloud Functionsの関数はaddPost
という名前にしているので、.httpsCallable('addPost')
とすることで呼び出すことができます。
アプリを起動して登録操作し、
npx prisma studio
で確認したところ、無事データが登録されていました🎉
さいごに
無事にFirestore → Firebase Cloud Functions / PlanetScale / Prisma に置き換えることができました。
Cloud FunctionsとPlanetScaleを使うことでサーバーレスでRDBを扱うことができますし、低予算で試すことができるので、個人開発レベルでの導入もオススメかなと思います。
とはいえ、Firestoreが便利なことに変わりはないので、ユースケースに応じて組み合わせて使うのがよさそうです。
だいぶ省略したので、ご質問等あればお気軽にコメントください!
参考記事
Discussion