🙌

aws-sdk-rustでDynamoDBにデータ保存

2021/06/20に公開

複雑な形式のデータを DynamoDB に保存

aws-sdk-rust を使って、AWS の DynamoDB に JSON オブジェクトや配列などを含む複雑な形式のデータを保存していきます。
全データを AttributeValue 型に変換するのがポイントです。
プロジェクトは以下のリンク先で確認できます。また、Serverless Framework を使っています。
コード全体を示したあとに説明が続くので、そちらご確認ください。

サンプルレポジトリ
https://github.com/Nakamurus/dynamodb-example-rust

役立つリンク集

aws-sdk-rust について広い知識を得るために
https://dev.classmethod.jp/articles/try-aws-sdk-for-rust-alpha/

Rust で Serverless Framework を使うために
https://zenn.dev/ryosukeeeee/articles/rust-serverless-framework

公式レポジトリ
https://github.com/awslabs/aws-sdk-rust

AttributeValue を知るために
https://rusoto.github.io/rusoto/rusoto_dynamodb/struct.AttributeValue.html

DynamoDB のデータ型について公式ページ
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes

コード全体

main.rs
use dynamodb;
use dynamodb::model::AttributeValue;
use std::collections::HashMap;

struct User {
    id: usize,
    name: String,
    subscribed: bool,
    followers: Vec<String>,
    tweets: Vec<Tweet>,
}

struct Tweet {
    title: String,
    like: usize,
    liked_users: Vec<String>,
}

#[tokio::main]
async fn main() -> Result<(), dynamodb::Error> {
    let client = dynamodb::Client::from_env();
    let user = User {
        id: 0,
        name: "Sergey".to_string(),
        subscribed: true,
        followers: vec!["Satou".to_string(), "Cathy".to_string()],
        tweets: vec![Tweet {
            title: "Rust最高!".to_string(),
            like: 2,
            liked_users: vec!["Sizuka".to_string(), "Mononohu".to_string()],
        }],
    };
    put_item_manually(&client, user).await?;
    Ok(())
}

async fn put_item_manually(client: &dynamodb::Client, user: User) -> Result<(), dynamodb::Error> {
    let res = client
        .put_item()
        .table_name("example_dynamo")
        .item("id", AttributeValue::N(user.id.to_string()))
        .item("name", AttributeValue::S(user.name.clone()))
        .item("subscribed", AttributeValue::Bool(user.subscribed))
        .item(
            "followers",
            AttributeValue::L(
                user.followers
                    .iter()
                    .map(|x| AttributeValue::S(x.to_string()))
                    .collect(),
            ),
        )
        .item(
            "tweets",
            AttributeValue::L(
                user.tweets
                    .iter()
                    .map(|t| {
                        let mut map = HashMap::new();
                        map.insert("title".to_string(), AttributeValue::S(t.title.clone()));
                        map.insert("like".to_string(), AttributeValue::N(t.like.to_string()));
                        map.insert(
                            "liked_users".to_string(),
                            AttributeValue::L(
                                t.liked_users
                                    .iter()
                                    .map(|x| AttributeValue::S(x.to_string()))
                                    .collect(),
                            ),
                        );
                        AttributeValue::M(map)
                    })
                    .collect(),
            ),
        )
        .send()
        .await?;
    Ok(())
}

Crate の dependencies 部分だけ抜粋。
今の aws-sdk-dynamodb のタグは"v0.0.8-alpha"のようですが、unstable なライブラリを使っているようでエラーになるので、古いのを使っています。

Cargo.toml
[dependencies]
dynamodb = { git = "https://github.com/awslabs/aws-sdk-rust", tag = "v0.0.4-alpha", package = "aws-sdk-dynamodb" }
tokio = { version = "1", features = ["full"] }

serverless Framework を使う場合…

Serverless.yml
service: YOUR_PACKAGE_NAME
provider:
  name: aws
  runtime: rust
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:PutItem
      Resource: "YOUR DYNAMODB TABLE'S ARN"
