あいつの日誌β

働きながら旅しています。

DynamoDB の特性を理解しないで開発していたら pagination の実装でつまづいた

前置き

私の勘違いが書かれている可能性がありますが、ご指摘頂けると大変喜びます。特に「いや、それはこうやったらできるよ」という情報をお待ちしております。

あらすじ

Serverless Framework 使って個人サイトを作ろうとしたら DynamoDB で若干手間取ったので後世のエンジニアの為に備忘録を残します。

DynamoDB について

DynamoDB は優れた分散データベースです。バックアップやスケールアップなどに時間を割く必要がないように工夫されたサービスです。なんですが、これまでの RDS や KVS とは注力されている箇所が違うで少し使いづらい部分があります。

パーティションキーの概念

DynamoDB では Pagination がしづらいです。できなくは無いのですが RDS と少し違うところがあります。検索条件にインデックスが含まれていないと正しい件数を取得できませんそしてインデックスを作成するには必ずパーティショニングする必要があります

というわけで実際に動作させて説明したいと思います。

準備

Serveless Framework の migration 機能が便利(再起動するたびにデータがセットされる)なのでこれを利用します。

環境は以下の通り

% node -v
v8.11.1

% sls -v
1.26.1

雛形を作成

sls create --template aws-nodejs --path practice-dynamodb && cd $_
touch example1.js example2.js
mkdir migrations
touch migrations/locations.json

migrations/locations.json を作成

[
  {
    "country": "JP",
    "cityEn": "Shibuya-ku",
    "stateEn": "Tokyo",
    "cityJp": "渋谷区",
    "createdAt": "2018-04-19 20:10:43",
    "stateJp": "東京都",
    "id": "1028337572",
    "name": "Xiang ni cafe(シャンニーカフェ)"
  },
  {
    "country": "JP",
    "cityEn": "Shibuya-ku",
    "stateEn": "Tokyo",
    "cityJp": "渋谷区",
    "createdAt": "2018-04-18 20:08:45",
    "stateJp": "東京都",
    "id": "620336124833357",
    "name": "岡本肉店"
  },
  {
    "country": "JP",
    "cityEn": "Shibuya-ku",
    "stateEn": "Tokyo",
    "cityJp": "渋谷区",
    "createdAt": "2018-04-17 15:31:09",
    "stateJp": "東京都",
    "id": "429313840761469",
    "name": "香港ロジ 原宿店"
  },
  {
    "country": "JP",
    "cityEn": "Sapporo-shi",
    "stateEn": "Hokkaido",
    "cityJp": "札幌市",
    "createdAt": "2018-04-16 14:07:07",
    "stateJp": "北海道",
    "id": "5841572",
    "name": "5坪 gotsubo"
  },
  {
    "country": "JP",
    "cityEn": "Sapporo-shi",
    "stateEn": "Hokkaido",
    "cityJp": "札幌市",
    "createdAt": "2018-04-15 14:07:07",
    "stateJp": "北海道",
    "id": "3518106",
    "name": "SAKANOVA"
  },
  {
    "country": "JP",
    "cityEn": "Toshima-ku",
    "stateEn": "Tokyo",
    "cityJp": "豊島区",
    "createdAt": "2018-04-15 14:07:07",
    "stateJp": "東京都",
    "id": "447856545563610",
    "name": "大衆酒場 かぶら屋池袋7号店"
  },
  {
    "country": "JP",
    "cityEn": "Sapporo-shi",
    "stateEn": "Hokkaido",
    "cityJp": "札幌市",
    "createdAt": "2018-04-15 14:07:07",
    "stateJp": "北海道",
    "id": "52232",
    "name": "菜もっきりや"
  },
  {
    "country": "JP",
    "cityEn": "Sumida-ku",
    "stateEn": "Tokyo",
    "cityJp": "墨田区",
    "createdAt": "2018-04-14 14:07:07",
    "stateJp": "東京都",
    "id": "8588811",
    "name": "立ち呑み 粋"
  },
  {
    "country": "JP",
    "cityEn": "Kawasaki-shi",
    "stateEn": "Kanagawa",
    "cityJp": "川崎市",
    "createdAt": "2018-04-14 14:07:07",
    "stateJp": "神奈川県",
    "id": "1032997013",
    "name": "和食と立喰い寿司 ナチュラ"
  },
  {
    "country": "JP",
    "cityEn": "Shibuya-ku",
    "stateEn": "Tokyo",
    "cityJp": "渋谷区",
    "createdAt": "2018-04-14 14:07:07",
    "stateJp": "東京都",
    "id": "896121",
    "name": "Bistro ひつじや"
  },
  {
    "country": "JP",
    "cityEn": "Takamatsu-shi",
    "stateEn": "Kagawa",
    "cityJp": "高松市",
    "createdAt": "2018-04-13 14:07:07",
    "stateJp": "香川県",
    "id": "1028420342",
    "name": "たべごとや 艸 - そう"
  },
  {
    "country": "JP",
    "cityEn": "Osaka-shi",
    "stateEn": "Osaka",
    "cityJp": "大阪市",
    "createdAt": "2018-04-12 14:06:28",
    "stateJp": "大阪府",
    "id": "242874420",
    "name": "魚屋ひでぞう 立ち呑み"
  },
  {
    "country": "JP",
    "cityEn": "Sapporo-shi",
    "stateEn": "Hokkaido",
    "cityJp": "札幌市",
    "createdAt": "2018-04-11 14:06:28",
    "stateJp": "北海道",
    "id": "1021458277",
    "name": "鮨角打ち 裏・小樽酒商たかの札幌駅前店"
  }
]

