なになれ

IT系のことを記録していきます

DynamoDBのスキーマ設計について学んだこと

DynamoDBのスキーマ設計について公式ドキュメントのベストプラクティスなどを見ながら学んだ記録です。

スキーマ設計の考え方

DynamoDBのスキーマ設計はRDBとは異なり、以下の考え方を採用します。

  • テーブルを分けずに1つのテーブルを使う
  • PartitionKeyとSortKeyで1対多の関係を表現する
  • インデックスやSortKeyを用いて、検索条件に対応する

スキーマ設計

テーブル設計

partitionKey sortKey attribute
posts post-id title,content,author
post-id comment-id content,author
  • 記事とコメントの1:多の関係はPartitionKeyとSortKeyで定義できる
  • 全記事を取得するにはPartitionKeyにpostsを指定する
  • 個別の記事を取得するにはPartitionKeyにpostsとSortKeyにpost-idを指定する
  • コメントを取得するにはPartitionKeyにpost-idを指定する

インデックス設計

indexKey partitionKey sortKey Attribute
user-id posts post-id title,content,author
  • 特定のユーザが書いた記事を取得するにはindexKeyにuser-idを指定する

インデックスを使わないパターン

インデックスを使わずにSortKeyだけでも同様のことが実現可能です。
SortKeyはbegins_with関数で特定の文字列から始まるクエリを記述することが可能です。
これを利用して、以下のように複合キーにします。

partitionKey sortKey attribute
posts user-id#post-id title,content,author

user-idをクエリの条件にすることで特定のユーザが書いた記事を取得可能です。

ただこのテーブル設計の場合、個別の記事を取得するのに事前にユーザが分かっていなければならないという問題点があります。
一方で、インデックスの作成数には制限があります。インデックスを使うのか、テーブル設計で対応するのか、使い分けが必要になります。
個人的には特定の値に依存しない汎用的なインデックスを作ってしまうのが良いと思います。

実践

今回はブログを想定した記事とコメントのデータ構造を題材とします。
以下のような構造です。
f:id:hi1280:20190617212423p:plain

ローカルのDynamoDBをインストールする

以下を参考にして、ローカルにDynamoDBをインストールします。

コンピュータ上の DynamoDB (ダウンロード可能バージョン) - Amazon DynamoDB

テーブルを作成する

今回はNode.js上でawssdkを使い、DynamoDBを操作します。

create-table.js

const AWS = require("aws-sdk");
const util = require("util");

AWS.config.update({
  region: "us-west-2",
  endpoint: "http://localhost:8000"
});

const dynamodb = new AWS.DynamoDB();
const params = {
  TableName: "Sample",
  KeySchema: [
    { AttributeName: "partitionKey", KeyType: "HASH" },
    { AttributeName: "sortKey", KeyType: "RANGE" }
  ],
  AttributeDefinitions: [
    { AttributeName: "partitionKey", AttributeType: "S" },
    { AttributeName: "sortKey", AttributeType: "S" }
  ],
  BillingMode: "PAY_PER_REQUEST"
};

async function createTable() {
  const createTable = util.promisify(dynamodb.createTable).bind(dynamodb);
  const data = await createTable(params);
  console.log(
    "Created table. Table description JSON:",
    JSON.stringify(data, null, 2)
  );
}

createTable();

partitionKeyとsortKeyを文字列型の属性で作成します。

インデックスを作成する

create-index.js

// 省略

const dynamodb = new AWS.DynamoDB();
const params = {
  TableName: "Sample",
  AttributeDefinitions: [{ AttributeName: "indexKey", AttributeType: "S" }],
  GlobalSecondaryIndexUpdates: [
    {
      Create: {
        IndexName: "index1",
        KeySchema: [
          {
            AttributeName: "indexKey",
            KeyType: "HASH"
          }
        ],
        Projection: {
          ProjectionType: "ALL"
        },
        ProvisionedThroughput: {
          ReadCapacityUnits: "0",
          WriteCapacityUnits: "0"
        }
      }
    }
  ]
};

async function createIndex() {
  const updateTable = util.promisify(dynamodb.updateTable).bind(dynamodb);
  const data = await updateTable(params);
  console.log(
    "update table. Table description JSON:",
    JSON.stringify(data, null, 2)
  );
}

createIndex();

グローバルセカンダリインデックスをindex1という名前で作成します。
このインデックスではindexKeyというpartitionKeyを持つことが定義されています。
これにより、indexKeyの値と等価の条件で項目を検索することが可能になります。

データをロードする

load-data.js

// 省略

