Azure Cosmos で無料で作るハイスコアDB(1)


競争本能とハイスコア

プログラム学習用ピコピコゲーム砂漠のひつじを公開している。

このゲームは学習用のピコピコゲームであるため、ゲーム自体は非常にシンプルであり、すぐに飽きてしまう。公開当初は気にならなかったが、せっかく作ったのだからリテンションが上がった方がいいと思い始めた。

そこで、人間が本来持つ競争本能に訴えかけることでリテンションを改善しつつ、NoSQLデータベースへのデータ保存を学習できるように、ハイスコアを保存する処理を作成することにした。

DBの無料枠

なぜNoSQLを選択したかと言うと、無料枠があるからだ。以下のサービスが候補になると思う。

  • Amazon DynamoDB

    AWS 無料利用枠は、お客様が AWS のサービスを無料で実際に体験できるように設定されています。AWS 無料利用枠の一部として、DynamoDB では以下の特典がご利用いただけます。各特典は、各リージョンで支払人アカウントごとに月単位で計算されます。

    25 GB のデータストレージ
    DynamoDB ストリームからのストリーム読み込みリクエスト 250 万回
    AWS のサービス全体での合計データ転送 (アウト) 1 GB

  • Azure Cosmos DB

    新しいアカウントで Free レベルを有効にすると、お客様のアカウントの有効期間中、毎月 400 RU/秒のプロビジョニング スループットと 5 GB のストレージを無料で利用することができます。

  • Firebase Realtime Database

    同時接続 100
    GB 保存済み 1 GB
    GB ダウンロード済み 10 GB/月

ハイスコアデータは非常に小さいデータであるためどれを選んでもよかったが、個人的に興味のあったAzure Cosmos DB を利用することにした。

アカウントとサブスクリプションとリソースグループ

AWSを使っててAzureを使う時に結構違うなと思うのが「サブスクリプション」と「リソースグループ」という考え方。AWSとは大きく異る考え方と感じるが、結構整理されていて多分便利なので覚えておきたい。

  • アカウント
    Microsoft Azure Portalより作成できるアカウント。

  • サブスクリプション
    何かの契約と感じるかもしれないが請求の単位を指している。会社でアカウントを作成した時に、部署やプロジェクトなど任意の単位で「サブスクリプション」を作成することにより容易に会計処理ができる模様。

無料アカウントでは、「無料試用版」サブスクリプションを利用することになる。

  • リソースグループ Azureで提供される全てのサービスはリソースと呼ばれ、それらを管理する単位として「リソースグループ」を定義できる。

CosmosDBの作成

Azure Portal のホーム画面より CosmosDBを選択

CosmosDBアカウントの作成画面で作成を行う。

作成時メモ:
- 初回作成時はリソースグループを作成していないが、本画面よりリソースグループを作成可能。
- アカウント名はサブドメイン名として利用される名称であるので、世界で一意になる名称を指定する。

  • APIは「コア(SQL)」を選択するとクイックスタートよりダウンロードできるソースが分かりやすいものになるので初回は「コア(SQL)」を選択するとよさそう。

  • 一つのサブスクリプションに無料枠を含むCosmosDBアカウントを複数作れない

  • アカウントの作成は5分程度かかる。辛抱強く待とう。

クイックスタート

Node.js のプロジェクトのzipをダウンロードする。
解凍すると、データベースの作成、データ作成/更新/削除が含まれるサンプルコードが含まれており、以下で実行できる。

npm install
npm start

npm start した後に、Azure Portal のデータエクスプローラーを確認すると、サンプルコードに含まれるデータが登録されているのが確認できる。適当にソースをいじって登録データを変更してみると理解が深まる。

以下は初期ハイスコア登録用に修正したコード(途中)

