DynamoDBの基本的な操作備忘録（AWS SDK for Ruby版）

# 扱える型

### スカラー型

- 数値
    - BigDecimal型で返されるため、必要に応じて`to_i`メソッドでInteger型に直す
- 文字列
- バイナリ
- ブール
- `null`

### ドキュメント型

- リスト
    - 複数の型を要素に持つことができる
- マップ

### セット型

ユニークで順序の保証されないリスト

- 数値セット
- 文字列セット
- バイナリセット

# データ取得

- `scan`
    - テーブルのすべての項目を取得する
    - 全てのカラムを条件に使用可能
- `query`
    - パーティションキーの指定が必須
    - ソートキーを指定しない場合、条件に合致する全てのレコードが返される
- `get_item`
    - 1件に絞ってレコードを取得

### `filter_expression`

- 取得したデータに対し、結果セットを返す前に返すレコードを絞り込む条件を記述する
    - 例：`filter_expression: 'Age > :age',` ※ `:age` は `expression_attribute_values`で指定する。
- `scan`, `query`で指定可能
- ただしフィルタ式の適用は結果取得後のため、フィルタ式の有無にかかわらずキャパシティーの消費量は同じ

### `projection_expression`

- 取得するカラムをカンマ区切りで指定する
    - 例：`projection_expression: 'Name, Age',`
- `scan`, `query`, `get_item`で指定可能

### `example_table1`

| Id(主キー) | Name | Age | Email |
| --- | --- | --- | --- |
| 1 | Name1 | 21 | one@example.com |
| 2 | Name2 | 18 | two@example.com |
| 3 | Name3 | 30 | three@example.com |

### `example_table2`

| K1(パーティションキー) | K2(ソートキー) | Code |
| --- | --- | --- |
| 1 | 1 | '1_1' |
| 1 | 2 | '1_2' |
| 2 | 1 | '2_1' |

## スキャン

- 主キーやパーティションキー・ソートキー以外を条件に指定可能
- `filter_expression` で条件を指定する。指定しない場合全件を取得する

```ruby
client.scan({
  table_name: 'example_table1',
  filter_expression: 'Age >= :age',
  expression_attribute_values: {
    ':age' => 20,
  }
})
```

結果

| Id | Name | Age | Email |
| --- | --- | --- | --- |
| 1 | Name1 | 21 | one@example.com |
| 3 | Name3 | 30 | three@example.com |

### ページング

- スキャンでは、1回の実行で最大1MBのデータを取得できる
    - ※フィルタ式を適用する前で1MB
- 1MBを超えるデータを取得する場合、複数回のスキャン実行が必要
    - 続きがある場合、実行結果に `last_evaluated_key` というパラメータが含まれている。同じ条件でスキャンを実行する際に、`exclusive_start_key` に `last_evaluated_key` の値を指定しておくことで、続きの結果を取得できる。

```ruby
scan_condition = {
  table_name: 'table_name',
  filter_expression: 'Age = :threshold',
  expression_attribute_values: {
    ':threshold' => 20,
  },
}

loop do
  result = client.scan(scan_condition)
  puts "items: #{result[:items]}"

  last_evaluated_key = result[:last_evaluated_key]
  puts "last_evaluated_key: #{last_evaluated_key}"

  # `last_evaluated_key` がnilの場合は次の結果が無いので終了する
  break if last_evaluated_key.nil?

  # `exclusive_start_key` に last_evaluated_key の値を指定する
  scan_condition[:exclusive_start_key] = last_evaluated_key
end
```

## クエリ

- `query` メソッドは主キーついての条件が必須
- パーティションキーのみの場合は必然的に最大1件になる
- パーティションキー＋ソートキーの場合、パーティションキーのみ条件に指定すると複数件の結果が返る
- `filter_expression` にパーティションキー・ソートキーを含めることはできない。`key_condition_expression` で指定する。

### パーティションキーのみ

```ruby
client.query({
  table_name: 'example_table1',
  key_condition_expression: 'Id = :id',
  expression_attribute_values: {
    ':id' => 1
  }
})
```

結果

| Id | Name | Age | Email |
| --- | --- | --- | --- |
| 1 | Name1 | 21 | one@example.com |

### パーティションキー＋ソートキー

```ruby
client.query({
  table_name: 'example_table2',
  key_condition_expression: 'K1 = :v1 AND K2 = :v2',
  expression_attribute_values: {
    ':v1' => 1,
    ':v2' => 1,
  }
})
```

