【詳解】JavascriptでDynamoDBを操作する

  • 17
    いいね
  • 2
    コメント

ややこしいドキュメントを何度も挫折しそうになりながら突き進んでみて、(なんとなく)分かったことを備忘録がてらまとめます。

前提:DynamoDB.DocumentClientを使う

JavascriptでDynamoDBを操作するには、AWS.DynamoDBを使う方法と、AWS.DynamoDB.DocumentClient(以下、"docClient")を使う方法の2つがあります。後者のdocClientを使うと、ネイティブなJavascritpのデータ型を自動的にDynamoDB上の型に変換してくれるので、コードが簡潔になります。この記事ではこのdocClientを使うことを前提にします。

例)AWS.DynamoDBを使う場合

var dynamodb = new AWS.DynamoDB();
var params = {
    TableName: 'DogTable',
    Item:{
        'dogId':{N: '12'},//整数型であることをNで指定
        'name':{S: 'ポチ'}//文字列型であることをSで指定
    }
};
dynamodb.putItem(params, callback);

例)AWS.DynamoDB.DocumentClientを使う場合

var docClient = new AWS.DynamoDB.DocumentClient();
var params = {
    TableName: 'DogTable',
    Item:{
         dogId: 12,
         name: 'ポチ'
    }
};
docClient.put(params, callback);

参考:AWS.DynamoDB.DocumentClient

put(1つ追加)

SuperCarTableに、プライマリキー(carId)が12の項目を追加します。

var params = {
    TableName: 'SuperCarTable',
    Item:{//プライマリキーを必ず含める(ソートキーがある場合はソートキーも)
         carId: 12,
         name: 'フェラーリ458',
         price: 28300000,
         engine: {
             type: 'V型8気筒',
             power: 578
         },
         color:['red', 'black', 'white']
    }
};
docClient.put(params, callback);

get(1つ取得)

var params = {
    TableName: 'SuperCarTable',
    Key:{//取得したい項目をプライマリキー(及びソートキー)によって1つ指定
         carId: 12
    }
};
docClient.get(params, function(err, data){
    if(err){
        console.log(err);
    }else{
        console.log(data.Item.name);//'フェラーリ458'
        console.log(data.Item.engine.type);//'V型8気筒'
        console.log(data.Item.color[2]);//'white'
    }
});

update(更新)

SuperCarTableにあるプライマリキー(carId)が12の項目の属性を更新する

var params = {
    TableName: 'SuperCarTable',
    Key:{//更新したい項目をプライマリキー(及びソートキー)によって1つ指定
         carId: 12
    },
    ExpressionAttributeNames: {
        '#n': 'name',
        '#d': 'designer'
        '#e': 'engine',
        '#t': 'type',
        '#p': 'power',
        '#c': 'color'
    },
    ExpressionAttributeValues: {
        ':newName': 'フェラーリ488GTB',//name属性を更新する
        ':newdesigner': 'フラビオ・マンツォーニ',//デザイナー属性を新たに追加する
        ':newType': 'V型8気筒ツインターボ',//engineのtype属性を更新する
        ':addPower': 92, //engineのpower属性に92を足す
        ':newColor': ['yellow'] //新しい色をcolorリストに追加する
    },
    UpdateExpression: 'SET #n = :newName, #d = :newDesigner, #e.#t = :newType, #e.#p = #e.#p + :addPower, #c = list_append(#c, :addPower)'
};
docClient.update(params, callback);

UpdateExpressionについて

UpdateExpressionにString型で更新式を定義することで更新内容を指示します。UpdateExpressionは必ずアクションキーワードから始まります。アクションキーワードはSET/REMOVE/ADD/DELETEの4種類あり、 同時に1つまでしか使用できません。 カンマで区切ることで複数の更新を指示することができます。

【追記】 UpdateExpressionは複数のアクションキーワードを含めることができました。ただし、それぞれのアクションキーワードはUpdateExpression内で必ず一度ずつ使うようにします。

//例
UpdateExpression: 'SET #a = :aval, #b = :bval REMOVE #c'

