Feedforce Developer Blog

フィードフォース開発者ブログ

Dynamoid の使い方【global_secondary_index 編】

どうも、バックエンドエンジニアのサトウリョウスケです ✌︎('ω')✌︎

前回に引き続き、 Dynamoid 第3弾です ✌︎('ω')✌︎

Rails で DynamoDB を利用する際の ORM として dynamoid があります。 今回は dynamoid から Global Secondary Index (GSI) を利用する方法について紹介します。

dynamoid の導入方法については以前書いたこちらの記事を参考にしてみて下さい。

tech.feedforce.jp

Global Secondary Index (GSI) ってなんぞ

今回も名称の整理をしておきます

文中に Hash Key やら Range Key という名称が出てきますが、現在は名称が異なります。 しかし、 dyanmoid では相変わらず旧名称のまま (hash_key, range_key) でパラメータを指定するので、今回も最初に対応表を記載しておきます。

旧名称 現名称
Hash Key Partition Key
Range Key Sort Key

GSI は検索のためのインデックス

DynamoDB にはプライマリキーの指定方法として、単一の Partition Key を使用する方法と、Partition Key と Sort Key を組み合わせて使用する方法があります。 これについては前回の【range 編】の記事でも触れました。

developer.feedforce.jp

プライマリキーに指定されたカラムに対してであれば、レコードの抽出や範囲検索などが実行可能となる訳ですが、プライマリキーに指定されていないカラムに対しては検索が実行できず、テーブルのフルスキャンを実行することになってしまい非効率です。そこで、別のカラムに対しても検索を行いたい場合は GSI を設定して、フルスキャンすることなく効率的にデータを抽出できるようにします。

GSI もプライマリキーの指定と同様に、単一の Partition Key のみで指定することも、 Partition Key と Sort Key の組み合わせで指定することも可能です。 ただし、 プライマリキーにはユニーク制約が設定されますが、 GSI にはユニーク制約が存在しない ので、その点には注意が必要です。

Local Secondary Index (LSI) もあるやで

LSI の Partition Key はプライマリキーと共通です。Sort Key の部分だけ別に設定したい場合に使用します。その点を除けば GSI とよく似ていますが、こちらはテーブルの作成時にしか定義することができないようです。

なお、LSI もdynamoid から指定することは可能 (local_secondary_index を使用する) ですが、本稿では触れません。

どういう用途で便利なのか

あまり複雑なテーブル設計が推奨されない DynamoDB ですが、簡単なテーブル間の関連付けを行いたいシーンが出てきます。親テーブルの ID を結合キーとして子テーブルに設定したい場合などに GSI は便利です。 以下に User Table と User Comment Table の例を示します。User Comment Table には親テーブルである User Table の ID が GSI の Partition Key として設定してあります。また、コメントの投稿日時 (Posted at) を GSI の Sort Key として設定しています。これで User 毎に投稿日時順にソートしたコメントを取得することができるようになります。

User Table

ID name
1 John
2 Marry
3 Taro

User Comment Table

ID (Primary Partition Key) User ID (GSI Partition Key) Posted at (GSI Sort Key) Comment
1 1 1509529916 Hello
2 1 1509530052 I am John
3 1 1509530085 How do you do?
4 2 1509523925 Thanks a lot!
5 3 1509527628 こんにちは
6 3 1509527101 どうも

ちなみに、プライマリキーと GSI を逆に設定してもほぼ成立するのですが、前述したように GSI にはユニーク制約が存在しないので、User Comment Table では ID をプライマリキーとして、重複が生じないように設定してあります。

Dynamoid での利用方法

テーブル定義

ここからは前述の User Table と User Comment Table を dynamoid から利用する例を示していきます。

class User
  include Dynamoid::Document

  table name: :users, key: :id

  field :name, :string

  # 現在のユーザーに紐付くコメントを作成する
  def create_comment!(attributes = {})
    attributes[:user_id] = id
    UserComment.new(attributes).tap(&:save!)
  end

  # 現在のユーザーのコメント一覧を取得する
  def comments
    UserComment.where(user_id: id)
  end

  # 現在のユーザーの最終コメントを取得する
  def latest_comment
    comments.scan_index_forward(false).scan_limit(1).all.first
  end
end
class UserComment
  include Dynamoid::Document

  table name: :user_comments, key: :id

  field :user_id, :string
  field :posted_at, :datetime

  global_secondary_index hash_key: :user_id, 
                         range_key: :posted_at, 
                         projected_attributes: :all
end

※ ちなみに dynamoid には has_many を利用して関連テーブルを実現する方法があるのですが、結合キーを親テーブルに持つ設計になるのがあまり好ましくなかったので、自前で実装しています。

いくつか注意する点があって、 global_secondary_index で使用する hash_keyrange_keyfield で定義されている必要があります。 また、 projected_attributes: :all というオプションをつけないと後述の #where でインデックスを利用した検索が行われません。一旦これが無い状態でリリースとしてしまうと、射影される属性が限定された GSI が作成されてしまい、実行時にエラーになります。その場合は AWS マネジメントコンソールから直接 GSI を作り直す羽目になりますのでご注意ください 🙏

One or more parameter values were invalid: Select type ALL_ATTRIBUTES is not supported for global secondary index

#where を使えば GSI を使って自動的にクエリで検索してくれる

#comments というメソッドの中で #where を使用した検索が登場しますが、 GSI が設定されていれば特別な記述がなくとも自動的にクエリ検索が行われます。

UserComment.where(user_id: id)
# => [#<UserComment:0x00007f44c86183e8>, ...]

前述の通り projected_attributes: :all が指定されていないとフルスキャンされてしまうのでご注意ください。

昇順・降順を入れ替えたい場合

#latest_comment というメソッド内で使用していますが、 #scan_index_forward(false) と指定すると降順でソートされた状態で結果が返ってきます。未指定の場合は昇順でソートされます。 また、 #scan_limit(n) と指定することで、先頭から n 件の結果に限定して取得が可能です。#latest_comment ではこれらを組み合わせて最終のコメントを取得しています。

まとめ

本稿では GSI と LSI とプライマリキーの違い、具体的な利用用途を紹介しました。前回の記事でも触れましたが、 dynamoid は初回実行時にテーブルや GSI が存在していないと作成する、という挙動になるため、後で設計を変えたくなった場合に GSI や最悪テーブルを作り直す羽目になります。特に初めて利用する場合は設計の勘所を掴むのが難しいので、リリース前に入念に設計を見直すことをお勧めします。その点では RDS 以上に慎重な設計が求められるように感じています。 色々と気を付けなければならない点も多いですが、並列動作性は非常に高いので、利用したくなるシーンが必ず出てくると思います。その際に本稿が少しでもお役に立てば幸いです 🙏