結果

| K1 | K2 | code |
| --- | --- | --- |
| 1 | 1 | '1_1' |

### パーティションキー＋ソートキー（パーティションキーのみ指定）

```ruby
client.query({
  table_name: 'example_table2',
  key_condition_expression: 'K1 = :v1',
  expression_attribute_values: {
    ':v1' => 1,
  }
})
```

結果

| K1 | K2 | code |
| --- | --- | --- |
| 1 | 1 | '1_1' |
| 1 | 2 | '1_2' |

## `get_item`

`query` や `scan` と異なり、1件のみを取得する

```
result = client.get_item({
  table_name: 'example_table1',
  key: {
    id: 1,
  },
})
puts result[:item]
# => {"Id"=>0.1e1, "Name"=>"Name1", "Age"=>0.21e2, "Email"=>"one@example.com"}
```

# 項目の更新

- `SET`
- `ADD`
- `REMOVE`
- `DELETE`

## `SET`

- 値の更新（上書き）を行う
- 存在しないカラムを指定した場合、新たにカラムが追加されそこに値がセットされる

### 値の更新

| Id | Val |
| --- | --- |
| 1 | 101 |
| 2 | 102 |
| 3 | 100 |

```ruby
client.update_item({
  table_name: 'example_table3',
  key: {
    Id: 3,
  },
  update_expression: 'SET Val = :new_val',
  expression_attribute_values: {
    ':new_val' => 103,
  }
})
```

結果

| Id | Val |
| --- | --- |
| 1 | 101 |
| 2 | 102 |
| 3 | 103 |

### データの追加

```ruby
client.update_item({
  table_name: 'example_table3',
  key: {
    Id: 4, # 存在しない値を指定
  },
  update_expression: 'SET Val = :val',
  expression_attribute_values: {
    ':val' => 104,
  }
})
```

結果

| Id | Val |
| --- | --- |
| 1 | 101 |
| 2 | 102 |
| 3 | 103 |
| 4 | 104 |

### カラムの追加

```ruby
client.update_item({
  table_name: 'example_table3',
  key: {
    Id: 1,
  },
  update_expression: 'SET Val2 = :val', # 存在しないカラムを指定
  expression_attribute_values: {
    ':val' => 1001,
  }
})
```

結果

| Id | Val | Val2 |
| --- | --- | --- |
| 1 | 101 | 1001 |
| 2 | 102 | |
| 3 | 103 | |
| 4 | 104 | |

### 複数の更新

例：値の更新とカラム追加を同時に行う

```ruby
client.update_item({
  table_name: 'example_table3',
  key: {
    Id: 2,
  },
  update_expression: 'SET Val = :val, Val3 = :val3', # カンマ区切りで式を並べる
  expression_attribute_values: {
    ':val' => 202,
    ':val3' => 10002,
  }
})
```

結果

| Id | Val | Val2 | Val3 |
| --- | --- | --- | --- |
| 1 | 101 | 1001 | |
| 2 | 202 | | 10002 |
| 3 | 103 | | |
| 4 | 104 | | |

## `ADD`

- 値の追加を行う
- 数値(Integer)型、リスト型、セット型で挙動が異なる
- SETと同じく、存在しないカラムを指定した場合、カラムを作成し値をセットする

### 各型のカラムを用意

```ruby
client.update_item({
  table_name: 'example_table4',
  key: {
    Id: 1,
  },
  # カンマ区切りで複数カラムに操作
  update_expression: 'ADD IntType :int, IntSetType :intset, StrSetType :strset, BinSetType :binset',
  expression_attribute_values: {
    ':int' => 1, # 数値型
    ':intset' => Set.new([1,2]), # 数値セット型
    ':strset' => Set.new(['one', 'two']), # 文字列セット型
    ':binset' => Set.new([StringIO.new('abc123')]), # バイナリセット型
  }
})
```

結果

| Id | IntType | IntSetType | StrSetType | BinSetType |
| --- | --- | --- | --- | --- |
| 1 | 1 | {2, 1} | {"one", "two"} | { YWJjMTIz } |

### 数値

- 数値型での `ADD` は単に足し算を行う
- マイナスの値を指定すれば引き算される（マイナスを足す）

```ruby
client.update_item({
  table_name: 'example_table4',
  key: {
    Id: 1,
  },
  update_expression: 'ADD IntType :int',
  expression_attribute_values: {
    ':int' => 100, # 1 + 100
  }
})
```