参考:UpdateExpression

SETアクション:

属性の追加や変更、数値属性の足し引き、リスト属性への値の追加ができます。(上の例を参照)
list_appendの注意点:ExpressionAttributeValuesでリスト属性に追加する値を定義する時に、値を[]で囲む必要があります。(docClientがリスト属性を認識する必要があるからです。)

REMOVEアクション:

特定の属性の項目からの除去、リスト属性からの値の除去ができます。

ExpressionAttributeNames:{
    '#pr': 'price',
    '#c': 'color'
}
UpdateExpression: 'REMOVE #pr, #c[1]'//price属性と、color属性の2番目の値を削除する

ADDアクション:

数値属性の足し算と、セット型への値の追加ができます。ドキュメントではなるべくSETアクションを使用することが推奨されています。なお、セット型はネイティブのJavascirptには存在しないデータ型なので、どうしても使いたい場合は項目をputで作成する時に、属性の値をcolor : docClient.createSet(['Red', 'Black', 'White'])とすると、このデータ型はリスト型(L)ではなくString Set型(SS)になり、ADDアクションで項目を追加することが出来るようになります。

参考:createSet()

ExpressionAttributeNames:{
    '#pr': 'price',
    '#c': 'color'//ここではcolor属性はString Set型とする
},
ExpressionAttributeValues:{
    ':addPrice': 7400000,//price属性に7400000を足す
    ':addColor': docClient.createSet(['Yellow', 'Gray'])//color属性にYellowとGrayを追加する
},
UpdateExpression: 'ADD #pr :addPrice, #c :addColor'

数値属性の足し算について、上記の例は既に値がある場合、インクリメントを7400000回繰り返すという処理になるようです。よくわからないですが、ADDアクションはSETアクション代替可能なので、使う必要はないと思います。

DELETEアクション:

セット型から特定の値を削除することができます。

ExpressionAttributeNames:{
    '#c': 'color'//ここではcolor属性はString Set型とする
},
ExpressionAttributeValues:{
    ':deleteColor': docClient.createSet(['Red', 'Blue'])//color属性からRedとBlueを削除する
},
UpdateExpression: 'DELETE #pr :deletePrice'

put と update について

put で存在するアイテムの更新を行うことが可能です。また、逆に update で存在しないアイテムを追加することも可能です。

この2つの違いは対象のアイテムが存在する時に、明示的に指定していない属性を削除する(put)か、削除しない(update)かの違いとなります。

delete(1つ削除)

SuperCarTableにあるプライマリキー(carId)が12の項目を削除します。

var params = {
    TableName: 'SuperCarTable',
    Key:{//削除したい項目をプライマリキー(及びソートキー)によって1つ指定
         carId: 12
    }
};
docClient.delete(params, callback);

query(検索)およびscan(全件取得)

query

DynamoDBでは、基本的にそのテーブルのプライマリーキーまたはソートキー(またはその両方)でしかqueryできません。ただしグローバル・セカンダリ・インデックス(GSI)を使うことでそれ以外の属性でもqueryできるようになります。

グローバル・セカンダリ・インデックス(GSI)

ノーベル賞の歴代受賞者について、次のような属性のあるテーブルを考えます。

name(プライマリーキー) year(ソートキー) category nation

受賞が1980年より前の受賞者をqueryする

この場合はqueryの対象がソートキーなので、そのままqueryできます。

var params = {
    TableName: 'NovelPrizeTable',
    ExpressionAttributeNames:{'#y': 'year'},
    ExpressionAttributeValues:{':val': 1980},
    KeyConditionExpression: '#y <= :val'//検索対象が満たすべき条件を指定
};
docClient.query(params, function(err, data){
    if(err){
        console.log(err);
    }else{
       data.Items.forEach(function(person, index){
           console.log(person.name);//1980年以前の受賞者の名前
       });
    }
});

経済学賞を受賞した人をqueryする