serverless.yml を修正

service: practice-dynamodb
plugins:
   - serverless-dynamodb-local

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
      seed: true
    seed:
      development:
        sources:
          - table: locations
            sources: [./migrations/locations.json]

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

resources:
  Resources:
    ArticlesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: locations
        AttributeDefinitions:
         - AttributeName: id
           AttributeType: S
         - AttributeName: country
           AttributeType: S
         - AttributeName: stateEn
           AttributeType: S
         - AttributeName: cityEn
           AttributeType: S
         - AttributeName: createdAt
           AttributeType: S
        KeySchema:
         - AttributeName: id
           KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: country-index
            KeySchema:
             - AttributeName: country
               KeyType: HASH
             - AttributeName: createdAt
               KeyType: RANGE
            Projection:
              NonKeyAttributes:
               - stateEn
               - cityEn
              ProjectionType: INCLUDE
            ProvisionedThroughput:
              ReadCapacityUnits: '1'
              WriteCapacityUnits: '1'
          - IndexName: stateEn-index
            KeySchema:
             - AttributeName: stateEn
               KeyType: HASH
            Projection:
              NonKeyAttributes:
               - id
              ProjectionType: INCLUDE
            ProvisionedThroughput:
              ReadCapacityUnits: '1'
              WriteCapacityUnits: '1'
          - IndexName: cityEn-index
            KeySchema:
             - AttributeName: cityEn
               KeyType: HASH
             - AttributeName: createdAt
               KeyType: RANGE
            Projection:
              NonKeyAttributes:
               - id
              ProjectionType: INCLUDE
            ProvisionedThroughput:
              ReadCapacityUnits: '1'
              WriteCapacityUnits: '1'
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

dynamodb を local に install して動作確認します。

sls dynamodb install

動作確認します。13件のアイテムを取得できていればOKです。

aws dynamodb scan --table-name locations --endpoint-url http://localhost:8000 | jq .Count
13

というわけで locations テーブルにはいくつかのインデックスが存在します。それぞれのインデックスで出来る事と出来ない事を確認します。

Just Do it

dynamoDB を参照するコマンドは大きく分けて scan, get, query の3種類です。 まずはプライマリーキーに対してコマンドを実行してみます。

example1: scan, get, query

create example1.js

const aws = require('aws-sdk');

function getDocClient() {
  return new aws.DynamoDB.DocumentClient({
    region: 'ap-northeast-1',
    endpoint: "http://localhost:8000"
  });
}

function main() {

  // Good
  getDocClient().scan({
    TableName: 'locations',
  }).promise().then(console.log).catch(console.error);

 // Good
  getDocClient().get({
    TableName: 'locations',
    Key: {
      id: "1032997013"
    }
  }).promise().then(console.log).catch(console.error);

  // Good
  getDocClient().query({
    TableName: 'locations',
    KeyConditionExpression: 'id = :id',
    ExpressionAttributeValues: {
      ':id': "1032997013"
    },
    Limit: 1
  }).promise().then(console.log).catch(console.error);

  // No Good
  getDocClient().query({
    TableName: 'locations',
    KeyConditionExpression: 'begins_with(id, :id)',
    ExpressionAttributeValues: {
      ':id': "103"
    }
  }).promise().then(console.log).catch(console.error);
}

main();

scan は全レコードをシーケンシャルに取得します。get は一意のユニークキーからレコードを一つだけ取得します。