app.js
createDatabase()
  .then(() => readDatabase())
  .then(() => createContainer())
  .then(() => readContainer())
  .then(() => scaleContainer())
  .then(() => {
    const creates = config.hiScores.map(v => createItem(v))
    return Promise.all(creates)
  })
  .then(() => queryContainer())
  .then(() => {
    exit(`Completed successfully`)
  })
  .catch(error => {
    exit(`Completed with error ${JSON.stringify(error)}`)
  })
config.js
config.hiScores = [
  {
    id: '1',  // Id must be a string.
    name: 'SHEEP',
    score: 1000
  },
  {
    id: '2',  // Id must be a string.
    name: 'SHEEP',
    score: 900
  },
  {
    id: '3',  // Id must be a string.
    name: 'SHEEP',
    score: 800
  },
  {
    id: '4',  // Id must be a string.
    name: 'SHEEP',
    score: 700
  },
  {
    id: '5',  // Id must be a string.
    name: 'SHEEP',
    score: 600
  },
  {
    id: '6',  // Id must be a string.
    name: 'SHEEP',
    score: 500
  },
  {
    id: '7',  // Id must be a string.
    name: 'SHEEP',
    score: 400
  },
  {
    id: '8',  // Id must be a string.
    name: 'SHEEP',
    score: 300
  },
  {
    id: '9',  // Id must be a string.
    name: 'SHEEP',
    score: 200
  },
   {
    id: '10', // Id must be a string.
    name: 'SHEEP',
    score: 100
  },
]

NoSQL なのに、SELECT文で検索できるのは便利そうだった。

app.js
// サンプルコードより
/**
 * Query the container using SQL
 */
async function queryContainer() {
  console.log(`Querying container:\n${config.container.id}`)

  // query to return all children in a family
  // Including the partition key value of lastName in the WHERE filter results in a more efficient query
  const querySpec = {
    query: 'SELECT VALUE r.children FROM root r WHERE r.lastName = @lastName',
    parameters: [
      {
        name: '@lastName',
        value: 'Andersen'
      }
    ]
  }

  const { resources: results } = await client
    .database(databaseId)
    .container(containerId)
    .items.query(querySpec)
    .fetchAll()
  for (var queryResult of results) {
    let resultString = JSON.stringify(queryResult)
    console.log(`\tQuery returned ${resultString}\n`)
  }
}

Azure テーブル

ここまで作成して、autoincrement できなくてスコア登録できないことに気づく。
他にもテーブルをいくつか持ちたくなることを考えると「コア(SQL)」ではなく「Azure テーブル」でCosmoDBアカウントを作り直した方がよさそうに思えた。APIはシンプルだったが、テーブル作成時点で課金が確定してしまう?

Microsoft Azure Storage SDK for Node.js and JavaScript for Browsers#Table Storage

const storage = require('azure-storage')
const config = require('./.env.js')
const client = storage.createTableService(config.cosmos)
const eg = storage.TableUtilities.entityGenerator

// table 作成
client.createTableIfNotExists(table, (error, result, response) => {
  if(!error) {
    console.log(result)
  }
})

// entry 作成例
async function initSequence() {
  console.group('init sequence')
  const entry = {
    PartitionKey: eg.String('sequences'),
    RowKey: eg.String('hiScores'),
    no: eg.Int64(1) 
  }
  await client.insertOrReplaceEntity('sequences', entry, (error, result, response) => {
    if(!error) {
      console.log(result)
    }
  })
  console.groupEnd()
}

// entry 取得例
function retrieveSequence(key) {
  client.retrieveEntity('sequences', 'sequences', key, (error, result, response) => {
    if(!error) {
      console.log(result)
    }
  })
}

おわりに

Webpush 通知も作る予定なので Firebase に寄せた方が簡単かと思ったが、Azure を触っておきたかったので Cosmos DB を利用した。
アカウント作成にクレカが必要だったり微妙なハードルはあったが、サンプルコードがダウンロードできたため思ったより簡単に確認までできたが、複雑なデータを格納したい場合には検討が必要そうだった。

参考:
2020年から始める Azure Cosmos DB - 環境構築