Hi,大家好! 前陣子在研究專案上從 MySQL migrate 到 Cloud Firestore 的可能性,選擇了 Fireorm 作為這次的主題,這篇文章會帶著大家認識 Fireorm 的基本操作。
# 什麼是 Cloud Firestore ?
以下為 Cloud Firestore 官方文件 上對自己的介紹:
Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.
簡單來說,Cloud Firestore 是一個有彈性與擴展性的 NoSQL cloud database。推薦看 Cloud Firestore 官方文件 瞭解更多。
# 什麼是 Fireorm ?
以下為 Fireorm 官方文件 上對自己的介紹:
Fireorm is a tiny wrapper on top of firebase-admin that makes life easier when dealing with a Firestore database.
Fireorm is heavily inspired by other orms such as TypeORM and Entity Framework. The idea is that we:
1- define our model as a simple JavaScript class,
2- decorate our model with fireorm’s Collection decorator to represent a Firestore collection
3- use our model’s repository to do CRUD operations on your Firestore database.
# 可以不使用 Fireorm 嗎?
筆者認為,Fireorm 不一定要用。這邊使用的原因單純是因為 fireorm 可以簡便的定義 schema。
值得注意的是,在 Fireorm 的 Roadmap 還有很多 功能 pending 中。
# Firebase 環境設定
首先,先進入 firebase console 開啟一個新的專案,接著在 firestore database 按下開啟使用。最後到專案設定中 把 project 名稱跟 service account key 的 json 先記下來,待會會使用到。
# 開始實作!
先來看一下如何 initialize firebase。
這邊特別值得注意的是 筆者使用的 firebase-admin 版本是 v10,引用的方法跟 v9 有些許的不同,推薦看 官方文件 瞭解更多。
剛剛記下來的 project 名稱跟 service account key 這邊使用在 initialize firebase。
import * as functions from "firebase-functions";
import { initializeApp, cert } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";
import * as fireorm from "fireorm";
import { Request, Response, NextFunction } from "express";
import express = require("express");
import routes from "./routes";
const serviceAccount = require("../firestore.creds.json");
const app = express();
initializeApp({
credential: cert(serviceAccount),
databaseURL: `https://${process.env.API_PROJECT}.firebaseio.com`,
});
const firestore = getFirestore();
firestore.settings({
timestampsInSnapshots: true,
ignoreUndefinedProperties: true,
});
fireorm.initialize(firestore, {
validateModels: true,
});
app.use("/fire/", routes);
// error handling middleware
app.use(function (err: Error, req: Request, res: Response, next: NextFunction) {
//console.log(err);
res.status(422).send({ error: err.message });
});
// listen for requests
app.listen(process.env.port || 4000, function () {
console.log("Ready to Go!");
});
# 實作 schema
這邊把 每個 document 都會有的 Timestamp 整理做一個統整。
import { Timestamp } from 'firebase-admin/firestore';
export abstract class AbstractSchema {
createdAt: Timestamp;
updatedAt: Timestamp;
deletedAt: Timestamp | null;
constructor() {
this.createdAt = Timestamp.now();
this.updatedAt = Timestamp.now();
this.deletedAt = null;
}
public async paranoid(): Promise<void> {
this.deletedAt = Timestamp.now();
}
}
import { Collection } from 'fireorm';
import { AbstractSchema } from './abstractSchema';
import { ICompanySchema } from '../interfaces/company.interface';
@Collection('companys')
export default class CompanySchema
extends AbstractSchema
implements ICompanySchema
{
id: string;
name: string;
stores: Array<string>;
}
# 從上面這段程式碼,看一下我們如何使用 fireorm 簡便的定義 schema
在範例中 companys collection 中預計會存放 name, stores 等資料,但是為什麼會有一個 type 是 string 的 id 呢? 因為這是 Firestore 會用 id 來辨識 document! 推薦看 Firebase 官方文件 瞭解更多。
在 Firestore 中我們儲存資料在 document,而 documents 組成 Collection! 這邊 fireorm 使用了 decorate 的方式宣告 Collection。 fireorm 在背後做了哪些事呢? 他會讓我們宣告的 Collection 中的 instance 變成 Firestore 的 Document!推薦看 Fireorm 官方文件 瞭解更多。
# 實作 interface
export interface ICompanySchema {
id: string;
uuid: string;
name: string;
stores: Array<string>;
}
export interface createCompanyReq
extends Omit<
ICompanySchema,
| 'createdAt'
| 'updatedAt'
| 'deletedAt'
| 'paranoid'
| 'id'
> {
isMainStore: boolean;
}
export interface createCompanyRes
extends Omit<
ICompanySchema,
'createdAt' | 'updatedAt' | 'deletedAt' | 'paranoid'
> {}
# 實作 repository
# 在開始之前先來看一下,什麼是 InversifyJS ?
以下為 InversifyJS 官方文件 上對自己的介紹:
InversifyJS is a lightweight (4KB) inversion of control (IoC) container for TypeScript and JavaScript apps. A IoC container uses a class constructor to identify and inject its dependencies.
簡單來說,InversifyJS 讓我們可以簡便的實作 Dependency injection。 推薦看 InversifyJS 官方文件 瞭解更多。
# 什麼是 Dependency injection ?
Dependency injection (DI) is a very simple concept that aims to decouple components of your software and ease their integration and testing.
簡單來說,Dependency injection 的概念是讓我們在設計架構上可以提高可測試性與重用性 。 推薦看 Introduction to Design Patterns and Dependency Injection 瞭解更多。
import {
getRepository,
BaseFirestoreRepository,
runTransaction,
} from 'fireorm';
import { injectable } from 'inversify';
import CompanySchema from '../schemas/company';
import {
ICompanySchema
createCompanyRes,
} from '../interfaces/company.interface';
export interface ICompanyRepository {
create(name: string): Promise<createCompanyRes>;
findOne(name: string): Promise<ICompanySchema>;
}
@injectable()
export class CompanyRepository implements ICompanyRepository {
private companyCollection: BaseFirestoreRepository<CompanySchema>;
constructor() {
this.companyCollection = getRepository(CompanySchema);
}
public async create(name: string): Promise<createCompanyRes> {
let company = new CompanySchema();
return await runTransaction(async (tran) => {
const companyTranRepository = tran.getRepository(CompanySchema);
company.name = name;
return await companyTranRepository.create(company);
});
}
public async findOne(id: string): Promise<ICompanySchema> {
const company = await this.companyCollection.findById(id);
return company;
}
}
# 從上面這段程式碼,看一下我們如何使用 fireorm 在 getRepository 中提供的 CRUD 方法
在 Repository 方面 fireorm 應用了 Repository Pattern,因此上方的 companyTranRepository 可以直接使用 create 方法在 Firestore 的 collection 中新增 documents。
使用
@injectable
&@inject
decorators 宣告 dependencies。
# 實作 service
import { CompanyRepository } from '../repositories/companyRepository';
import { injectable, inject } from 'inversify';
import {
ICompanySchema,
createCompanyReq,
createCompanyRes,
} from '../interfaces/company.interface';
@injectable()
export class CompanyService implements ICompanyService {
constructor(
@inject('CompanyRepository') private companyRepository: CompanyRepository
) {
this.companyRepository = new CompanyRepository();
}
async firnOrCreateCompany(
companyUuid: string,
companyName: string
) {
try {
let company;
let isMainStore: boolean = false;
/* find or create company by uuid */
if (companyUuid) {
company = await this.companyRepository.findOne(companyName);
} else {
company = await this.companyRepository.create(companyName);
isMainStore = true;
}
return { company, isMainStore };
} catch (error) {
throw error;
}
}
}
# 實作 inversify config
import 'reflect-metadata';
import { Container } from 'inversify';
import { CompanyService } from '../services/companyService';
import { CompanyRepository } from '../repositories/companyRepository';
const container = new Container();
// Services
container.bind<CompanyService>('CompanyService').to(CompanyService);
// Repositories
container.bind<CompanyRepository>('CompanyRepository').to(CompanyRepository);
export default container;
從上面這段程式碼可以看到,在 inversify.config.ts 中,我們新增和定義了 Container。
# 實作 controller
import { NextFunction, Request, Response } from 'express';
import container from '../config/inversify.config';
import { CompanyService } from '../services/companyService';
export class StoreController {
public static async buildStore(
req: Request & Express.CustomRequest,
res: Response,
next: NextFunction
) {
try {
const companyName: string = req.body.companyName; // option
/* clarify the type for companyUuid while assign value */
let companyUuid: string = req.headers.companyuuid!; // option
const companyService = await container.get<CompanyService>(
'CompanyService'
);
/* find or create company */
const {company, isMainStore} = await companyService.firnOrCreateCompany(
companyUuid,
companyName
);
return res.status(200).json({
code: 200,
message: '執行成功',
});
} catch (e) {
console.log(e);
next(e);
}
}
}
export default StoreController;
從上面這段程式碼可以看到,controller 中我們將 container 使用 get<T>
方法,在這邊 resolve 了 dependency。
# 實作 route
import { Router } from 'express';
import StoreController from '../controller/storeController';
const router: Router = Router();
require('dotenv').config();
// create store
router.post(
'/store',
StoreController.buildStore
);
export default router;
# firestore database
成功新增完資料後,進到 Firestore database 看一下吧!
# 小結
這次實作的過程認識了不同的架構設計概念,整體來說蠻有趣的!
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃
# 參考資料
- Documentation | firestore
- Documentation | fireorm
- Documentation | inversify
- Hackernoon | design-patterns
關於作者
喜歡有趣的設計