const fs = require("fs");
const docClient = new AWS.DynamoDB.DocumentClient();

const allData = JSON.parse(fs.readFileSync(__dirname + "/data.json", "utf8"));
function loadData() {
  allData.forEach(async data => {
    const params = {
      TableName: "Sample",
      Item: {
        partitionKey: data.partitionKey,
        sortKey: data.sortKey,
        attribute: data.attribute,
        indexKey: data.indexKey
      }
    };
    const put = util.promisify(docClient.put).bind(docClient);
    await put(params);
    console.log("PutItem succeeded:", data.partitionKey);
  });
}

loadData();

data.json

[
  {
    "partitionKey": "posts",
    "sortKey": "post-1",
    "attribute": {
      "title": "記事のタイトル",
      "content": "内容",
      "author": "書いた人"
    },
    "indexKey": "user-1"
  },
  {
    "partitionKey": "posts",
    "sortKey": "post-2",
    "attribute": {
      "title": "記事のタイトル2",
      "content": "内容2",
      "author": "書いた人2"
    },
    "indexKey": "user-2"
  },
  {
    "partitionKey": "posts",
    "sortKey": "post-3",
    "attribute": {
      "title": "記事のタイトル3",
      "content": "内容3",
      "author": "書いた人"
    },
    "indexKey": "user-1"
  },
  {
    "partitionKey": "post-1",
    "sortKey": "comment-1",
    "attribute": {
      "content": "コメント内容",
      "author": "書いた人"
    }
  },
  {
    "partitionKey": "post-1",
    "sortKey": "comment-2",
    "attribute": {
      "content": "コメント内容2",
      "author": "書いた人2"
    }
  },
  {
    "partitionKey": "post-2",
    "sortKey": "comment-1",
    "attribute": {
      "content": "コメント内容3",
      "author": "書いた人3"
    }
  }
]

data.jsonの値を読み込んでDynamoDBに登録します。

クエリを実行する

query.js

// 省略

const docClient = new AWS.DynamoDB.DocumentClient();

async function query(params) {
  const query = util.promisify(docClient.query).bind(docClient);
  const data = await query(params);
  data.Items.forEach(function(item) {
    console.log(JSON.stringify(item));
  });
}

async function exec() {
  console.log("全記事を取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression: "partitionKey = :value",
    ExpressionAttributeValues: {
      ":value": "posts"
    }
  });
  console.log("特定の記事を取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression:
      "partitionKey = :partitionValue and sortKey = :sortValue",
    ExpressionAttributeValues: {
      ":partitionValue": "posts",
      ":sortValue": "post-1"
    }
  });
  console.log("特定のユーザの全記事を取得");
  await query({
    TableName: "Sample",
    IndexName: "index1",
    KeyConditionExpression: "indexKey = :value",
    ExpressionAttributeValues: {
      ":value": "user-1"
    }
  });
  console.log("特定の記事の全コメントを取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression: "partitionKey = :partitionValue",
    ExpressionAttributeValues: {
      ":partitionValue": "post-1"
    }
  });
}

exec();

スキーマ設計時に想定していた検索ができることを確認します。

まとめ

DynamoDBを利用する場合には事前にスキーマ設計を考えなければいけないです。
どのような検索条件が必要かといったことを踏まえて設計を考える必要があり、RDBのようにリレーショナルな形で設計するよりも難しいように思います。

参考情報

Jestを使ってJavaScriptで快適にテストする

Jestとは

JavaScriptのテストフレームワークです。
フロントエンド向けのテストフレームワークで注目されました。
ただJest自体はフロントエンドに限定されずに有用なテストフレームワークです。

jestjs.io

Jestのメリット

  • テストを書き始めるまでが簡単
  • Matcherが揃っている
  • Mockが簡単に用意できる

これらのメリットについて具体的な実装を交えて説明します。

テストの書き方

テストを書く準備

Jestをインストールする

npm install --save-dev jest

テストファイルを書く

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

これで始められます。
toBeなどのテストの検証に必要なMatcherが最初から用意されています。
テストの記述の仕方はJasmineやMochaといったテストフレームワークと同様です。

Matcherを使う

toBe,toEqualのMatcherを覚えておけば大抵のことはカバーできると思われます。

FizzBuzzの結果を返すcreateFizzBuzzListメソッドで動作確認をします。

FizzBuzz.js

