Rails × Hasuraの共存環境を作ってみる

こんにちは！
気持ちは永遠の新卒1年目、いつの間にか3年目の株式会社エイチームコマーステック北村 (@NamedPython )です。

今日は、コマーステックがもしかしたら辿るかもしれない技術スタックを先行して検証していきます。
試す技術はタイトルにもある通り「**Rails × Hasura**」です。
役割分担としては、

- **Rails(API mode)**
    - マイグレーション
    - ビジネスロジック
- **Hasura**
    - GraphQL Engine
        - 単純なCRUD
    - ゲートウェイ
    - スケジューラー

みたいな感じにしたいな〜と想像しています。
シンプルなCRUDのGraphQLは**Hasura**にお任せ、複雑な処理は**Actions**で**Rails**に流したり、リゾルバを書いて**Remote Schemas**で**Hasura**に取り込んだりしてもいい。**Rails**でリゾルバを書くのが面倒なら別のバックエンドを立ててしまえばいい。これが実現できると、分厚くなりがちな**Rails**を徐々にマイクロにしていきつつ、他のスタックに移行するなり、**Rails**のアップグレードや改善に取り組める。という妄想をしているのです。

:::note warn
本当はMySQLなRails × HasuraのMySQL-previewな構成で試したかったんですが、IssueやDiscussionを見るにHasura社内の開発的にもMySQL対応の難易度が高いようで、公開されているもの↓は実用レベルになかったのでPostgreSQL構成で書いています。
:::

https://hasura.io/docs/latest/graphql/core/guides/mysql-preview.html

## 環境をつくっていく

それでは早速作っていきましょう。
以下のような構成を作ります。

```:プロジェクト構成
rails-hasura/        <- current, project dir
  ├ hasura
  │   └ config.yaml
  ├ rails
  │   ├ Dockerfile
  │   ├ Gemfile
  │   └ Gemfile.lock
  └ docker-compose.yaml
```

`./rails/Gemfile`、`./rails/Dockerfile`、`./docker-compose.yaml`、は以下です。

```ruby:./rails/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'rails', '~> 6.1.4', '>= 6.1.4.4'
```

```dockerfile:./rails/Dockerfile
FROM ruby:2.6.5

RUN apt-get update && apt-get install -y \
  build-essential \
  libpq-dev \
  nodejs \
  default-mysql-client \
  yarn

WORKDIR /rails-app

COPY Gemfile Gemfile.lock /rails-app/

RUN bundle install
```

```yaml:./docker-compose.yml
version: '3.8'

services:
  rails:
    build:
      dockerfile: Dockerfile
      context: ./rails
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
        - 3000:3000
    volumes:
      - ./rails:/rails-app
    tty: true
    stdin_open: true
  hasura:
    image: hasura/graphql-engine:v2.1.1.cli-migrations-v3
    ports:
      - 8080:8080
    depends_on:
      - db
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:password@db:5432/postgres
      HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false'
      HASURA_GRAPHQL_ENABLE_CONSOLE: 'true'
      HASURA_GRAPHQL_DEV_MODE: 'true'
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
      HASURA_GRAPHQL_ADMIN_SECRET: devenvsecret
  db:
    image: postgres:12
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
volumes:
  db_data:
```

続いて`./hasura/config.yaml`です。

```yaml
version: 3
endpoint: http://localhost:8080/
admin_secret: devenvsecret
enable_telemetry: false
api_paths:
  v1_query: v1/query
  v2_query: v2/query
  v1_metadata: v1/metadata
  graphql: v1/graphql
  config: v1alpha1/config
  pg_dump: v1alpha1/pg_dump
  version: v1/version
metadata_directory: metadata
migrations_directory: migrations
seeds_directory: seeds
actions:
  kind: synchronous
  handler_webhook_baseurl: http://localhost:3000
  codegen:
    framework: ''
    output_dir: ''
```

さて、ここまで来たらあとは`rails new`の儀式です。

```bash
docker compose run rails rails new . --force --database=postgresql --skip-webpack-install
```