DynamoDB のテーブル設計で考えないといけないのは query です。上記の結果からHash Key に対して部分一致の query が発行できないことが分かります。

DynamoDB では HASH Key を country:state:city のような構造にして先頭部分から一致したものを取得することはできません。その場合は RANGE KEY にする必要があります。

example2: セカンダリインデックス

今度は都道府県別に locations を取得したい場合は以下のようにします。 index を使用するので IndexName に 'stateEn-index' と cityEn-index を指定します。

create example2.js

const aws = require('aws-sdk');

function getDocClient() {
  return new aws.DynamoDB.DocumentClient({
    region: 'ap-northeast-1',
    endpoint: "http://localhost:8000"
  });
}

function main() {

  // Good
  getDocClient().query({
    TableName: 'locations',
    IndexName: 'stateEn-index',
    KeyConditionExpression: 'stateEn = :stateEn',
    ExpressionAttributeValues: {
      ':stateEn': "Hokkaido"
    }
  }).promise().then(console.log).catch(console.error);

  // Good
  getDocClient().query({
    TableName: 'locations',
    IndexName: 'cityEn-index',
    KeyConditionExpression: 'cityEn = :cityEn AND createdAt >= :createdAt',
    ExpressionAttributeValues: {
      ':cityEn': "Sapporo-shi",
      ':createdAt': "2018-04-16"
    }
  }).promise().then(console.log).catch(console.error);

  // Good
  getDocClient().query({
    TableName: 'locations',
    IndexName: 'cityEn-index',
    KeyConditionExpression: 'cityEn = :cityEn AND begins_with(createdAt, :createdAt)',
    ExpressionAttributeValues: {
      ':cityEn': "Sapporo-shi",
      ':createdAt': "2018-04"
    }
  }).promise().then(console.log).catch(console.error);
  
  // Good
  getDocClient().query({
    TableName: 'locations',
    IndexName: 'country-index',
    KeyConditionExpression: 'country = :country',
    ExpressionAttributeValues: {
      ':country': "JP",
    },  
    ScanIndexForward: false
  }).promise().then(console.log).catch(console.error);
  
}

main();

stateEn-indexHASH, cityEn-indexHASH + RANGE で作成されています。 stateEn-index は createdAt を含まないのでソート出来ないのですが、 cityEn-index はcreatedAt でソートされています。

このように Pagination が必要となる場合は HASH + RANGE でインデックスを作成する必要があります。

ただし KeyConditionExpression: 'cityEn = :cityEn AND begins_with(createdAt, :createdAt)' のように必ず HASH Key を指定しなければいけません。

このように DynamoDB ではデータがパーティションで区切られる前提で設計されています。もし全件をソートするなら country-index のように全データを同一のパーティションに含める必要があります。たぶんその辺りの事を理解した上で DynamoDB を利用する、しないを考えた方が良いでしょう。この例だとすでに Index を 3つ追加しているので RDS を検討した方が良いのかもしれません。

まとめ

DynamoDB は我々エンジニアにとっては強力な補助ツールとなるでしょう。ただし利用する場合はその特性を良く理解する必要があります。

近年では Serverless Framework という選択肢が出てきていますが、案件によってはそのような選択をできる様にする為に DynamoDB の Table 設計に一度慣れておいた方が良いと思います。

あとそれから

その DynamoDB で個人サイトつくりました。 飲み歩くのが好きなので飲み歩いたお店を記録するサイトです。

http://drunkard.tokyo/

こちらからは以上です。

MacOSX で使用されているポートを指定して kill する

やり方

lsof -i :8000 -t | xargs kill

ところで

local Dynamodb を使って開発していて dynamoDB を再起動しようとするといつも port が開きっぱなしなので lsof -i :8000 してプロセスを特定してから kill してから再起動しているんだけど serverless で dynamoDB を使っている人たちは普段どうやっているんだろう...

lsof -i :8000 -t | xargs kill && sls dynamodb start

Single Page Application を Serverless Framework と React で作って見ました。

とうわけで S3 に配備しました。API へのアクセスは API Gateway -> Lambda -> DynamoDB なのでそんなにお金かからないはず。しばらくは動かしたままにするのでよかったら下記URLへ訪問してみてください。

http://serverless-fav-articles.s3-website-ap-northeast-1.amazonaws.com/

この作業で覚えた事を備忘録として Qiita に記事を書いたのでそちらも合わせてご覧ください。

Single Page Application を Serverless Framework と React でやる Tutorial(1) https://qiita.com/okamuuu/items/57ef47ac43602fd9e5f9