module.exports = class FizzBuzz {
  static createFizzBuzzList(num) {
    const list = [];
    for (let i = 1; i <= num; i++) {
      if (i % 15 === 0) {
        list.push("FizzBuzz");
      } else if (i % 3 === 0) {
        list.push("Fizz");
      } else if (i % 5 === 0) {
        list.push("Buzz");
      } else {
        list.push(i.toString());
      }
    }
    return list;
  }
};

toBeで等価であることを検証できます。

FizzBuzz.spec.js

test("FizzBuzz test", () => {
  const actual = FizzBuzz.createFizzBuzzList(16);
  expect(actual.length).toBe(16);
  expect(actual[0]).toBe("1");
  expect(actual[1]).toBe("2");
  expect(actual[2]).toBe("Fizz");
  expect(actual[3]).toBe("4");
  expect(actual[4]).toBe("Buzz");
  expect(actual[5]).toBe("Fizz");
  expect(actual[6]).toBe("7");
  expect(actual[7]).toBe("8");
  expect(actual[8]).toBe("Fizz");
  expect(actual[9]).toBe("Buzz");
  expect(actual[10]).toBe("11");
  expect(actual[11]).toBe("Fizz");
  expect(actual[12]).toBe("13");
  expect(actual[13]).toBe("14");
  expect(actual[14]).toBe("FizzBuzz");
  expect(actual[15]).toBe("16");
});

toEqualでオブジェクトや配列の値を検証できます。

FizzBuzz.spec.js

test("FizzBuzz test custom", () => {
  const expected = [
    "1",
    "2",
    "Fizz",
    "4",
    "Buzz",
    "Fizz",
    "7",
    "8",
    "Fizz",
    "Buzz",
    "11",
    "Fizz",
    "13",
    "14",
    "FizzBuzz",
    "16"
  ];
  const actual = FizzBuzz.createFizzBuzzList(16);
  expect(actual.length).toBe(16);
  expect(actual).toEqual(expected);
});

Snapshotテストを使う

実行結果を保存するのに有用なSnapshotテストという機能があります。
Matcherのテストとは違い、期待値となる値を定義する必要がなくなるのでテストコードがシンプルになります。

FizzBuzz.spec.js

test("FizzBuzz snapshot test", () => {
  const actual = FizzBuzz.createFizzBuzzList(16);
  expect(actual).toMatchSnapshot();
});

通常のMatcherとの使い分けとしては、Snapshotテストはプロダクションコードの結果が正しいことが前提です。
正しいと判断するためのテストを書いて、そのテストを通すためのプロダクションコードを書くといったTDDのようなアプローチは取れなくなります。
実行結果を保持することで壊れていないかを確認するのがテストの目的となることもあるため、Snapshotテストは有用な手段だと思います。

Mockを使う

Jestでは自動モック機能というものがあり、簡単にMock作成が可能です。

AccountDao.js

module.exports = class AccountDao {
  findOrNull(userId) {
    return userId;
  }
};

Authentication.js

module.exports = class Authentication {
  getDao() {
    return this.dao;
  }

  setDao(val) {
    this.dao = val;
  }

  authenticate(userId, password) {
    const account = this.dao.findOrNull(userId);
    if (account === null) return null;
    return account.password === password ? account : null;
  }
};

Authentication.spec.js

const Authentication = require("../src/Authentication");
const AccountDao = require("../src/AccountDao");

jest.mock("../src/AccountDao");

describe("Authentication", () => {
  test("not exist account", () => {
    const sut = new Authentication();
    const dao = new AccountDao();
    dao.findOrNull.mockReturnValue(null);
    sut.setDao(dao);
    expect(sut.authenticate("user001", "pw123")).toBeNull();
  });
});

jest.mock関数でモジュールを全てMock化したオブジェクトを作成することができます。
これにより、new AccountDao()で作成したオブジェクトは全てMockになります。
daoオブジェクトのfindOrNullメソッドはMockになり、mockReturnValue関数などのMock用の関数を実行することが可能になります。

まとめ

テストを書くときにMockを使うのが前提となることが多いと思います。個人的には自動でMock化してくれる機能が強力です。
Jestで快適なテストライフを!

参考資料

Jestで書いたテストコード集です
github.com

SIerとWeb系の差分

f:id:hi1280:20190511180125p:plain:w600

現職で働き始めて1ヶ月ほど経過して色々わかってきたので前職との差分についてまとめたいと思います。

前提事項

ここでのSIer(前職)とは大企業に分類される情報サービス業の会社です。受託開発やR&Dなどの業務を行っていました。
大企業 - Wikipedia

ここでのWeb系(現職)とは中小企業に分類されるスタートアップ企業です。ITエンジニアとして働いています。
中小企業 - Wikipedia