そうすると、ガーっとファイルが生成され、`bundle install`なども実行されるので待ちます。`Gemfile`, `Gemfile.lock`の中身も書き変わりますよ。

続いては、`./rails/config/database.yml`をいじる必要があるので、お好みのエディタで開き、次のように編集します。

```diff_yaml:./rails/config/database.yml
# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
#   gem install pg
# On macOS with Homebrew:
#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On macOS with MacPorts:
#   gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
#   gem install pg
#       Choose the win32 build.
#       Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem 'pg'
#
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ host: db
+ port: 5432
+ username: postgres
+ password: password
```

そうしましたらいよいよ起動でございます。

```bash
docker compose up --build -d

# 起動ログを見たい方は
docker compose logs -f
```

:::note
こちらのコマンド実行もちょっとかかると思うので猫でも撫でてください :cat: 
:::

## Railsからのマイグレーションをしていく

...とその前に個人的な好みと、現行開発の方針的に顧客の目に触れそうな識別子はUUIDv4にしているので、Railsでそれができるようにしていく。

### 余談: RailsでUUIDv4なプライマリキー

:::note warn
この手順を踏むと一瞬でPostgreSQLに依存するマイグレーションが走ったりしちゃうので注意されたし。
:::

こちらの記事を参考にさせていただきました。インターネットに**感謝** :pray: 

https://tech.mof-mof.co.jp/blog/rails-use-uuid/

手順書こうかと思いましたが、ほぼ記事ままなので割愛。以下からは適用済みの前提があることをご認識ください。

### `rails g model` -> `rails db:migrate`

エイチーム**コマーステック**ですし、EC的にありきたりなテーブル構造を組みつつ、それがHasuraからどう認識されるかみてみましょう。一度に複数商品注文できるようにすると面倒なので、1注文1商品で。

- お客さん `customers`
- 製品 `products`
- 注文 `customer_orders`
    - 買ったお客さん `customer_orders`.`customer_id` -> `customers`.`id`
    - 買った商品 `customer_orders`.`product_id` -> `products`.`id`

```bash
docker compose exec rails bin/rails g model customer
docker compose exec rails bin/rails g model product
docker compose exec rails bin/rails g model customer_order
```

`./rails/db/migrate/`配下にできたそれぞれのファイルを以下のように編集！

```ruby:./rails/db/migrate/#{timestamp}_create_customers.rb
class CreateCustomers < ActiveRecord::Migration[6.1]
  def change
    create_table :customers, id: :uuid do |t|
      t.string :name, null: false
      t.string :email, null: false

      t.timestamps
    end
  end
end
```

```ruby:./rails/db/migrate/#{timestamp}_create_products.rb
class CreateProducts < ActiveRecord::Migration[6.1]
  def change
    create_table :products, id: :bigint do |t|
      t.string :label_name, null: false
      t.integer :price, null: false, comment: 'tax excluded'
      t.string :description

      t.timestamps
    end
  end
end
```

```ruby:./rails/db/migrate/#{timestamp}_create_customer_orders.rb
class CreateCustomerOrders < ActiveRecord::Migration[6.1]
  def change
    create_table :customer_orders, id: :uuid do |t|
      t.string :order_code, null: false
      t.datetime :sold_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
      t.references :customer, type: :uuid, foreign_key: true
      t.references :product, foreign_key: true
      t.integer :price, null: false
      t.integer :price_with_tax, null: false

      t.timestamps
    end
  end
end
```

そしていよいよマイグレーション実行〜

```bash
docker compose exec rails bin/rails db:migrate
```

## Hasuraで確認

さて続いてHasuraでどう認識されているかみてみましょう！
一旦見るだけなので http://localhost:8080/ から見ましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/6798c80a-ca7d-1265-bf84-d43e77b8b6f7.png)

おや、`Enter admin-secret`とありますね。大丈夫、まだ慌てる時間じゃありません。
`./docker-compose.yml`の`hasura`サービス内の環境変数に`HASURA_GRAPHQL_ADMIN_SECRET`として設定されている値を入力すればOKです(今回は`devenvsecret`)。

