ActiveRecordっぽくFirestoreを操作できるgemを作りました（ActAsFireRecordBeta）

## はじめに
ActiveRecordっぽくFirestoreを操作できるgemを作りました。

https://github.com/JunichiIto/act_as_fire_record_beta

## 概要

こんなモデルを定義します。

```ruby 
class Book
  include ActAsFireRecordBeta

  # 属性の名前と型を定義
  firestore_attribute :title, :string
  firestore_attribute :published_on, :date
  firestore_attribute :page, :integer

  # 必要に応じてバリデーションも追加
  validates :title, presence: true
end
```

CRUDする例です。

```ruby 
# Create
book = Book.new(title: 'An Awesome Book', published_on: '2022-12-01'.to_date, page: 200)
book.save
bool.id #=> IdG0ZWT0I5DucEQuRgoP

# Read
id = 'IdG0ZWT0I5DucEQuRgoP'
book = Book.find(id)

# Update
book.update(page: 210)

# Delete
book.destroy
```

データを検索する例です。

```ruby 
book = Book.find_by(title: 'An Awesome Book')

books = Book.all

books = Book.order(:title)
books = Book.order(:title, :desc)

books = Book.where(:page, :>=, 200)
books = Book.where(:page, :>=, 200).order(:page)
```

実際にデータの登録や検索を実行しているのは [google-cloud-firestore](https://rubygems.org/gems/google-cloud-firestore) というgemです。

`order(:title, :desc)`や`where(:page, :>=, 200)`みたいな書き方はActiveRecordと少し異なりますが、これはgoogle-cloud-firestoreのAPIをそのまま利用しているためです。

https://www.rubydoc.info/gems/google-cloud-firestore/2.7.2/Google/Cloud/Firestore/Query

また、FirestoreはいわゆるNoSQLであって、RDBMSではありません。そのため、検索にも独特の制約があります。Firestoreが持っている制約はこのgemを使った場合にもそのまま当てはまります。

```ruby
# 検索の制約に引っかかってエラーが発生する例 
Book.where(:page, :>=, 200).order(:title).first
#=> GRPC::InvalidArgument: 3:inequality filter property and first sort order must be the same: page and title.
```

### APIについてもっと詳しく

ドキュメントをまだちゃんと整備していないので、このgemが提供しているAPIを知りたい場合は以下のテストコードをチェックしてみてください。

- [test/act_as_fire_record_beta_test.rb](https://github.com/JunichiIto/act_as_fire_record_beta/blob/main/test/act_as_fire_record_beta_test.rb)
- [test/google/cloud/firestore/query_test.rb](https://github.com/JunichiIto/act_as_fire_record_beta/blob/main/test/google/cloud/firestore/query_test.rb)

とはいえ、このgemが実装しているActiveRecord風のAPIは、オリジナルのActiveRecordのごく一部です。。

## このgemを作った動機
HerokuのPostgreSQLが有料化されたので、個人開発していたRailsアプリの運用コストを下げるためにFirestoreを使おうと思いました。そのあたりの思考過程は以下のブログにまとめています。

https://blog.jnito.com/entry/2022/10/04/104100

Firestoreなら1日当たりのオペレーションが以下の上限を超えなければ無料で使えます（2022年12月現在の料金設定）。

- 読み取り = 5万件
- 書き込み = 2万件
- 削除 = 2万件

個人で細々と運用しているRailsアプリであれば無料枠で十分まかなえるはずです。

https://cloud.google.com/firestore/pricing?hl=ja

Firestoreを操作するには前述のgoogle-cloud-firestore gemを使えばいいのですが、記述スタイルがActiveRecordとかなり異なります。

```ruby
# 引用元 https://github.com/googleapis/google-cloud-ruby/blob/main/google-cloud-firestore/README.md
city = firestore.col("cities").doc("SF")
city.set({ name: "San Francisco",
           state: "CA",
           country: "USA",
           capital: false,
           population: 860000 })

firestore.transaction do |tx|
  new_population = tx.get(city).data[:population] + 1
  tx.update(city, { population: new_population })
end 
```

そこでコードの修正量を必要最小限にするため、なるべくActiveRecordのインターフェースに近づけた形でFirestoreを操作できるようにしたいと思ってこのgemを作りました。

## セットアップ

このgemはRailsアプリケーションでの使用を前提としています。
Gemfileに以下の行を追加して`bundle install`してください。

```ruby:Gemfile 
gem "act_as_fire_record_beta"
```

### Firebase Emulatorを使う場合 (development or test)

- [Firebase Emulatorをインストール](https://firebase.google.com/docs/emulator-suite/install_and_configure)します。
- `firebase init`でプロジェクト用のFirestoreをセットアップします。
- [このWikiページ](https://github.com/JunichiIto/act_as_fire_record_beta/wiki/.gitignore-example)を参考にして`.gitignore`の設定を追加します。
- 環境変数`FIRESTORE_EMULATOR_HOST`にEmulatorのhost情報を設定します。（`export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080`など）
- `firebase emulator:start`でEmulatorを起動し、それからRailsを起動します。
  - `firebase emulators:start --import=./firebase-local --export-on-exit` のように `--import` オプションと `--export-on-exit` オプションを付けると、作成したデータをローカルに保存できます。
  - データを保存するディレクトリは`.gitignore`に追加しておきましょう。
- `firebase emulators:exec 'bundle exec rspec' --only firestore` のようなコマンドを入力すると、Emulatorの起動、テストの実行、Emulatorの終了が一度に行えます。

### 本物のFirestoreを使う場合

- Firebaseプロジェクトを作成し、 プロジェクト内にFirestoreを追加します。

![Screen Shot 2022-12-28 at 15.21.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/1f59b250-89d8-82a2-3139-a9f56cd5d3be.png)

- 「https://console.cloud.google.com/ > 左サイドメニュー > APIとサービス > 認証情報 > サービスアカウント > 名前=App Engine default service account のアカウントの鉛筆マークをクリック > キータブ > 鍵を追加 > 新しい鍵を作成 > JSON形式」でCredentialをダウンロードします。

![key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/134e8fce-9fbe-a4a0-fc83-162d1614c756.png)

- CredentialをRailsプロジェクト内の任意のディレクトリに配置します。
  - CredentialがGitHubにpushされないよう、必ず`.gitignore`にCredentialのパスを追加してください。
- （ローカルで実行する場合）環境変数`GOOGLE_APPLICATION_CREDENTIALS`にCredentialへのパスを追加します。（`export GOOGLE_APPLICATION_CREDENTIALS=path/to/your-key.json`など）
- （サーバーで実行する場合）環境変数`FIRESTORE_CREDENTIALS`にCredentialの内容（JSON文字列）を書き込みます。
- `config/initializers/firestore.rb` に`project_id`を設定します。

```ruby 
require "google/cloud/firestore"

Google::Cloud::Firestore.configure do |config|
  config.project_id = "your-awesome-project"
end
```

- 作成したデータ（ドキュメント）は「(モデル名)-(実行環境)」という名前のコレクションに保存されます（例：`books-development`、`blog_comments-production`）

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/efab06a1-ac57-a01a-e530-e17a62009378.png)

## 実際の利用例
[妻のパン屋のホームページ](https://coupe-baguette.com/
)で表示している"INFORMATION"や"Instagram"の情報はFirestoreに保存したデータを取得しています。

![Screen Shot 2022-12-28 at 11.01.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/2d0b9a19-ce6f-534e-3150-899a66fbabbc.png)

https://coupe-baguette.com/

## 制限事項等

- このgemはあくまでActiveRecord風のAPIを提供しているだけであり、ActiveRecordと完全な互換性があるわけではありません。そのため、ActiveStorageやDevise、Kaminari、Ransackなど、ActiveRecordの使用を前提としたgemやフレームワークをFirestoreに置き換えることはできません。
- FirestoreはいわゆるNoSQLであるため、RDBMSとは根本的に異なるデータ設計を考える必要があります。このgemを使ってActiveRecord風のAPIを手に入れたとしても、根本的なデータ設計についてはNoSQLのベストプラクティスに従う必要があります。
- gem名に"Beta"を入れているように、まだ出来たてホヤホヤのgemです。足りない機能はたくさんありますし、今後APIがガラッと変わる可能性もあるので注意してください。バージョンごとの変更点は[CHANGELOG.md](https://github.com/JunichiIto/act_as_fire_record_beta/blob/main/CHANGELOG.md)にまとめてあります。

## まとめ

というわけで、本記事ではActAsFireRecordBetaというActiveRecordっぽくFirestoreを操作できるgemを紹介しました。

本格的なRailsアプリを作る場合は全然役不足だと思いますが、「たったこれだけしかデータを保存しないのに、毎月5ドル（約670円）を毎月Herokuに払うのもな〜」という規模であればこのgemがうまくフィットするかもしれません。興味がある方は使ってみてください！

https://github.com/JunichiIto/act_as_fire_record_beta