local環境のRailsでDynamoidを使ってみる

# この記事について

この記事は、local環境でRails・DynamoDB・ORMとしてDynamoidを使用してみた際のメモです。

https://aws.amazon.com/jp/dynamodb/

https://github.com/Dynamoid/dynamoid

[さいごに](#さいごに)でも書いてありますが、この記事はこれからも追記予定です。

## 前提

- Docker/docker-composeを使用します
- macOSでのみ動作確認を行なっています


# 環境構築

## RailsとDynamoDBをdocker-composeで起動する

https://github.com/nissyi-gh/docker-compose-sample/blob/main/rails/dynamoid

RailsとDynamoDBをlocal環境に構築するスクリプトを作成しました。このスクリプトを実行すると開発環境が立ち上がります。スクリプトの中では、docker-compose.ymlなどのファイルの生成、コンテナの立ち上げ、`rails new`などを記述しています。

docker-compose.ymlの中身は、以下のとおりです。

```yaml:docker-compose.yml
services:
  dynamo-demo-db:
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    image: amazon/dynamodb-local:latest
    container_name: dynamo-local
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
  app:
    build:
      context: .
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b 0.0.0.0"
    volumes:
      - .:/app
    ports:
      - 3000:3000
volumes:
  dynamodb-data:
networks:
  default:
    name: dynamo-demo-network
```

DynamoDBをDockerで動かすにあたって、こちらを参考にしています。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

## Dynamoidを導入する

開発環境の準備ができたら、Gemfileに`"aws-sdk-dynamodb"`と`"dynamoid"`を追加します。

```ruby:Gemfile
gem "aws-sdk-dynamodb"
gem "dynamoid"
```

`bundle install`を実行します。

```
% docker-compose exec app bundle install
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Fetching aws-eventstream 1.3.0
Fetching aws-partitions 1.1050.0
Fetching jmespath 1.6.2
Installing aws-eventstream 1.3.0
Fetching aws-sigv4 1.11.0
Installing aws-partitions 1.1050.0
Installing jmespath 1.6.2
Installing aws-sigv4 1.11.0
Fetching aws-sdk-core 3.218.1
Installing aws-sdk-core 3.218.1
Fetching aws-sdk-dynamodb 1.136.0
Installing aws-sdk-dynamodb 1.136.0
Fetching dynamoid 3.11.0
Installing dynamoid 3.11.0
Bundle complete! 20 Gemfile dependencies, 114 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
1 installed gem you directly depend on is looking for funding.
  Run `bundle fund` for details
```

initializerを定義します。

```ruby:config/initializers/dynamoid.rb
Dynamoid.configure do |config|
  config.namespace = "dynamoid_demo"
  config.region = "us-east-1"
  config.access_key = "Dummy"
  config.secret_key = "Dummy"
  config.endpoint = "http://dynamo-demo-db:8000"
end
```

## User modelを定義する

次に、DynamoDBに記録したいmodelを定義します。今回は、Userモデルを定義します。通常のRailsアプリケーションと異なり、`ApplicationRecord`を継承せず、`Dynamoid::Document`を`include`します。また、tableやfieldについても定義します。

```ruby:app/models/user.rb
class User
  include Dynamoid::Document

  table name: :users, key: :id

  field :name, :string
end
```

## rails consoleからUserモデルを操作してみる

### `User.create!`

rails consoleで`User.create!`をしてみます。実行すると以下のようにログが出力されました。

```ruby:rails console
app(dev)> User.create!(name: 'first user')
[Aws::DynamoDB::Client 200 0.317321 0 retries] list_tables(exclusive_start_table_name:nil)  

(399.08 ms) LIST TABLES
(399.64 ms) CACHE TABLES
Creating dynamoid_demo_users table. This could take a while.
[Aws::DynamoDB::Client 200 0.059265 0 retries] create_table(table_name:"dynamoid_demo_users",key_schema:[{attribute_name:"id",key_type:"HASH"}],attribute_definitions:[{attribute_name:"id",attribute_type:"S"}],billing_mode:"PROVISIONED",provisioned_throughput:{read_capacity_units:100,write_capacity_units:20})  

(62.79 ms) CREATE TABLE
[Aws::DynamoDB::Client 200 0.03618 0 retries] put_item(table_name:"dynamoid_demo_users",item:{"name"=>{s:"first user"},"id"=>{s:"824bf1cd-5da9-42d0-87e6-aff658152fe6"},"created_at"=>{n:"1739450865.216913975"},"updated_at"=>{n:"1739450865.217141442"}},expected:{"id"=>{exists:false}})  

(39.91 ms) PUT ITEM - ["dynamoid_demo_users", {name: "first user", id: "824bf1cd-5da9-42d0-87e6-aff658152fe6", created_at: 0.1739450865216913975e10, updated_at: 0.1739450865217141442e10}, {}]
=> #<User id: "824bf1cd-5da9-42d0-87e6-aff658152fe6", name: "first user", created_at: Thu, 13 Feb 2025 12:47:45 +0000, updated_at: Thu, 13 Feb 2025 12:47:45 +0000>
```

`Creating dynamoid_demo_users table` と書かれたログがあります。このとき、`create_table`が実行されており、存在しないtableを自動で作ってくれているようです。
また、`CREATE TABLE`が完了したあとには`put_item`が実行されています。

### `User.first`

```ruby:rails console
app(dev)> User.first
(0.19 ms) SCAN - ["dynamoid_demo_users", [{}]]
[Aws::DynamoDB::Client 200 0.077663 0 retries] describe_table(table_name:"dynamoid_demo_users")  

[Aws::DynamoDB::Client 200 0.033125 0 retries] scan(table_name:"dynamoid_demo_users",limit:1)  

=> #<User id: "824bf1cd-5da9-42d0-87e6-aff658152fe6", name: "first user", created_at: Thu, 13 Feb 2025 12:47:45 +0000, updated_at: Thu, 13 Feb 2025 12:47:45 +0000>
```

ActiveRecordと同様に`User.first`で先ほど作成したUserを取得できました。

## field定義を追加する

Userモデルにage fieldを追加します。

```diff_ruby:app/models/user.rb
class User
  include Dynamoid::Document

  table name: :users, key: :id

  field :name, :string
+ field :age, :integer
end
```

fieldを追加したあと、`User.craete!`を実行すると`age`も一緒に記録できました。RDBみたいにmigrateを実行しなくてもスキーマを変更できるのは非常に便利ですね！

```ruby:rails console
app(dev)> User.create!(name: 'second user', age: 10)
[Aws::DynamoDB::Client 200 0.03165 0 retries] put_item(table_name:"dynamoid_demo_users",item:{"name"=>{s:"second user"},"age"=>{n:"10"},"id"=>{s:"c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd"},"created_at"=>{n:"1739460583.365408009"},"updated_at"=>{n:"1739460583.366512404"}},expected:{"id"=>{exists:false}})  

(33.71 ms) PUT ITEM - ["dynamoid_demo_users", {name: "second user", age: 10, id: "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", created_at: 0.1739460583365408009e10, updated_at: 0.1739460583366512404e10}, {}]
=> #<User id: "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", name: "second user", age: 10, created_at: Thu, 13 Feb 2025 15:29:43 +0000, updated_at: Thu, 13 Feb 2025 15:29:43 +0000>
```

## field定義を削除する

今度は逆に、Userモデルからage fieldを削除します。

```diff_ruby:app/models/user.rb
class User
  include Dynamoid::Document

  table name: :users, key: :id

  field :name, :string
- field :age, :integer
end
```

field定義を削除すると、以下のように、Userインスタンスの属性から`age`が消えていました。

```ruby:rails console
app(dev)> User.find_by_id("c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd")
DEPRECATION WARNING: [Dynamoid] .find_by_id is deprecated! Call .find instead of (called from <main> at (app):60)
[Aws::DynamoDB::Client 200 0.033229 0 retries] get_item(table_name:"dynamoid_demo_users",key:{"id"=>{s:"c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd"}},consistent_read:nil)  

(36.62 ms) GET ITEM - ["dynamoid_demo_users", "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", {}]
=> #<User id: "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", name: "second user", created_at: Thu, 13 Feb 2025 15:29:43 +0000, updated_at: Thu, 13 Feb 2025 15:29:43 +0000>

```

また、`age`を呼び出すと`NoMethodError`が発生しました。

```ruby:rails console
app(dev)> User.find_by_id("c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd").age
DEPRECATION WARNING: [Dynamoid] .find_by_id is deprecated! Call .find instead of (called from <main> at (app):61)
[Aws::DynamoDB::Client 200 0.011879 0 retries] get_item(table_name:"dynamoid_demo_users",key:{"id"=>{s:"c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd"}},consistent_read:nil)  

(15.01 ms) GET ITEM - ["dynamoid_demo_users", "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", {}]
(app):61:in '<main>': undefined method 'age' for an instance of User (NoMethodError)
```

「field定義を削除 = テーブルからも削除されたのか？」と気になったので、再度field定義を追加します。

```diff_ruby:app/models/user.rb
class User
  include Dynamoid::Document

  table name: :users, key: :id

  field :name, :string
+ field :age, :integer
end
```

`reload!`してからUserインスタンスに対して`age`を呼び出すと、`create!`で指定していた10が返ってきました。

```ruby:rails console
app(dev)> reload!
Reloading...
=> nil
app(dev)> User.find_by_id("c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd").age
DEPRECATION WARNING: [Dynamoid] .find_by_id is deprecated! Call .find instead of (called from <main> at (app):63)
[Aws::DynamoDB::Client 200 0.010293 0 retries] get_item(table_name:"dynamoid_demo_users",key:{"id"=>{s:"c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd"}},consistent_read:nil)  

(11.86 ms) GET ITEM - ["dynamoid_demo_users", "c0b4b075-7f7a-4ea5-8de0-f44d0de87dbd", {}]
=> 10
```

# さいごに

ここまで、Dynamoidをつかってみたメモを残しました。もっとDynamoidを使う予定なので、わかったことがあればこの記事に追記します。