MongoDB: jsonの数値データをIntやDecimal128でインポートする

MongoDBで、データをjsonからインポートしたい場合、[mongoimport](https://docs.mongodb.com/manual/reference/program/mongoimport/)を使用するのが一般的だと思います。

ですが、jsonでは数値のTypeは区別されないため、mongoimportをそのまま使用すると、全てがfloating point numberと扱われてしまうようです。

[Schema Validation](https://docs.mongodb.com/manual/core/schema-validation/)を定義しており、`double`以外の数値型を設定している場合、これでは弾かれてしまうので、なんとかする必要があります。

しばらく詰まって辛かったのでメモ書きしておきます。

## やり方は色々あるっちゃある

jsonとmongoimportの組み合わせでは、どう頑張っても無理そうなので、回避策を探します。

1. CSVにする
2. jsでやる
3. Schema Validationを無効化して入れて、その後でここのデータを正しいTypeに更新する
4. BSONでやる <= これを選択

### CSVに変換してから

最初、jsonとmongoimportでなんとかできないかと思い、ドキュメントを読んでいたところ見つけたのが、[`columnsHaveTypes`](https://docs.mongodb.com/manual/reference/program/mongoimport/#cmdoption--columnsHaveTypes)オプションです。

これはどうやら、フィールド毎にTypeを指定できるもののようですが、残念ながらCSVとTSVからのインポートにのみ対応しているオプションのようで、ご丁寧に「json噛ませるとエラーになるからな」と注意書きがなされています。

まぁなら、何かしらの方法でjsonをCSVに変換してからインポートすればいけそうです。(未検証)

### jsでスクリプト書く

Mongo Shellはjsを呼べるので、スクリプトを書くのも手です。

ですが、どうやらMongoShellでjsonファイルを直接読み込むのは無理で、jsファイルに変換する必要があるようです。

ref: https://stackoverflow.com/questions/27670996/import-json-with-mongo-script

ちょっと流石にめんどいし何より気持ち悪いのでやめました。

まぁMongoShellを使わず、どっか適当なところにnpmの環境を作って、[MongoDBのNode.js Driver](https://mongodb.github.io/node-mongodb-native/)とかを使ってやれば、そんなことせずともいけそうです。(未検証、けどこれが一番順当な方法かも)

### Schema Validationを無効化

とりあえず入れてしまって、後からTypeを変える方法も考えました。

後から、[Bypass Document Validation](https://docs.mongodb.com/manual/core/schema-validation/#bypass-document-validation)する方法は色々あるようで、[`findAndModify`](https://docs.mongodb.com/manual/reference/command/findAndModify/)とかが使えれば、サクサク終わりそうと期待しました。

ですが、この`findAndModify`だと、ヒットしたデータのCurrent Valueを、修正時に参照することができなさそうです。

ref: https://stackoverflow.com/questions/3788256/mongodb-updating-documents-using-data-from-the-same-document

つまり、やるなら1つ1つのデータを取得して、そのデータを元に新しいオブジェクトを[`$set`する](https://docs.mongodb.com/manual/reference/operator/update/)みたいなスクリプを書く必要があります。

くそ

## BSONにしてしまえば良い

よくよく考えれば、jsonだと表現できる幅が限られているからimport時にType情報がなくなるわけで、最初からbsonでインポートすれば問題は解決です。

["bson"](https://github.com/mongodb/js-bson)というnpmライブラリが公式から出てるので、それを使ってjsonをbsonに変換してから、インポートさせれば良いです。

MongoDBにbsonで直接インポートするには、mongoimportではなく[mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/)を使います。

### jsonからbsonへの変換

`npm add bson`したら、変換するスクリプトを書きます。もちろんtsで書くので、`npm add @types/bson`も忘れずにします。

```ts
import fs from 'fs';
import { promisify } from 'util';
import BSON from 'bson';

const Int32 = BSON.Int32;
const Double = BSON.Double;

type NewData = {
  code: BSON.Int32;
  price: BSON.Double;
}

const newData: NewData = {
  code: new Int32(9064),
  price: new Double(2500.05)
}

const newData2: NewData = {
  code: new Int32(9984),
  price: new Double(5200)
}

const save = async () => {
  await promisify(fs.writeFile)('path/to/file.bson', BSON.serialize(newData));
  await promisify(fs.appendFile)('path/to/file.bson', BSON.serialize(newData2));
}

save();
```

中身はこんな感じです。(あくまで参考例レベルですので悪しからず)
ファイルに書き込むときに`BSON.serialize()`することで、bsonとして正しく保存できます。

### 複数データを入れる場合の注意：Arrayにしない

```ts
const save = async () => {
  await promisify(fs.writeFile)('path/to/file.bson', BSON.serialize([newData, newData2]));
}
```

最初、このような感じで配列をBSONに書き込んでいました。

しかしその場合、インポートしようとすると1つの配列が1つのデータとして登録されてしまいます。

mongoimportの場合は[`--jsonArray`](https://docs.mongodb.com/manual/reference/program/mongoimport/#cmdoption-mongoimport-jsonarray)オプションがあったので、mongorestoreにも`--bsonArray`的なオプションがあるのかと思ったのですが、ないです。

この解決方法を探して小一時間さまよっていましたが、結論は「そもそもArrayにする必要がない」です。

ref: https://stackoverflow.com/questions/56585231/how-to-deserialize-dumped-bson-with-arbitrarily-many-documents-in-javascript

> Which clearly breaks the JSON syntax. The internal BSON is of similar shape, but it seems BSON allows this kind of multi-object stuffing in one file.

これを知るために結構時間を無駄にした。。。。😭

Arrayにせず、直接`appendFile`で追記していきましょう。

### 保存はmongoimportではなくmongorestoreで

dumpしたデータの実態はただのbsonファイルなので、`mongorestore`は`mongodump`で出力したデータでなくとも、`.bson`であれば使用できます。

```restore.sh
DB_NAME=$1
COLLECTION_NAME=$2
FILE_PATH=$3

// authはあくまで例
mongorestore \
  --authenticationDatabase admin -u root -p root \ 
  -d $DB_NAME -c $COLLECTION_NAME $FILE_PATH
```

疲れた 😇