どうも、バックエンドエンジニアのサトウリョウスケです ✌︎('ω')✌︎
最近こうして 弊社の tech ブログが移転した 訳ですが、自社で管理してるブログだと投稿フローがめんどくさいと僕がボヤいたのが移転理由の一端だったりします 😎 でも移転作業したのは僕じゃなくて、球だけ投げてどっか行きました 😎 移転ありがとうございます 🙇
移転して一発目の投稿なので張り切って参ります 💪
さて、Rails で DynamoDB を利用する際の ORM として dynamoid
があります。
今回は dynamoid
から Hash-Range Table (Partition Key と Sort Key の複合) を利用する方法について紹介します。
dynamoid
の導入方法については以前書いたこちらの記事を参考にしてみて下さい。
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
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 両方で正しく設定されているかをリリース前に入念にチェックした方が良いでしょう。