この場合、categoryをqueryの対象とするため、categoryをプライマリーキーとするグローバル・セカンダリ・インデックスを作成します。
1. DynamoDBのコンソールの「インデックス」というタブを開く
2. 「インデックスを作成」
3. プライマリーキーに'category'と入力し、データ型は文字列を選択
4. ソートキーは空欄
5. インデックス名は'category-index'
6. 「インデクスの作成」

コードは下記のようになります。

var params = {
    TableName: 'NovelPrizeTable',
    IndexName: 'category-index',//インデックス名を指定
    ExpressionAttributeNames:{'#c': 'category'},
    ExpressionAttributeValues:{':val': 'economics'},
    KeyConditionExpression: '#c = :val'//検索対象が満たすべき条件を指定
};
docClient.query(params, function(err, data){
    if(err){
        console.log(err);
    }else{
       data.Items.forEach(function(person, index){
           console.log(person.name);//経済学賞受賞者の名前
       });
    }
});

GSIに対するquery時のIAMロールについて

NovelPrizeTableへの操作を許可するためには、NovelPrizeTableのARNに対する操作を許可するIAMロールを割り当てると思います。しかしNovelPrizeTableで作成したGSIに対してqueryを行うためには、新たに当該GSIのARNを操作を許可するResourceに追加する必要があります。その場合のIAMロールは下記のようになります。(テーブル名の後に/index/(インデックス名))

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:Query"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:********:table/NovelPrizeTable",
                "arn:aws:dynamodb:ap-northeast-1:********:table/NovelPrizeTable/index/category-index"
            ]
        }
    ]
}

queryの条件式について

