Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

GCP Cloud Firestore をRailsから使う

スタディプラスでサーバーサイドを担当している花井です。

先日田口さんが投稿したこちらのプロジェクトで、実験的にCloud Firestore / Cloud StorageとRailsでAPIを構築したので、その顛末を紹介します。

Firestoreの理由

今回のプロジェクトの要件に、一度データを入稿してしまえば変更はほとんど必要ないような要件がありました。 極端なことを言えば、yamlかjsonファイルをマスタデータとして扱うという選択でもよかったのですが、それだと変更のたびにデプロイが必要なのとスケールするかが心配でした。 また、普段AWSを使っているので単純にGCP環境を使う実績解除という意味あいも多分にありました。そんなわけで、画像の置き場としてCloud Storage、データベースがわりにCloud Firestore を使う構成になりました。

Railsから使う

install Gem

Cloud Firestoreの操作には公式のGemを使いました。

gem 'google-cloud-firestore'

ローカルでテストしている時にGCPにつながらず、地味にはまりました。認証のための情報はこちらを参考に環境変数を設定しています。

データ構造と/modelsの構成

データ構造は以下のようにしています。

マスターデータCollection
|_ 親モデルDocument
|  |_ url : 遷移先URL
|  |_ banner_image_url : バナー画像のURL(Cloud Storageの公開URL)
|  |_ published: 公開フラグ
|  |_ 子モデルのMap
|  |  |_ 子モデル識別子
|  |    |_ [いろいろな情報]
|  |    |_ 孫モデルのMap
|  |_ [いろいろな情報]

Clientから取得するデータCollection
|_  Document
|   |_ 子モデル識別子
|     |_ [いろいろな情報]

DBとして扱うので、Railsのお作法にしたがってCloud Firestoreを操作するクラスは /modelsに定義しました。 GCPへの接続を各モデルに書かなくていいように、GCPへの接続とFirestore操作を司るクラスは別々に用意しています。

class Gcp
  require 'google/cloud'
  class_attribute :client

  self.client = Google::Cloud.new(ENV['gcp_project_id'])
end
class Firestore < Gcp
  class_attribute :connection

  self.connection = client.firestore
end

Firestoreから取得したデータをModelに変換するところはActiveModel::Attributesを使いました。

class Collection名 < FirebaseFirestore
  include ActiveModel::Model
  include ActiveModel::Attributes

  class_attribute :parents
  self.parents = connection.collection ENV['parent_collection_name']

  attribute :id, :string
  attribute :url, :string
  attribute :banner_image_url, :string
  attribute :published, :boolean

  attr_reader :child_model

  class << self
    def available
      parents.where('published', '==', true).get.map do |parent|
        new(parent_params(parent))
      end
    end

    private

    def parent_params(parent)
      # parentはGoogle::Cloud::Firestore::DocumentSnapshotを想定
      # parentのドキュメントIDをidとして扱うためにここで操作
      parent.data.merge({ id: parent.document_id })
    end
  end

  def child_model=(child_models)
    @child_models = child_models.map do |id, fields|
      params = { id: id }
      child_model_params = params.merge(fields)
      ChildModel.new(child_model_params)
    end
  end
end

今回の要件では一定数を超えないことがわかっていたので、child_model =の中でループを回してしまっていますが、もうちょっとうまいやり方もあったかなぁとは思っています。

はじめて使ってみてこのあたりのモデリングをした感想としては、階層関係が深くなったり、件数に制限がなくなったりするデータをCloud Firestoreから読むのは厳しそうと思いました。