ScalaからDynamoDBを扱うサンプル ~DynamoDBMapper編~ 

[前回](http://qiita.com/Peranikov/items/3e9e82594f0ab5b3aa9b)の続きです。今回はより高級APIであるDynamoDBMapperを扱います。
こちらも足りないAPIは必要に応じて増やしていきます。

フルのソースコードはこちら
https://github.com/Peranikov/dynamodb-scala-sample/blob/master/src/main/scala/HighLevelApiSample.scala


# はじめに

build.sbtやテーブル作成などは[前回](http://qiita.com/Peranikov/items/3e9e82594f0ab5b3aa9b)のはじめにをご覧ください。
この辺りをimportしておきます。

```scala
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
import com.amazonaws.services.dynamodbv2.datamodeling.{DynamoDBQueryExpression, _}
import com.amazonaws.services.dynamodbv2.model.AttributeValue
```

# アイテムをマッピングするクラスを定義

DynamoDBのアイテムをマッピングするためのクラスを定義します。
各Annotationの解説は[こちら](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.Annotations.html#DynamoDBMapper.Annotations.DynamoDBTable)です。
getter/setterのメソッド名はgetXXX/setXXXにする必要があります。

```scala
@DynamoDBTable(tableName="Music")
class MusicItem {
  private var artist: String = _
  private var songTitle: String = _
  private var members: java.util.Set[String] = _
  private var origin: String = _

  // Hash key
  @DynamoDBHashKey(attributeName="Artist")
  @DynamoDBIndexRangeKey(attributeName="Artist", globalSecondaryIndexName="OriginGlobalIndex")
  def getArtist: String = { this.artist }
  def setArtist(artist: String): Unit = { this.artist = artist }

  // Range key
  @DynamoDBRangeKey(attributeName="SongTitle")
  def getSongTitle: String = { this.songTitle }
  def setSongTitle(songTitle: String): Unit = { this.songTitle = songTitle }

  @DynamoDBIndexHashKey(attributeName="Origin", globalSecondaryIndexName="OriginGlobalIndex")
  @DynamoDBIndexRangeKey(attributeName="Origin", localSecondaryIndexName="OriginIndex", globalSecondaryIndexName="OriginGlobalIndex")
  def getOrigin: String = { this.origin }
  def setOrigin(origin: String): Unit = { this.origin = origin }

  @DynamoDBAttribute(attributeName="Members")
  def getMembers: java.util.Set[String] = { this.members }
  def setMembers(members: java.util.Set[String]): Unit = { this.members = members }

  override def toString: String = {
    s"""{
       |  Artist: ${this.artist}
       |  SongTitle: ${this.songTitle}
       |  Origin: ${this.origin}
       |  Members: ${this.members}
       |}""".stripMargin
  }
}
```

Scalaらしくコンパニオンオブジェクトを定義しておくと便利です。

```scala
object MusicItem {

  def apply(artist: String, songTitle: String): MusicItem = {
    val item = new MusicItem()
    item.setArtist(artist)
    item.setSongTitle(songTitle)
    item
  }

  def apply(artist: String, songTitle: String, members: Set[String]): MusicItem = {
    val item = new MusicItem()
    item.setArtist(artist)
    item.setSongTitle(songTitle)
    item.setMembers(members.asJava)
    item
  }
}
```

# クライアントとマッパーの生成

Javaでのサンプルは[こちら](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.Methods.html#DynamoDBMapper.Methods.parallelScan)です。

```scala
val client: AmazonDynamoDBClient = (new AmazonDynamoDBClient())
    .withEndpoint("http://localhost:8000")
val mapper: DynamoDBMapper = new DynamoDBMapper(client)
```

# Save

アイテムを保存します。APIで言うPutにあたります。

```scala
println(
    mapper.save(MusicItem("Guns N' Roses", "Welcome to the Jungle", Set("Axl Rose", "Slash")))
  )
```

# Load

アイテムを取得します。APIで言うGetにあたります。

```scala
println(
    mapper.load(classOf[MusicItem], "Guns N' Roses", "Welcome to the Jungle")
  )
```

# Delete

アイテムを削除します。

```scala
println(
    mapper.delete(MusicItem("Guns N' Roses", "Welcome to the Jungle"))
  )
```

# Batch Save

複数のアイテムを同時に保存します。

```scala
println(
    mapper.batchSave(Seq(
      MusicItem("Black Sabbath", "Paranoid", Set("Ozzy Osbourne", "Tony Iommi")),
      MusicItem("Black Sabbath", "Mr. Crowley", Set("Ozzy Osbourne", "Tony Iommi")),
      MusicItem("Black Sabbath", "Heaven or Hell", Set("Ronnie James Dio", "Tony Iommi"))
    ).asJava)
  )
```

# Query

Queryを発行します。ここではHashキーを指定しています。

```scala
println(
    mapper.query(
      classOf[MusicItem],
      new DynamoDBQueryExpression[MusicItem]()
        .withKeyConditionExpression("Artist = :a")
        .withExpressionAttributeValues(Map(
          ":a" -> new AttributeValue("Black Sabbath")
        ).asJava)
    ).asScala
  )
```

# リミットを指定してQueryPage

QueryPageは最初のページ、1MBに収まる分だけを取得します。
なおリミットはQueryPageでのみ有効で、Queryに指定しても無視されます。

```scala
val limitQueryResult = mapper.queryPage(
    classOf[MusicItem],
    new DynamoDBQueryExpression[MusicItem]()
      .withKeyConditionExpression("Artist = :a")
      .withExpressionAttributeValues(Map(
        ":a" -> new AttributeValue("Black Sabbath")
      ).asJava)
      .withLimit(1)
  )
```

# 続きのページをクエリ

QueryPageで取得後に続きのページがある場合、LastEvaluatedKeyが取得できるのでこれを指定して続きを取得します。

```scala
println(s"LastEvaluatedKey: ${limitQueryResult.getLastEvaluatedKey}")
  println(
    mapper.queryPage(
      classOf[MusicItem],
      new DynamoDBQueryExpression[MusicItem]()
        .withKeyConditionExpression("Artist = :a")
        .withExpressionAttributeValues(Map(
          ":a" -> new AttributeValue("Black Sabbath")
        ).asJava)
        .withExclusiveStartKey(limitQueryResult.getLastEvaluatedKey)
    ).getResults.asScala
  )
```

# Local Secondly IndexからQuery

Local Secondly Index名を指定して取得します。
ここではインデックス作成時にProjectionをKEYS_ONLYと指定しているので、Key以外であるMembersは取得されません。

```scala
println(
    mapper.query(
      classOf[MusicItem],
      new DynamoDBQueryExpression[MusicItem]()
        .withIndexName("OriginIndex")
        .withKeyConditionExpression("Artist = :a and Origin = :o")
        .withExpressionAttributeValues(Map(
          ":a" -> new AttributeValue("Black Sabbath"),
          ":o" -> new AttributeValue("England")
        ).asJava)
    ).asScala
  )
```

# Global Secondly IndexからQuery

Global Secondly Index(GSI)名を指定して取得します。
このGSIではOriginをプライマリーキーに指定しているのでOriginに対してクエリが実行できます。
ProjectionExpressionにOrigin, Artistを指定しているので、それらしか取得しません。

```scala
println(
    mapper.query(
      classOf[MusicItem],
      new DynamoDBQueryExpression[MusicItem]()
        .withIndexName("OriginGlobalIndex")
        .withKeyConditionExpression("Origin = :o")
        .withExpressionAttributeValues(Map(
          ":o" -> new AttributeValue("England")
        ).asJava)
        .withProjectionExpression("Origin, Artist")
        .withScanIndexForward(false)
        .withConsistentRead(false)
    ).asScala
  )
```

# Scan

全てのアイテムを取得します。

```scala
println(
    mapper.scan(classOf[MusicItem], new DynamoDBScanExpression()).asScala
  )
```