続いての画面はGraphiQLですね。ここも使うんですが今見たいのは画像内の`DATA`タブです。
![スクリーンショット 2021-12-25 5.18.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/5eb5ac10-6705-8a58-aece-817d6450969f.png)

そして次もまたよくわからん画面が出るとは思いますが、続いては`public`スキーマをポチー

![スクリーンショット 2021-12-25 5.23.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/4685cf35-5117-dca4-5cc8-e4312a62d5c0.png)

そうすると、こういった表示になるかと思います。

![スクリーンショット 2021-12-25 5.25.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/a7ad7738-4557-be01-8545-6ada191cadbd.png)

おや、赤枠内をみると`customer_orders`や`customers`、`products`が見えますね。
他も見えてしまっていますが、これは`ActiveRecord`のためのメタデータ保管庫でしょう。

`Untracked tables or views`とのことなので、まあ**Hasura**的に未追跡なんでしょう。
`Track`のボタンを押してしまいたくなりますが、永続化できなくなってしまうので**Real console**を立ち上げます。

## Hasura CLIのインストールとReal consoleの立ち上げ

[公式ドキュメント](https://hasura.io/docs/latest/graphql/core/hasura-cli/index.html)に従い、Hasura CLIをインストールしてください。筆者はHomebrewで入れました。

続いてこんな感じでコマンドをぽちぽち。

```bash
hasura --project hasura console
```

すると、規定のブラウザで勝手に**Real console**が開きます。
「え、何が違うん？」と思うかもしれませんが、僕も見た目の違いはわかりません :yum:

まあポートが違うのでそこで見分けてください。
このコンソールは、さっきの`:8080`なコンソールと違い、スキーマや設定に影響が出る変更をすると、設定ファイルとして吐き出され、永続化できます。

## Hasuraでtrack、metadataの設定

さて、これで永続化される状態になったので、`./hasura/`下に吐かれるファイルをみつつテーブルを`Track`したり、リレーションを定義していきましょう。

### テーブルのtrack

さて、**Hasura**にテーブルを認識してもらうために、`Track`しましょう。
さっきと同じ手順で、DATAタブ > publicスキーマのところまでいきましょう。そして、見覚えのあった3つのテーブルの`Track`ボタンを押していきます。

まずは`customers`テーブル....

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/b9973dd7-950b-01f6-fc18-50452056bc52.png)


おお〜〜〜〜〜〜〜〜〜〜色々読み込まれとる〜〜〜〜〜〜〜〜！

そしてファイルはどんな感じで吐かれているかというと...?

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/c89ac7a5-1a44-c423-1ac3-2c4fc2336e99.png)

め、めちゃくちゃ増えてる...
そしてよく見てみると、今回`Track`した`customers`テーブルの名前がついたファイルがありますね(`./hasura/metadata/databases/default/tables/public_customers.yaml`)。
中身はどんな感じかというと...？

```yaml
table:
  name: customers
  schema: public
```

おっ...、うん...。割とシンプルな感じですね。
まあ`Track`しただけならこんなもんなので、残りのテーブルも`Track`しちゃいましょう。

![スクリーンショット 2021-12-25 5.55.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/048ebc26-8906-8382-b242-1fba8b5da1d8.png)

左のツリーにはテーブルが増え、なにやら別種の`Untracked ...`も増えてきました。
どうやらこれは、外部キー制約をもとに判断した「これrelationships貼ったほうがいいんじゃないの？」という**Hasura**のレコメンデーションみたいです。

ざっと見ただけでも、

- お客さんは注文をいっぱい持ってる `customers` -> `[ customer_orders ]`
- 製品はいっぱい注文されている `products` -> `[  customer_orders ]`
- 注文したお客さんを辿れる `customer_orders` -> `customers`
- 注文された製品を辿れる `customer_orders` -> `products`

と列挙されているので、いい感じですね。賢いッ！

### リレーションのtrack

