Gitをアプリケーションデータベースに変える


今日は、アプリケーションを作成している場合は、選択するデータベースの多くのオプションがあります.
あなたが実現しないかもしれないことは、いくつかの大きな利点を持つオプションの一つとしてGitを考慮することができます.どうしてそんなことをするのですか.さて、先にお読みください.
最近、私は私のニーズのための個人的なプロジェクトとして弟と簡単な請求書アプリケーションを作成していた.非常に最初から、我々は、単一のユーザーモードでローカルアプリケーションを実行することを意図したが、我々は複数の場所からデータにアクセスできるようにする必要がありました.
過去に、Gitと共にファイルシステムを使用する可能性について議論し、シンプルで強力なデータベースを作成しました.畝

なぜgit?
アプリケーションデータベースとしてgitバックファイルシステムを使用する利点を見てみましょう.
  • 歴史-それに直面して、一般的なデータベースの歴史を管理することは通常、楽しみではない.あなたは、現在アクティブなレコードを得るために日付をチェックする必要がありますレコードの完全なテーブルで終わるでしょう.または履歴テーブルにレコードを移動する必要があります.いずれにせよ、それは退屈です.しかし、Gitでは、基本的に無料で歴史を取得します.あなたも、バージョンを比較することができるし、また、誰が知っているときに、なぜ変更した.
  • 無料でホスティング-今日は、任意のコストなしでBitbucketやGithubのプライベートレポを作成することができます.一般的なデータベースの無料ホスティングに比べて、行の制約は、読み込み/書き込みの制約はない、と私は両方のgithubとbitbucket信頼性の高いプロバイダを考慮します.
  • 人間の読み取り可能なデータストア-プレーンテキストファイルにデータを格納することにより、あなたもお気に入りのテキストエディタでデータを編集することができます.おかげで、すぐにいくつかのデータ、または迅速にプロトタイプ新機能をハックできます.たとえば、古いインボイスをクローンする必要がありますが、まだこの機能をアプリケーションに実装する時間がありませんでしたか?いいえ問題は、単にファイルをコピーして、お気に入りのテキストエディタを開き、完了です.ほとんどのデータベース管理ツールよりもずっと簡単です.
  • 分散して接続されていないデータベース- Gitについての大きなことは、分散された自然です.各ローカルリポジトリには、完全な履歴が含まれます.リポジトリのうちの1つが死ぬならば、それは通常データの損失を意味しません.また、任意の追加頭痛なしでオフラインモードで動作することが可能です.
  • コンフリクトの競合- 2つのユーザーが同じエンティティを変更すると、競合解決が必要です.それはしばしば、後の変更が勝つか、または最初の1つを意味します.他方、Gitはユーザーに手動で衝突を合併する能力を与えます、そして、多くの場合、自動的に変化を合併することができます.
  • さて、Gitアプリケーションデータベースの利点が何かを知っているときは、実装に移りましょう.

    文書の保存方法
    まず、どのように我々のデータベースをモデル化するかを決定する必要があります.我々は、ドキュメントのコレクションやファイルのフォルダを使用します.

    この方法では、1つのコレクションに格納されているすべてのエンティティを収集するのは簡単ですが、それはまた、人間のために読みやすいです.私たちは非裸のリポジトリを使うつもりです.したがって、直接ファイルにアクセスすることができます.
    ここで面白い記事を指摘しましょうhttps://www.kenneth-truyers.net/2016/10/13/git-nosql-database/ それはまた、Gitをデータベースとして使用するという概念を扱っていますが、それは裸のリポジトリを使用しています.
    ルックアップを高速化するには、ファイル名に直接ドキュメントIDを保存します.この方法では、IDを取得するファイルを開く必要はありません.また、ファイル名により多くのフィールドを格納して、インデックスの種類を作ることも可能です.
    文書ファイルのファイル形式として、YAMLを使用します.YAMLはデータをシリアル化するのに最適ですが、Gitの歴史の中でDiffsをチェックすることは、私たちにとって簡単になります.

    パフォーマンスを持ちましょう
    ので、今私たちのファイル構造を持っているとき、速度の任意の意味を持って、我々はキャッシュが必要です.我々は2つのレベルでキャッシュを行うつもりです.まず、コレクションをキャッシュし、個々のドキュメントをキャッシュします.
    個々のドキュメントのキャッシュは、より興味深いので、私たちはそれを開始しましょう.私たちは、ドキュメントを永久にメモリに保管しないほうがよいでしょう.しかし、むしろ、彼らはTTL(生きている時間)が指定されるべきです.実際の実装のためにnode-cache 図書館.
    async function getDocument(collection: string, id: string): Promise<any> {
      const documentCacheKey = getDocumentCacheKey(collection, id);
      let document = documentCache.get(documentCacheKey);
      if (!document) {
        const file = getDocumentPath(collection, id);
        const fileContent = await readFile(file, 'utf-8');
        document = yaml.safeLoad(fileContent) || {};
        (document as any)['id'] = id;
        documentCache.set(documentCacheKey, document);
      }
      return document;
    }
    function getDocumentCacheKey(collection: string, id: string) {
      return `${collection}-${id}`;
    }
    function getDocumentPath(collection: string, id: string) {
      return path.join(baseDir, collection, `${id}.yaml`);
    }
    
    まず、与えられたドキュメントのキャッシュキーを取得します.このために、必要なドキュメントのコレクション(フォルダ)名とIDを連結します.次に、このキーを使ってキャッシュ内のドキュメントにアクセスします.そこになければ、ファイルシステムからロードし、キャッシュキーの下にキャッシュに格納する必要があります.
    YAMLファイルの解析のためにjs-yaml 図書館.
    コレクションをキャッシュするには、配列の単純な辞書が十分です.私たちはこの場合、TTLを必要としません.なぜなら、実際のドキュメントよりも頻繁にコレクションを照会し、実際のメモリの内容はかなり小さいからです.
    async function getCollection(collection: string): Promise<string[]> {
      if (!collectionCache[collection]) {
        collectionCache[collection] =
          (await glob(path.join(baseDir, collection, '*.${yaml}')))
            .map(p => ({
              id: path.basename(p, path.extname(p)),
              path: p
            }));
      }
      return collectionCache[collection]!
        .map(f => f.id);
    }
    

    テキストエディタ
    我々はファイルシステムからうまくアクセス可能なすべてのドキュメントを持っているので、アプリケーションの外部での変更をサポートしないのは恥です.このように、ユーザビリティパターンを処理することができ、アプリケーションで直接サポートしていません.例えば、我々は以前に述べたように、我々はアプリケーションにこの機能を追加する前に請求書をコピーすることができます.
    このためには、ファイルウォッチャーをchokidar 図書館.
    watcher = chokidar.watch(`**/*.yaml`, { cwd: baseDir });
    watcher
      .on('add', file => onChange(file, 'add'))
      .on('unlink', file => onChange(file, 'unlink'))
      .on('change', file => onChange(file, 'change'));
    
    function onChange(file: string, changeType: 'add' | 'unlink' | 'change') {
      const collection = path.basename(path.dirname(file));
      const id = path.basename(file, path.extname(file));
      invalidateDocumentInCache(collection, id, changeType !== 'change');
    }
    function invalidateDocumentInCache(collection: string, id: string,
      invalidateCollectionCache: boolean) {
      const documentCacheKey = getDocumentCacheKey(collection, id);
      documentCache.del(documentCacheKey);
      if (collectionCache && invalidateCollectionCache) {
        collectionCache[collection] = null;
      }
    }
    
    YAMLファイルへの変更を検出する場合、ノードキャッシュから削除します.ファイルを削除、追加、または名前を変更(アン+リンク)を追加した場合、コレクションキャッシュ内のコレクション全体をクリアします.

    を探しましょう
    最後に、実際のクエリを行うために準備されたすべてを持っています.我々は2つのレベルでそれを行うつもりです.IDとコンテンツによるクエリ.私たちはmicromatch IDによる問い合わせのためのライブラリ.
    async function getAllIds(query?: IdQuery): Promise<string[]> {
      let ids = await getCollection(collectionName);
      if (query && query.id) {
        ids = ids.filter(d => micromatch.isMatch(d, query.id!));
      }
      return orderBy(ids, 'id');
    }
    
    MicroMatchはIDに基づいて簡単なクエリを実行することを可能にします.例えば、請求書の場合、各IDは年と数字で構成されています.特定の年に作成されたすべてのインボイスを取得するには、"${ year }*"クエリを使用できます.
    ファイルの内容で問い合わせを行うために、単純なフィルタリングを使用します.
    let docs = await Promise.all(
        collection.map(doc => doc.getDocument(collectionName, doc)));
    if (query && query.where) {
      docs = docs.filter(query.where)
    }
    

    それを保存してください
    ローカルおよびリモートのgitリポジトリを同期させるには、定期的にプルしてプッシュする必要があります.つのページ・アプリケーションのために、引っ張る最高の時間はページ・リフレッシュの間、あります.各変更後にプッシュすることが可能ですので、リモートリポジトリをできるだけ早く更新されます.実際のGit処理のために、我々は使用するつもりですsimple-git git操作を簡単にするライブラリ.
    async function pull(): Promise<any> {
      await ensureRepo();
      await repo!.pull();
    }
    async function commitAndPush(message: string): Promise<any> {
      await ensureRepo();
      await repo!.add('.');
      var commitRes = await repo!.commit(message);
      var pushRes = await repo!.push();
    }
    async function ensureRepo() {
      if (!repo) {
        repo = simplegit(db.dir);
      }
    }
    
    実際のアップデートコードでは、
    async function update(invoice: InvoiceUpdateModel): Promise<InvoiceDocument> {
      let invoiceDocument = await db.invoices.single(invoice.id);
      invoiceDocument = { ...invoiceDocument, ...invoice };
      invoiceDocument = await db.invoices.update(invoiceDocument);
      await repoService.commitAndPush(`invoice(${invoice.id}):updated`);
      return invoiceDocument;
    }
    

    結論
    私は結果に満足していると言わなければならない.この特定のユースケースのために、Gitはアプリケーションデータのデータベースとして非常に素晴らしく働きます.もちろん、パフォーマンスは一般的なデータベースソリューションに比べてはるかに低いです.また、全体の概念は、リモートサーバ上でホストされているアプリケーションの場合には理想的ではない.しかし、一方、それは本当に簡単で、安いです、そして、それは我々に文書の速いハッキングをする能力を与えます.
    ここで請求書アプリケーションのコード全体にアクセスできますhttps://github.com/pruttned/owl-invoice