スタートアップ企業とは|金融経済用語集

なお、本内容は前職と現職を比べた限られた観測範囲であることを注意書きしておきます。

差分について

主に環境と人についてです。
前職と比べてどうかという観点で記載していきます。

メインの業務であるITの仕事に関してはそこまで変わりがないというのが個人的な感想です。
ツールの違いはありますが、ITでシステムを作るということに変わりはなく、必要とされる考え方や手法はあまり変わらない気がします。

仕事の環境

業務関連

  • コミュニケーションはほぼSlack
    • 前職ではチャットもあったが口頭でやり取りすることも多かった。口頭だとログ追えないし今の方が効果的な気はする
    • ただ話した方が早いと思うこともありちょっとモヤモヤ
  • 進捗管理が緩い
    • 個々人がベストエフォートで取り組んでいる前提であるため予定と実績に関しての進捗状況を細かく言われることがない
    • 前職は事前に決めたスケジュールを遵守することが優先事項だった
  • ドキュメントが整備されていない
    • 初期の段階では動くものを出来るだけ早くリリースするということを経ているためと推察
    • 良くも悪くもコードがドキュメントとなっている
  • 自分から聞かないと何も分からない
    • 教えてもらうという文化がない
    • これまでのやり方についてはSlackのpublicチャンネルで質問している。技術的なことはなるべく自己解決する
    • 前職だと協力会社の人など新しい人が入ってきたときには仕事に必要なことを一通り説明していた。これを見ればわかるみたいなドキュメントも用意していた
  • 開発者は基本的にMacを使う
    • 前職はMacを使っている人もいたが特に理由がなければWindows
  • 大きいディスプレイと良い椅子が完備されている
    • ディスプレイは前職でもあったのであまり変わらないかも

事務関連

  • 社内ルールはあまり面倒なことがなく、シンプル
    • 勤怠くらいしか意識することがない
    • 前職はセキュリティなど過剰に意識することが多かった
  • 勤怠が自由
    • フレックスタイム制なこともあり、遅い時間(10時くらい)に出社するのが普通
    • 前職は9時に始業開始という状況だったため満員電車が辛かった
  • 社内ツールはほぼSaaS
    • 有料のSaaSを活用して面倒ごとをショートカットしている感じ
    • 前職は内製のツールやOSSのツールをオンプレで運用していることが多かった。コストかかってそうだけどマンパワーがあるのでなんとかなっていたのだと思う

全般

  • 個性豊かな人が多い
    • ほぼ中途採用の人たちばかりで、色々なキャリアの人がいるためと推察
    • 前職では新卒で入ってそのままずっと働いているという人が大多数なため仕事ぶりに関して似たり寄ったり
  • 人の出入りが激しい
    • 過去数カ月で新しい人が何人も入っている

バックオフィスの人たち

  • 会社の制度を良くしていくことや広報活動に積極的に取り組んでいる
    • 前職では長年の社内ルールがあり、それをチェックするのが仕事という印象
  • バックオフィスといっても様々な業務があり、専門ではない業務範囲を担当している人もいる
    • 人的リソースが限られているためと推察
    • 前職だときっちり部門が分かれていて専門的にやっていた印象

ビジネスサイドの人たち

  • 事業を軌道に乗せることに注力している
    • 積極的な営業行為や既存業務の効率化など
    • 前職ではそもそもあまり関わりがなく何を思って仕事をしているのかよく分からなかった

※これはどんな会社でもそうと言えばその通りですが新しいビジネスモデルであるがゆえの悩みがあるように思います

技術の人たち

  • ビジネスに関心がある人と技術に関心がある人がハッキリ分かれる
    • 合意形成が難しい。ユーザがすぐそばにいるわけではないということも大きいように思う
    • 前職は良くも悪くも顧客という絶対的な判断基準があり合意形成が行いやすかった
  • 新しいことを取り入れていく意識が強め
    • 新しいことへのハードルが低いので妥当性があれば新しいモジュールなりツールなりを導入できる環境にある。結果的に新しいことに積極的になっていくように思う
    • 前職は顧客ありきだったため新しいことへのハードルは高かった

まとめ

総じて、多様性を楽しめたり、あまりルールや外的環境に縛られたくないという環境を望むのであればWeb系はピッタリです。
新しいことが一概に良いかというと個人的には疑問があり、技術的なスキルアップを望む環境としてはWeb系である必要はない気がしています。
ただIT業界では新しいことに取り組めると待遇が良くなる傾向があり、待遇を求めるとWeb系になるのかなと思います。