せっかく上記のようにレコメンドが出ていますし、全部良さそうなので`Track All`しちゃいましょう。
`alert(...)`で確認され、OKするとそのままレコメンドだけが消えて、完了のtipsが出ますね。

ファイルの変更を確認してみましょう。

```diff_yaml:./hasura/metadata/databases/default/tables/public_customers.yaml
  table:
    name: customers
    schema: public
+ array_relationships:
+   - name: customer_orders
+     using:
+       foreign_key_constraint_on:
+         column: customer_id
+         table:
+           name: customer_orders
+           schema: public
```

お〜、やはりリレーションが追加されるのもちゃんとファイルに吐かれますね。

### テストデータを入れてGraphiQLで読み書きする

さて、いよいよ下準備整ってきたので、ラストの検証します。
まずはテストデータですね。こんなところでしょうか。

- `customers`
    - `name`: NamedPython
    - `email`: namedpython@namedpython.local
    - `allowed_to_send_emails`: true
- `products`
    - `label_name`: ゆずシトラスティー
    - `price`: 390
    - `description`: パッションティーカスタムが美味しい
- `customer_orders`
    - `order_code`: NAMED-ORDERCODE-123
    - `customer_id`: ↑
    - `product_id`: ↑
    - `price`: 390
    - `price_with_tax`: 429


テストコードは、`Rails` consoleでもいいですし、`Hasura` consoleでもできます。
後者でやるなら以下のような感じで入力画面にたどり着けます！

![スクリーンショット 2021-12-25 6.35.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/926d2a7f-afaa-c3c1-c436-1b9e66f9e751.png)

:::note warn
僕もこのタイミングで気づいたんですが、Rails製のcreated_at, updated_atはDB上にデフォルト値が設定されるわけではなくしかもNULL不可なので、後者でやる場合はcreated_at, updated_atもnow()で埋めてあげる必要があります。
ここはRails側のデフォルトのマイグレーション設定を書き換えてあげた方が良さそうなポイントですね。
:::

テストデータを投入できたら早速クエリしてみましょう！
これまで触ってきたDATAタブの左側、APIタブを開きましょう！

![スクリーンショット 2021-12-25 6.53.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/344ad52d-3504-5718-9d01-b172dca5e62c.png)

いつの間にか、ここにもいろいろできていますね。
では単純なところから行ってみましょうか。

```graphql
query ShowProductInformations {
  products {
    id
    label_name
    price
    description
  }
}
```

![スクリーンショット 2021-12-25 6.56.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/47aa1c13-098e-cd3b-6369-f0a441004f78.png)

お〜取得できましたね〜！
そしたらこういうのはどうですかね？

```graphql
query ShowCustomersProfileAndOrders {
  customers {
    id
    name
    email
    customer_orders {
      order_code
      price_with_tax
      product {
        label_name
        price
      }
    }
  }
}
```

![スクリーンショット 2021-12-25 6.59.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115622/7747ce9c-551b-6345-25af-91c1443ab3e7.png)


お〜、**has_many** / **has_one**な関係もちゃんとGraphQLでとれてますね！！！！

さて、GraphQLがいい感じに叩けるのがわかったところで、今回の検証は以上にしようと思います。
実は他にも`Enum`だったり`Computed Fields`だったりの検証もしたかったんですが、それはまた別の機会に:information_desk_person_tone1:

## おわりに + We're hiring!

ここまで読んでいただいてありがとうございました！

株式会社エイチームコマーステックには、僕みたいな新しいモノ好きがこういったことに挑戦できる土壌があり、そういった環境で「Eコマースを面白くしていきたいぜ」という方を募集しています:zap:

「ちょっとお話聞いてみたいぞ！」だったり、「Rails/Hasuraどんな感じで使ってるんだ？」が気になる方は、Twitter([@NamedPython](https://twitter.com/NamedPython))のDMやリプライまでお気軽にお声がけください！

コマーステックのコーポレートサイトや、採用ページもチェックしてみてくださいね！
それでは良い年末年始をお過ごしください！

https://commerce-tech.a-tm.co.jp

https://www.a-tm.co.jp/recruit/