Feedforce Developer Blog

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

Dynamoid の使い方【range 編】

どうも、バックエンドエンジニアの佐藤です。

最近こうして 弊社の tech ブログが移転した 訳ですが、自社で管理してるブログだと投稿フローがめんどくさいと僕がボヤいたのが移転理由の一端だったりします 😎 でも移転作業したのは僕じゃなくて、球だけ投げてどっか行きました 😎 移転ありがとうございます 🙇

移転して一発目の投稿なので張り切って参ります 💪

さて、Rails で DynamoDB を利用する際の ORM として dynamoid があります。 今回は dynamoid から Hash-Range Table (Partition Key と Sort Key の複合) を利用する方法について紹介します。

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

tech.feedforce.jp

Hash-Range Table ってなんぞ

その前に名称の整理をしておきます

タイトルに 【range 編】と書いているのですが、これは Sort Key の事を指します。 どうやら DynamoDB は初期の頃と現在で一部の名称が変化したようです。 しかし、 Dyanmoid では相変わらず旧名称のまま (hash_key, range_key) でパラメータを指定するので、対応表を記載しておきます。

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

DyanmoDB には 2 種類のプライマリキーがある

こちらのスライドが分かりやすいのですが、 DynamoDB のテーブル定義として Hash Table と Hash-Range Table というものがあります。

  • Hash Table
    • Hash Key (Partition Key) という一つのカラムの値でプライマリキーを表現するテーブル
    • この構成だと Hash Key は 重複させることができない
  • Hash-Range Table
    • Hash Key と Range Key (Partition Key, Sort Key) の二つの値でプライマリキーを表現する
    • Range Key が異なっていれば、同一の Hash Key を持つレコードが複数存在しても良い
    • スキャンより高速なクエリ 1 で複数のレコードを取得することが可能
      • スキャンだと物凄くコストが高いので、基本的にクエリだけでデータ取得できるように設計すべき
    • Range Key での昇順・降順でのソートが可能
    • Range Key に対しての 範囲検索 も可能

dynamoid での利用方法

テーブル定義

class User
  include Dynamoid::Document

  table name: :users, key: :hash_key
  range :range_key, :string # <= これ
end

range :(フィールド名), :(データ型) で Range Key の定義が可能です。 ちょっと試せていないのですが、AWS コンソールからだとテーブル作成時にしか Range Key (ソートキー) を定義できないので、既に存在しているテーブルに途中で range の定義を加えても動作しないと思います。

使い方

Dynamoid にも ActiveRecord と同じように #where というメソッドが実装されています。 ドキュメントでは内部でどのような動きをするのかが見当たらなかったので、実装から確認したのですが、検索条件に Hash Key や Range Key が含まれているかどうかを判断して、クエリが使える場合はクエリで検索してくれるようです。

User.where(hash_key: 'hash_key') # クエリで検索される
User.where(hash_key: 'hash_key', range_key: 'range_key') # クエリで検索される
User.where(name: 'name') # Hash Key が無いのでスキャンが実行される

ただし、引数の指定方法や定義の仕方が少しでも間違っていると #where でスキャンが実行されてしまっているケースがあります。本当にクエリ検索されているか、念のため Rails のログ出力を確認し、スキャンが実行されていないかどうか確認するようにして下さい ⚠️

#where の使い方は ActiveRecord とほぼ同じです。

User.where(hash_key: 'hash_key').all # => [#<User:0x000000076ed848>, #<User:0x0000000779abb0>, ...]
User.where(hash_key: 'hash_key').each do |user|
  user # => #<User:0x000000076ed848>
end
User.where(hash_key: 'hash_key').first # => #<User:0x000000048cb050>
User.where(hash_key: 'hash_key').last # => #<User:0x000000048cb050>

そして、 range_key に対して gt, lt, gte, lte, begins_with, between の演算子が使用できます。

User.where(hash_key: 'hash_key', 'range_key.gt': 123)
User.where(hash_key: 'hash_key', 'range_key.lt': 123)
User.where(hash_key: 'hash_key', 'range_key.gte': 123)
User.where(hash_key: 'hash_key', 'range_key.lte': 123)
User.where(hash_key: 'hash_key', 'range_key.begins_with': 'range_')
User.where(hash_key: 'hash_key', 'range_key.between': [100, 200])

ハマりポイント

ここからは Range Key を dyanmoid を使っていてハマった点をいくつか紹介したいと思います。

range を定義していると #find_by_id の動作が変わる

# Hash Table として利用
class User
  include Dynamoid::Document

  table name: :users, key: :hash_key
end

# OK!
User.find_by_id('hash_key')
# => #<User:0x000000048cb050>
# Hash-Range Table として利用
class User
  include Dynamoid::Document

  table name: :users, key: :hash_key
  range :range_key, :string
end

# Error!
User.find_by_id('hash_key')
# => Aws::DynamoDB::Errors::ValidationException: The provided key element does not match the schema

んん??ってなったのですが、こういう事らしいです。

  • #find_by_id は内部的には Aws::DynamoDB::Client#get_item を呼び出している
  • #get_item は結果が一意に定まる検索条件を指定しないとエラーになる
    • つまり range_key (primary sort key) を定義している場合は引数一つだとエラー
    • 引数に range_key を指定すれば OK
Line::User.find_by_id('hash_key', range_key: 'range_key')
# => #<User:0x000000048cb050>
# OK!

has_many は Hash-Range Table に対応していない

dynamoid では ActiveRecord のような has_many has_one belongs_to が定義されているのですが、 Hash-Range Table だと上手く動作しません。 内部の実装を見てみましたが、 Hash Table の状態で利用することが前提となっているようでした。

Hash Table であればこんな感じで利用することができます。

class User
  include Dynamoid::Document

  table name: :users, key: :hash_key
  has_many :talks, class: Talk
end

class Talk
  include Dynamoid::Document

  table name: :talks, key: :hash_key
  belongs_to :user, class: User
end

user = User.create(name: 'Taro')
user.talks.create(content: 'Hello world')

まとめ

Hash Table と Hash-Range Table の違いから、 Dynamoid における実装方法についてを紹介しました。 Dynamoid を利用した場合は migration を明示的に実行する訳ではないため、Rails のソースコードと DyanmoDB のテーブルの実態が必ずしも一致していないケースがある点がハマりどころのような気がします。 本稿で紹介した Hash-Range Table が DynamoDB と Dynamoid 両方で正しく設定されているかをリリース前に入念にチェックした方が良いでしょう。