BSONとMapオブジェクト

前回の続きです。
今回は Map オブジェクトとの関係を少し調べてみます。

Map

こういうの

newMap.ts
const foo = new Map([
  ['bar', 'baz'],
  ['hoge', 'fuga'],
])
console.log(foo.get('hoge')) // fuga

JSON と Map

JSON は Map を許容しないので、そのままでは入らない。

newMap.ts
const foo = new Map([
  ['bar', 'baz'],
  ['hoge', 'fuga'],
])

console.log(
  JSON.stringify({
    mapObject: foo,
  }),
)
// {"mapObject":{}}

ただ、Map は iterable なので、Array.from()やスプレッド構文で配列にするやり方がある。
(ネストされた Map は Map のままなので入りません、注意)

newMap.ts
const foo = new Map([
  ['bar', 'baz'],
  ['hoge', 'fuga'],
])


console.log(
  JSON.stringify({
    mapObject: Array.from(foo),
  }),
)

// {"mapObject":[["bar","baz"],["hoge","fuga"]]}

BSON と Map

BSON ではどうなってしまうの。
まず、MongoDB Node.js Driver の挙動を確認。

mongo.ts
import mongodb from 'mongodb'

const MongoClient = mongodb.MongoClient
const URL = 'mongodb://127.0.0.1:27017/myDB'
const DB_NAME = 'mydb'
const COLLECTION_NAME = 'foo'

const client = new MongoClient(URL, {
  useNewUrlParser: true,
})

const connect = async () => {
  try {
    await client.connect()

    const collection = client.db(DB_NAME).collection(COLLECTION_NAME)

    await collection.insertOne({
      mapObject: new Map([['bar', 'baz']]),
    })
    // { _id: 5e9bc479294b2ea5713b8ac8, mapObject: { bar: 'baz' } }

    const itemList = await collection.find().toArray()

    console.log(itemList)
  } catch (error) {
    console.log(error.stack)
  }

  client.close()
}

connect()

無慈悲にも object に変換される。
MongoDB Node.js Driver が使用する BSON parserMap.prototype.entries()を使って Iterate し、シリアライズした値を一つずつ入れるのでこうなってしまう。

ついでに、Map は色々な値を key にできるので、やってみた。

mongo.ts
// 略
    await collection.insertOne({
      mapObject: new Map([
        [undefined, 'baz'],
        [NaN, 'fuga'],
      ]),
    })
// 略

TypeError: argument must be a string が出て Insert に失敗した。
BSON parser が文字列以外の key をbuf.write()に渡してしまうので、エラー。

困ること

Map オブジェクトではなくなるので、クライアントが Map を知りすぎているとつらい。
例えば、以下のgetPersonListMap.prototype.entries()を使用している。

person.ts
export type PersonList = Map<string, number>

export const getPersonList = (personList: PersonList) => {
  const array = []
  for (const [key, value] of personList.entries()) {
    array.push({ key, value })
  }
  return array
}

次に、personList を持った hoge コミュニティのデータを insert する。
この時点ではgetPersonList(personList)の結果は問題なく表示される。

mongo.ts
const personList: PersonList = new Map()

personList.set('taro', 18)
personList.set('jiro', 26)
personList.set('goro', 56)

console.log(getPersonList(personList))
/*
[
  { name: 'taro', age: 18 },
  { name: 'jiro', age: 26 },
  { name: 'goro', age: 56 }
]
*/

await collection.insertOne({ community: 'hoge', personList })

ところが、DB には object が入るので、getPersonList(personList)の中でresult.personList.entries()という存在しない関数を実行しようとして、エラーが発生する。

mongo.ts
const result = await collection.findOne({ community: 'hoge' })

console.log(getPersonList(result.personList)) // result.personList.entries() がない

ちょっと辛いけどArray.fromなどを使って、配列で保存すれば対処はできる。
取り出す時にはnew Map()する。

mongo.ts
// 略
    await collection.insertOne({
      community: 'hoge',
      personList: Array.from(personList), // Array.from
    })

    const result = await collection.findOne({ community: 'hoge' })

    console.log(getPersonList(new Map(result.personList)))
    /*
    [
      { name: 'taro', age: 18 },
      { name: 'jiro', age: 26 },
      { name: 'goro', age: 56 }
    ]
    */
// 略

これで一応の挿入順は保証される。
(ネストされた Map は Map のままなので object になります、注意)

終わり

Map 難しい