Single Page Application を Serverless Framework と React でやる Tutorial(2) https://qiita.com/okamuuu/items/06b2c0de27bb267e792a

Single Page Application を Serverless Framework と React でやる Tutorial(3) https://qiita.com/okamuuu/items/4028c86f1ba72bbd12a2

Single Page Application を Serverless Framework と React でやる Tutorial(4) https://qiita.com/okamuuu/items/6e9c0b3e30a1881f5da5

Single Page Application を Serverless Framework と React でやる Tutorial(4) https://qiita.com/okamuuu/items/9082ef45928d98328a9d

こちらからは以上です。

マクロンを除去したい

マクロンとは

マクロン - Wikipedia

マクロンは、ダイアクリティカルマーク(発音区別符号)の一つで、長音記号ともいう。これに対する伝統的な短音記号はブレーヴェである。

どうして除去したいのか?

Google Map を使ったアプリを作ろうと思って reverseGeocoding したら何故かマクロンがついてきてしまい DB にそのまま突っ込むとマクロンあるのと無いので検索結果がずれるのがいやです。

結局こうした

もうちょっとエレガントな書き方できそうな気もしますが...

function deleteMacron(string) {
  return string
    .replace(`Ā`, `A`)
    .replace(`ā`, `a`)
    .replace(`Ī`, `I`)
    .replace(`ī`, `i`)
    .replace(`Ū`, `U`)
    .replace(`ū`, `u`)
    .replace(`Ē`, `E`)
    .replace(`ē`, `e`)
    .replace(`Ō`, `O`)
    .replace(`ō`, `o`)
}

console.log(deleteMacron('Hokkaidō')); // Hokkaido

とりあえずマクロンって言葉検索しづらいですね。

2018年春からフロントエンドを始める為の開発環境構築手順

あらすじ

春ですね。新人研修の準備をしましょう。

動作環境

babel-preset-env はこの記事を作成している時点では version 1 を使っています。しばらくすると version 2になるかもしれません。

% cat package.json                                                                     {
  "name": "practice-frontend",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.6.1",
    "http-server": "^0.11.1",
    "webpack": "^4.1.1",
    "webpack-cli": "^2.0.12"
  }
}

準備1: 静的サーバーを用意

mkdir practice-frontend && cd $_ 
mkdir dist
echo 'hello' > dist/index.html
yarn init -y  
yarn add http-server --save-dev 

localhost:8080 で静的サーバーを起動します。

yarn http-server dist

http://localhost:8080 にアクセスして hello が表示される事を確認します。

準備2: babel

mkdir src lib
touch .babelrc
yarn add babel-cli babel-preset-env --save-dev

.babelrc を作成します

{
  "presets": [
    ["env", {
      "targets": {
         "node": "v6.10",
         "ie": 11
      }
    }]
  ]
}

src/Dog.js を作成します。

export default class Dog {

  constructor(voice="bow wow") {
    this.voice = voice;
  }

  say() {
    return this.voice;
  }
}

トランスパイルします。

yarn babel -d lib/ src/

test.js を作成して node test.js を実行して動作を確認します。

var Dog = require('./lib/Dog').default;

var dog1 = new Dog();
var dog2 = new Dog('ruff ruff')

console.log(dog1.say())
console.log(dog2.say())

準備3: webpack 4

webpack は version 4から webpack と webpack-cli が別れたのでそれぞれを install します.

yarn add webpack webpack-cli --save-dev

webpack 4 からはある程度の規約がデフォルトで設定されるので webpack.config.js がなくても動作します。デフォルトではエントリーポイントが src/index.js となっています。先ほどの test.js と少し似ていますが src/index.js を用意します。

import Dog from './Dog';

var dog1 = new Dog();
var dog2 = new Dog('ruff ruff')

console.log(dog1.say())
console.log(dog2.say())

以下を実行すると dist/main.jsコンパイルされたファイルが出力されます。

yarn webpack --mode=development

以下のコマンドが動作することを確認します。

node dist/main.js

準備4: index.html を修正

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>test</title>
</head>
<body>
  hello world
<script type="text/javascript" src="main.js"></script>
</body>
</html>

この状態で yarn http-server dist を実行して http://localhost:8080 にアクセスしてデベロッパーコンソールなどで console.log が犬の鳴き声を出力している事を確認します。

http-server が cache を長くもっている(?)みたいなのでうまく表示されない場合は reload してみたりしてください。

まとめ

駆け足ですが最低限必要な開発環境を構築する手順を紹介しました。

webpack extensions の指定方法

hogehoge.jsrequire(‘hogehoge’) しているなら

