MongoDBで、データをjsonからインポートしたい場合、mongoimportを使用するのが一般的だと思います。
ですが、jsonでは数値のTypeは区別されないため、mongoimportをそのまま使用すると、全てがfloating point numberと扱われてしまうようです。
Schema Validationを定義しており、double
以外の数値型を設定している場合、これでは弾かれてしまうので、なんとかする必要があります。
しばらく詰まって辛かったのでメモ書きしておきます。
やり方は色々あるっちゃある
jsonとmongoimportの組み合わせでは、どう頑張っても無理そうなので、回避策を探します。
- CSVにする
- jsでやる
- Schema Validationを無効化して入れて、その後でここのデータを正しいTypeに更新する
- BSONでやる <= これを選択
CSVに変換してから
最初、jsonとmongoimportでなんとかできないかと思い、ドキュメントを読んでいたところ見つけたのが、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とかを使ってやれば、そんなことせずともいけそうです。(未検証、けどこれが一番順当な方法かも)
Schema Validationを無効化
とりあえず入れてしまって、後からTypeを変える方法も考えました。
後から、Bypass Document Validationする方法は色々あるようで、findAndModify
とかが使えれば、サクサク終わりそうと期待しました。
ですが、このfindAndModify
だと、ヒットしたデータのCurrent Valueを、修正時に参照することができなさそうです。
つまり、やるなら1つ1つのデータを取得して、そのデータを元に新しいオブジェクトを$set
するみたいなスクリプを書く必要があります。
くそ
BSONにしてしまえば良い
よくよく考えれば、jsonだと表現できる幅が限られているからimport時にType情報がなくなるわけで、最初からbsonでインポートすれば問題は解決です。
"bson"というnpmライブラリが公式から出てるので、それを使ってjsonをbsonに変換してから、インポートさせれば良いです。
MongoDBにbsonで直接インポートするには、mongoimportではなくmongorestoreを使います。
jsonからbsonへの変換
npm add bson
したら、変換するスクリプトを書きます。もちろんtsで書くので、npm add @types/bson
も忘れずにします。
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にしない
const save = async () => {
await promisify(fs.writeFile)('path/to/file.bson', BSON.serialize([newData, newData2]));
}
最初、このような感じで配列をBSONに書き込んでいました。
しかしその場合、インポートしようとすると1つの配列が1つのデータとして登録されてしまいます。
mongoimportの場合は--jsonArray
オプションがあったので、mongorestoreにも--bsonArray
的なオプションがあるのかと思ったのですが、ないです。
この解決方法を探して小一時間さまよっていましたが、結論は「そもそもArrayにする必要がない」です。
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
であれば使用できます。
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
疲れた 😇