KeyConditionExpressionでは下記の演算子が使えます。

  • #a = :val
  • #a < :val
  • #a <= :val
  • #a > :val
  • #a >= :val
  • #a <> :val
  • #a BETWEEN :minVal AND :maxVal
  • begins_with (#a, :str) → #aという属性が:strで始まる

また、下記の論理演算子も使えます。

  • #a = :aval AND #b = :bval
  • #a = :aval OR #b = :bval
  • NOT #a = :aval

【追記】NOR演算子はありませんでした。また、KeyConditionExpressionでは、プライマリーキーの条件式では=しか使えないなどの制限があります。最終的には下記のパターンが有効となります。( @nak2k さんご指摘ありがとうございます! )


#partitionKeyName = :partitionkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName = :sortkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName < :sortkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName <= :sortkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName > :sortkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName >= :sortkeyval
#partitionKeyName = :partitionkeyval AND #sortKeyName BETWEEN :sortkeyval1 AND :sortkeyval2
#partitionKeyName = :partitionkeyval AND begins_with ( #sortKeyName, :sortkeyval )

scan(全件取得)

SuperCarTableの項目を全件取得します。

var params = {
    TableName: 'SuperCarTable',
};
docClient.scan(params, function(err, data){
    if(err){
        console.log(err);
    }else{
       data.Items.forEach(function(car, index){
           console.log(car.name);
       });
    }
});

FilterExpressionについて

queryとscanをした後で、さらに別の属性で特定の条件に合う項目だけを返したい場合には、FilterExpressionを使うことができます。しかし、これはqueryもしくはscanをした後で項目をフィルタリングするものなので、queryやscan結果の件数が多くなるごとに処理に多くの時間がかかるようになってしまいます。(要出典)ベストプラクティスはqueryを使ってかなり件数を絞り込んだあとで、さらに絞り込むためにFilterExpressionを使うことです。(筆者の意見です。要出典。)

var params = {
    TableName: 'NovelPrizeTable',
    ExpressionAttributeNames:{
        '#y': 'year',
        '#n': 'nation'
    },
    ExpressionAttributeValues:{
        ':year': 1980
        ':nation': '日本'
    },
    KeyConditionExpression: '#y <= :year'//検索対象が満たすべき条件を指定
    FilterExpression: '#n = :nation'//検索対象の中から返す項目が満たすべき条件を指定
};
docClient.query(params, function(err, data){
    if(err){
        console.log(err);
    }else{
       data.Items.forEach(function(person, index){
           console.log(person.name);//1980年以前の日本の受賞者の名前
       });
    }
});

注意点: FilterExpressionで指定できる属性は、プライマリーキーとソートキー以外の属性である必要があります。プライマリーキーとソートキーの条件はKeyConditionExpressionの中で指定します。

バッチ処理

DynamoDBのバッチ処理では、最大100項目を同時に追加/削除/取得できます。ただし1項目当たりの容量によって100項目より少なくなる可能性があります。

参考:プロビジョニングされたスループット

batchWrite(一気にたくさん追加or削除)

同時に複数の項目を追加したり削除したい場合、putやdelete処理を回していては埒が明かないので、batchWriteを使って一気に書き込みandし削除ます。

PutRequest

var putArry = [
    {companyId: 1, name: 'トヨタ', nation: '日本'},
    {companyId: 2, name: 'メルセデス・ベンツ', nation: 'ドイツ'},
    {companyId: 3, name: 'フォード', nation: 'アメリカ'},
    {companyId: 4, name: 'アストン・マーチン', nation: 'イギリス'},
    {companyId: 5, name: 'マセラティ', nation: 'イタリア'},
];//この内容を一気に追加したい時

var requestArry = [];
putArry.forEach(function(companyObj, index){
    requestObj = {
        PutRequest:{
            Item: companyObj //PutRequest > Item の階層にオブジェクトを一つずつ入れていく
        }
    };
    requestArry.push(requestObj);
});

var params = {
  RequestItems: {
    CarCompanyTable : requestArry //PutRequestをまとめた配列を、テーブル名をkeyとするオブジェクトに格納し、それをさらにRequestItemsというkeyのオブジェクトに格納する。
    //BoatCompanyTable: anotherRequestArry とすると、別のテーブルにも同時に項目を追加できる
  }
};
docClient.batchWirte(params, callback)

DeleteRequest

var deleteArry = [1,2,3,4,5];//companyIdが1~5の項目を一気に削除したい時

var requestArry = [];
deleteArry.forEach(function(val, index){
    requestObj = {
        DeleteRequest:{
            Key:{
                companyId: val
            }
        }
    };
    requestArry.push(requestObj);
});

var params = {
  RequestItems: {
    CarCompanyTable : requestArry //DeleteRequestをまとめた配列を、テーブル名をkeyとするオブジェクトに格納し、それをさらにRequestItemsというkeyのオブジェクトに格納する。
    //BoatCompanyTable: anotherRequestArry とすると、別のテーブルの項目も同時に削除できる
  }
};
docClient.batchWirte(params, callback)

batchGet(一気にたくさん取得)

var getArry = [1,2,3,4,5];//companyIdが1~5の項目を一気に取得したい時

var requestArry = [];
getArry.forEach(function(val, index){
    keyObj = {
        companyId: val
    };
    keyArry.push(keyObj);
});

var params = {
    RequestItems: {
        CarCompanyTable:{Keys: keyArry}//テーブル名をキーとするオブジェクトの中にKeysをキーとするオブジェクトの値としてkeyArryを格納
        //BoatCompanyTable:{Keys: anotherKeyArry}とすると、他のテーブルからも同時に項目を取得できる
    }
};
docClient.batchGet(params, function(err, data){
    if(err){
        console.log(err);
    }else{
        data.Responses.CarCompanyTable.forEach(function(company, index){
           console.log(company.name);
        });
    }
});

テクニックの補足

ExpressionAttributeNamesについて

updateやqueryなどで~~Expressionの中で属性名を指定する場合に、その属性名が数値で始まるか、スペース、特殊文字、または予約語を含む場合、その属性名をプレースホルダー(置換変数)に置き換えて指定する必要があります。逆にそれらに当てはまらなければ、属性名をそのまま使うことができます。

プレースホルダーは、必ず#で始まり、1文字以上の英数字が続くものです。

ExpressionAttributeNames:{
    '#n': 'name'//nameという属性名は予約語なので、#nというプレースホルダーに置き換える
}

参考:属性の名前および値でのプレースホルダーの使用
参考:DynamoDBの予約語

ExpressionAttributeValuesについて

updateやqueryなどで、~~Expressionの文字列の中で属性値を指定する場合は必ず属性値をプレースホルダーに置き換えて指定する必要があります。属性値のプレースホルダーは、必ず:で始まり、1文字以上の英数字が続くものです。

ExpressionAttributeValues:{
    ':val': 1994
}

ProjectionExpressionについて

get, scan, query, batchGetにおいて、必ずしも全ての属性値をユーザーに返したくない場合があります。その場合はProjectionExpressionに返す属性名だけを指定することで、指定されていない属性を取得しないようにできます。Lambda経由でユーザーにデータを渡す場合は、ProjectionExpressionによって属性別のアクセス制御が行なえます。

{
    carId: 12,
    name: 'フェラーリ458',
    price: 28300000,
    engine: {
        type: 'V型8気筒',
        power: 578
    },
    color:['red', 'black', 'white']
};
//このような項目があった場合に、price属性を取得したくないケース

var params = {
    TableName: 'SuperCarTable',
    Key:{
         carId: 12,
    },
    ExpressionAttributeNames:{
        '#n': 'name'//予約語なのでプレースホルダーに置き換え
    },
    ProjectionExpression: 'carId,#n,engine,color'//price以外の属性名をString型&カンマ区切りで列挙
};
docClient.get(params, function(err,data){
    if(err){
        console.log(err);
    }else{
        console.log(data.Item.name);//'フェラーリ458'
        console.log(data.Item.price);//undefined
    }
});

ConditionExpressionについて

put, get, update, deleteにおいて、対象となる項目が特定の条件を満たしている場合のみその操作を行うように条件をつけることができます。なお、下記の#aや:valなどは全て属性名もしくは属性値のプレースホルダーです。

比較演算子

  • #a = :val
  • #a < :val
  • #a <= :val
  • #a > :val
  • #a >= :val
  • #a BETWEEN :minVal AND :maxVal
  • #a IN (:val1, :val2, :val3) →#aが:val1か:val2か:val3の場合に操作を実行

論理演算子

  • #a = :aval AND #b = :bval
  • #a = :aval OR #b = :bval
  • #a = :aval NOR #b = :bval
  • NOT #a = :aval

その他

  • attribute_exists (#a) →#aという属性の値が存在した場合に操作を実行
  • attribute_not_exists (#a) →#aという属性の値が存在しない場合に操作を実行
  • attribute_type (#a, :typeS) →#aという属性の値が:typeS('S'のプレースホルダー:String型)であれば操作を実行
  • begins_with (#a, :sbstr) →#aという属性の値が:sbstrの文字列で始まれば操作を実行
  • contains (#a, :val)→#aという属性の値が:valを含めば操作を実行
  • size (#a) < :maxSize →#aという属性の値のサイズ(バイト)が:maxSizeを超えなければ操作を実行

参考:Comparison Operator and Function Reference

例) 既に存在する項目の上書きを防ぐ

var params = {
    TableName: 'SuperCarTable',
    Item:{
         carId: 12,
         name: 'フェラーリ458スパイダー',
         price: 28300000,
         engine: {
             type: 'V型8気筒',
             power: 578
         },
         color:['red', 'black', 'white']
    },
    ExpressionAttributeNames:{
        '#c': 'carId'
    },
    ConditionExpression: 'attribute_not_exists(#c)'//既にcarIdが12の項目の属性値にcarIdが存在する(=既にcarIdが12の項目が存在する)場合には操作を行わない
};
docClient.put(params, callback);

注意点: batchWriteとbatchGetにおいては、ConditionExpressionを使うことはできません。

まとめ

  • 各操作毎にparamsに突っ込む内容を押さえる
  • Expression系のプレースホルダーに慣れる

割りとこの2つを押さえると入り口はいけるんじゃないかと思いました。あとはIAMのアクセス制御とか消費するキャパシティとかですかね・・・がんばります。

P.S 間違ってる箇所があったらご指摘願いますm(_ _)m