plugins:
  # this registers the plugin
  # with serverless
  - serverless-rust
# creates one artifact for each function
package:
  individually: true
functions:
  test:
    # handler value syntax is `{cargo-package-name}.{bin-name}`
    # or `{cargo-package-name}` for short when you are building a
    # default bin for a given package.
    handler: YOUR_PACKAGE_NAME
    events:
      - http:
          path: /test
          method: GET
custom:
  rust:
    # custom docker tag
    dockerTag: "1.51"
    #  custom docker image
    dockerImage: "softprops/lambda-rust"

説明

以下の 7 行を拡張するのが基本形です。

let client = dynamodb::Client::from_env();
client
    .put_item()
    .table_name("TABLE NAME")
    .item("ITEM NAME")
    .send()
    .await?;

aws-sdk-rust を使う場合、手動でデータをdynamodb::model::AttributeValueに変換していく必要がありそうです(まともに機能する自動シリアライズのライブラリあれば教えて下さい)。

どういったデータをどう変換するかの詳細はリンク集にも挙げた以下のリンクを確認するのがベストですが、次節からかんたんに列挙していきます。
AttributeValue を知るために
https://rusoto.github.io/rusoto/rusoto_dynamodb/struct.AttributeValue.html

DynamoDB のデータ型について公式ページ
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes

AttributeValue 型と Rust データ型の対応関係

数値

AttributeValue::N を使います。数値は文字列に変換する必要があります。

AttributeValue::N(100usize.to_string());

文字列

AttributeValue::S を使います。構造体のフィールドを使う場合は、所有権の問題で clone()を使う必要がありそうです。

AttributeValue::S("Example".to_string());

真偽値

AttributeValue::Bool を使います。

AttributeValue::Bool(false);

配列

AttributeValue::L を使います。注意点として、配列要素も AttributeValue に変換する必要があります。
そのため、現時点では map()を使わないといけません。

AttributeValue::L(some_list.iter().map(|x| Attribute::N(x.to_string())).collect());

JSON オブジェクトや構造体

AttributeValue::M を使います。これは、HashMap<String, AttributeValue>の形を取ります。
そのため、HashMap を作って insert()してから、そのマップを AttributeValue::M に変換してください。
また、文字列リテラル"&str"ではなく、"String"が必要なので、構造体のフィールド名.to_string()をお忘れなく。

struct Example {
    title: String,
    number: i64
}
let example = Example {
    title: "exmple".to_string(),
    number: -50
};
let mut map = std::collections::HashMap::new();
map.insert("title".to_string(), example.title.clone());
map.insert("number".to_string(), example.number.to_string());
AttributeValue::M(map);

なお、構造体を持つ配列の場合、ヴェクタに map()を当てて、中で上記処理を施してください。

注意点

aws-sdk-rust はまだ α 版でフィードバック用に公開されているだけなので、本番環境には使えません。

Rust でシリアライズとデシリアライズといえば Serde ですが、aws-sdk-rust に投げられて、かつまともに機能している Crate はまだなさそうです。
serde_dynamo など試しましたがエラーが出ました。

クレデンシャルもまだ環境変数からしか受け取れないので、環境を作って環境変数を入れて Docker でイメージ公開するとパスワード流出します。
secret は試していないですが、環境変数ではないので無理っぽいですね。

上記コードを走らせるとエラーがでますが、おそらく main()でレスポンスを返していないからでしょう。
DynamoDB へのデータ保存自体は成功しています。

感じたこと

使い方が素直ですごく良い!
rusoto だと..Default::default()が必要でボイラープレート多めだったけど、これはスッキリしてます。
これから公式な aws-sdk-rust が発展していくはずなので、すぐ本番環境で使う必要がある場合をのぞき、個人プロジェクトや練習なら aws-sdk-rust を使うのが良さそう。

手動変換は少し面倒くさいですね…Serde 対応したら最強です。

Discussion