resolve: {
  extensions: [".js"]
}

fugafuga.jsxrequire(‘fugafuga’) しているなら

resolve: {
  extensions: [".js", ".jsx"]
}

mogemoge.jsonrequire(‘mogemoge’)しているなら

resolve: {
  extensions: [".js", ".jsx", ".json"]
}

なんですが私は基本的には .js 以外は拡張子をつけて import したほうが良い気がします。つまり以下のようします。

require('hogehoge')
require('mogemoge.json');
require('mogemoge.scss');

webpack 2 以降は以下の記述でOK。

resolve: {
  extensions: [".js"]
}

ちなみに webpack 1 では拡張子無しで import する場合は以下のように空文字を記述していました。

resolve: {
  extensions: ["", ".js"]
}

おしまい

標準偏差の求め方

あらすじ

数学は思ったよりも人生に影響すると思ったので復習しようとおもいました。

標準偏差とは

下記URLをご覧ください。本記事はこちらを参考に Node.js を使って紹介しています。

https://atarimae.biz/archives/5379

標準偏差を求める

主に以下の手順で求めることができます。

  • 平均値を求める
  • 平均値から偏差を求める
  • 偏差から分散を求める

平均値を求める

以下のようなテスト情報があります。

const childMathTests = [
  {name: "A", value: 35},
  {name: "B", value: 55},
  {name: "C", value: 70},
  {name: "D", value: 80}
];

この生徒たちの平均値は以下の通り

(35 + 55 + 70 + 80) / 4 // 60

平均値から偏差を求める

各データの値から平均値を引いた値を求めます。これを偏差と呼びます。

35 - 60 === -25 // A
55 - 60 === -5  // B
70 - 60 === 10  // C
80 - 60 === 20  // D

求めた偏差を2乗します。

(35 - 60) ** 2 === 625
(55 - 60) ** 2 === 25
(70 - 60) ** 2 === 100
(80 - 60) ** 2 === 400

分散を求める

求めた偏差の2乗を合計して 4 で割ります。

(625 + 25 + 100 + 400) / 4 === 287.5

標準偏差を求める

分散の正の平方根を求めます。これがこの4人の数学の標準偏差です。

Math.sqrt((625 + 25 + 100 + 400) / 4) === 16.95582495781317

実装

Node.js では以下のように書けます

const childMathTests = [ 
  {name: "A", value: 35},
  {name: "B", value: 55},
  {name: "C", value: 70},
  {name: "D", value: 80} 
];

const values = childMathTests.map(x => x.value);

function sum(accumulator, currentValue) {
  return accumulator + currentValue;
}

function getStandardDeviation(values) {
  const average = values.reduce(sum) / values.length;
  const deviations = values.map(x => x - average);
  const dispersion = deviations.map(x => x ** 2).reduce(sum) / values.length;
  return Math.sqrt(dispersion);
}

console.log(getStandardDeviation(values));

実践

というわけでテスト結果からばらつきが多いテストかばらつきが少ないテストかを判断できるようになりました。

const childMathTestValues0316 = [0, 5, 10, 70, 80, 80, 82, 85, 93, 95];
const childMathTestValues0318 = [50, 52, 54, 60, 60, 60, 61, 61, 70, 72];

console.log(getStandardDeviation(childMathTestValues0316)); // 36.67151483099655
console.log(getStandardDeviation(childMathTestValues0318)); // 6.6783231428256

0316 はほとんどのが80点以上とっているもしくは一部の生徒が平均点を下げています。つまり平均点付近にあまりマッチする生徒がいない。 0318 はほとんどの人が平均点付近である点数にマッチしています。

両日のテストはどちらも平均点が60点です。どちらのテストも「平均点が60点のテストで70点もとった」という言い方ができます。

なんですがばらつきの多い 0316 ではほとんどの生徒が 80点を超えているので実はそんなにすごくないです。 0318 だと70点を超えている生徒が 2人しかいないのですごいと言えると思います。

まとめ

というわけで標準偏差は学校で習ったようですが(記憶にない)社会に出てから使う機会はないと思っていました。 なんですが使う機会がなかったわけではなく、応用が効く場面に遭遇してもそれを使えてなかった気がします。

そんなわけで久しぶりに数学を勉強してみました。こちらからは以上です。

補足: 平均偏差と標準偏差

偏差を2乗する理由は平均偏差ではばらつきを正しく把握できないケースがあるので、「離れれば離れるほど、ポイントを高くする」という手法が編み出されました。これが標準偏差。2乗して計算したものを平方根で元に戻しています。