結果

| Id | IntType | IntSetType | StrSetType | BinSetType |
| --- | --- | --- | --- | --- |
| 1 | 101 | {2, 1} | {"one", "two"} | { YWJjMTIz } |

### セット型

セット型の `ADD` は、互いの要素を合わせたものから重複を取り除いたセット型になる

```ruby
client.update_item({
  table_name: 'example_table4',
  key: {
    Id: 1,
  },
  update_expression: 'ADD IntSetType :intset, StrSetType :strset, BinSetType :binset',
  expression_attribute_values: {
    ':intset' => Set.new([2,3]), # 2は重複
    ':strset' => Set.new(['two', 'three']), # 'two'は重複
    ':binset' => Set.new([StringIO.new('abc123'), StringIO.new('xyz999')]), # StringIO.new('abc123')は重複
  }
})
```

結果

| Id | IntType | IntSetType | StrSetType | BinSetType |
| --- | --- | --- | --- | --- |
| 1 | 101 | {3, 2, 1} | {'three', 'two', 'one'} | { YWJjMTIz, eHl6OTk5 } |

### 配列で要素の追加をしたい場合、ADDではなくSETを用いる

- `list_append(list1, list2)`
    - 2つのリストを受け取り結合したものを返す

| Id | ListType |
| --- | --- |
| 1 | [{"N": "1"}, {"N": "2"}] |

```ruby
client.update_item({
  table_name: 'example_table5',
  key: {
    Id: 1,
  },
  update_expression: 'SET AnyList = list_append(AnyList, :l)',
  expression_attribute_values: {
    ':l' => [2, '3'], # 注：配列を渡す
  },
})
```

結果

| Id | ListType |
| --- | --- |
| 1 | [{"N": "1"}, {"N": "2"}, {"N": "2"}, {"S": "3"}] |

ちなみに `AnyList` というカラムがまだない場合や、存在しないキー（レコードの作成を期待）を指定するとエラーが発生する

```
The provided expression refers to an attribute that does not exist in the item (Aws::DynamoDB::Errors::ValidationException)
```

カラムが存在しない場合の代わりの値を渡すことで解決することができる

- `if_not_exists(Column, default)`
    - 第一引数に指定したカラムが存在する場合、第一引数の値を返す
    - 第一引数に指定したカラムが存在しない場合、第二引数の値を返す

```ruby
client.update_item({
  table_name: 'example_table5',
  key: {
    Id: 1,
  },
  update_expression: 'SET AnyList2 = list_append(if_not_exists(AnyList2, :empty), :l)',
  expression_attribute_values: {
    ':l' => [1, '2', 3],
    ':empty' => [], # もちろん空でない配列でもOK
  }
})
```

## `REMOVE`

### すべての型

- あるレコードの指定したカラムの値を空にする
- DynamoDBの性質上、この操作によりすべてのレコードがそのカラムに値を持たなくなった場合、カラム自体も削除される
- 存在しないカラムを指定してもエラーにはならない

| Id | Column1 | Column2 | Column3 | Column4 |
| --- | --- | --- | --- | --- |
| 1 | col1 | col2 | col3 | |
| 2 | col1 | col2 | | col4 |

```ruby
client.update_item({
  table_name: 'example_table6',
  key: {
    Id: 1,
  },
  update_expression: 'REMOVE Column2, Column3, Column4, Column5', # カンマ区切りで複数指定可能
}
```

結果

| Id | Column1 | Column2 | Column4 |
| --- | --- | --- | --- |
| 1 | col1 | | |
| 2 | col1 | col2 | col4 |

### リスト型

- `REMOVE AnyList[0]` のように、`REMOVE` + インデックスで要素指定を行うことで、リストの任意の位置の要素を削除できる

| Id | AnyList |
| --- | --- |
| 1 | [{"N": "1"}, {"N": "2"}, {"S": "3"}] |

```ruby
client.update_item({
  table_name: 'example_table5',
  key: {
    Id: 1,
  },
  update_expression: 'REMOVE AnyList[1]',
}
```

結果

| Id | AnyList |
| --- | --- |
| 1 | [{"N": "1"}, {"S": "3"}] |

## `DELETE`

- セット型から要素を削除する

| Id | IntSet1 | IntSet2 |
| --- | --- | --- |
| 1 | {1,2,3} | {4,5} |

```ruby
client.update_item({
  table_name: 'example_table6',
  key: {
    Id: 1,
  },
  update_expression: 'DELETE IntSet1 :s1, IntSet2 :s2',
  expression_attribute_values: {
    ':s1' => Set.new([1,4]),
    ':s2' => Set.new([4,5]),
  },
})
```

結果

| Id | IntSet1 |
| --- | --- |
| 1 | {2,3} |

## 条件

- 更新する際に条件を設けることも可能
- 条件を満たさない場合更新は行われずエラーが返る

| Id | Val |
| --- | --- |
| 1 | 100 |

```ruby
client.update_item({
  table_name: 'example_table7',
  key: {
    id: 1,
  },
  update_expression: 'SET Val = Val + :v',
  condition_expression: 'Val > :threshold',
  expression_attribute_values: {
    ':threshold' => 100,
    ':v' => 100,
  }
})
# => The conditional request failed (Aws::DynamoDB::Errors::ConditionalCheckFailedException)
```

```ruby
client.update_item({
  table_name: 'example_table7',
  key: {
    id: 1,
  },
  update_expression: 'SET Val = Val + :v',
  condition_expression: 'Val > :threshold',
  expression_attribute_values: {
    ':threshold' => 99,
    ':v' => 100,
  }
})
```

結果

| Id | Val |
| --- | --- |
| 1 | 200 |

# 項目の削除

| Id | Val |
| --- | --- |
| 1 | 10 |
| 2 | 20 |
| 3 | 30 |

```ruby
client.delete_item({
  table_name: 'example_table8',
  key: {
    Id: 2,
  },
})
```

結果

| Id | Val |
| --- | --- |
| 1 | 10 |
| 3 | 30 |

更新と同様条件を設定することも可能

```ruby
client.delete_item({
  table_name: 'example_table8',
  key: {
    id: 1,
  },
  condition_expression: 'Val > :twenty',
  expression_attribute_values: {
    ':twenty' => 20
  }
})
```

条件(`condition_expression`)により削除できなかった場合はエラーが返るので注意

```
The conditional request failed (Aws::DynamoDB::Errors::ConditionalCheckFailedException)
```

## 戻り値

- 更新や削除を実行する際、戻り値を指定することができる
- `return-values` に文字列で指定する

| キー | 説明 |
| --- | --- |
| ALL_OLD | 更新前のすべての項目(カラム) |
| ALL_NEW | 更新後のすべての項目 |
| UPDATED_OLD | 更新された項目のみ、以前の値 |
| UPDATED_NEW | 更新された項目のみ、更新後の値 |

`ALL_OLD` 以外は `update_item` でのみ指定可能

| Id | Val1 | Val2 | Val3 |
| --- | --- | --- | --- |
| 1 | 11 | 21 | 31 |
| 2 | 12 | 22 | 32 |
| 3 | 13 | 23 | 33 |

```ruby
return_values = client.update_item({
  table_name: 'example_table9',
  key: {
    id: 1,
  },
  update_expression: 'SET Val1 = :zero',
  expression_attribute_values: {
    ':zero' => 0,
  },
  return_values: 'ALL_OLD'
})
puts return_values[:attributes]
# => {"Id"=>1, "Val1"=>0.11e2, "Val2"=>0.21e2, "Val3"=>0.31e2}

return_values = client.update_item({
  table_name: 'example_table9',
  key: {
    id: 2,
  },
  update_expression: 'SET Val1 = :zero, Val2 = :zero',
  expression_attribute_values: {
    ':zero' => 0,
  },
  return_values: 'UPDATED_NEW'
})
puts return_values[:attributes]
# => {"Val1"=>0.0, "Val2"=>0.0}
```

# `expression_attribute_names`

- `update_expression` 等にカラム名を記述する際、プレースホルダーを使用しなければならない場合がある。
    - 数字始まり
    - スペースを含む
    - 特殊文字を含む
    - 予約語である
- プレースホルダーは `#` で始める必要がある

| Id | Col umn | Value |
| --- | --- | --- |
| 1 | "スペースを含むカラム名" | "予約語のカラム名" |

```ruby
client.update_item({
  table_name: 'example_table3',
  key: {
    Id: 1,
  },
  update_expression: 'SET #Col1 = :v1, #Col2 = :v2',
  expression_attribute_names: {
    '#Col1' => 'Col umn',
    '#Col2' => 'Value',
  },
  expression_attribute_values: {
    ':v1' => 'aaa',
    ':v2' => 'bbb',
  }
})
```