JS初心者がMongodbデータベースを使って、じっくり非同期処理(callback, async)を勉強し直してみた

callback編

今回は、コールバック編ですので、次記事が async 編になります。

  • 初心者にとって、非同期処理はやっかいなものの一つです。
  • 以前に、Promiseの記事 で非同期について書いたのですが、疑似的なものではなく、実際の実装に近い形で学べたらいいなと思い、データベースの一つであるmonogodbを選んで見ました。
  • なぜ一般的なデータベースに使われているMySQLではないかというと、処理にJSの文法が使えるから、JSを勉強している人には好都合のデータベースなのです。
  • ここでは、mongodb を Nodejs を使って操作するところを扱います。 Nodejsで操作するとき非同期処理が必要となりますがコールバック関数とasync関数を使い比べてみるのが今回の趣旨になります。

  • 今回、nodejs, mongodb は既に入っている前提です. まだの方は、Mac nodejs インストール, windows nodejs等で記事を探すと良記事が見つかるので、ここでは特に説明しません。

  • 私の環境です。
  • node --version v10.9.0
    • async をつかいますので、version 10以上を使ってください
  • mongo --help MongoDB shell version v4.0.4

    • サーバーであるmongodも version v4.0.4で同じです。
  • 環境を作っていきます

    • mongodb サーバーを立ち上げます ターミナルでmongod と打ちます。
    • オプションを指定しない場合、default設定になりますので、実際データ読み書きでエラーになることがありますので、その場合はsudo付きでmongod を立ち上げてください。
mongod
sudo mongod
  • もう一つターミナルを立ち上げて、mongoクライアントである mongo shellで動作しているか確認します。
mongo --version
  • ヴェージョン表記がされていれば無事, mongodbが正常動作しています。
  • 今回は、mongo shell の操作方法は説明しません、ドットインストールさんなどにmongoクライアントでの操作方法が詳しく解説されているので、少しバーションが古いようですが、今のversion4系でも特に支障がないので、見てみるといいかもしれません。

  • mongodb側が終わったので、nodejs 側に移ります。

    • 適当に練習用のディレクトリを作ります。
    • yarn が入っていない方は、 npm -i -g yarn です。
    • ここでいれるmongodb は nodejs用のドライバーです。
    • git 関係は、練習になくてもいいですが、習慣的に入れた方がいいです。
    • gibo が入っていない場合は、 Mac の方は、 brew install gibo です。 windowsの方は、検索して入れてみてください。
    • お好きなエディターを使ってください。
      • vim appCallback.js
      • code appCallback.js
      • atom appCallback.js
mkdir myapp && cd myapp
yarn init -y
yarn add mongodb
git init
gibo node > .gitignore
vim appCallback.js
  • 現在のpackage.jsonの中身です。

    • くどいようですが、nodejsのmongodbドライバーはversion3系が入っていますが、これは mongodbサーバー(mongod)とmongodbクライアント(mongo shell)には直接関係ありません。 私はごっちゃに考えてしまい余計な手間がかかりました。mongodbサーバーとのやり取りに使うので、サーバーとは間接的に関係がありますが。
  • package.json

{
  "name": "myapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "mongodb": "^3.1.10"
  }
}
  • appCallback.js
const MongoClient = require('mongodb').MongoClient
const assert = require('assert')

const url = 'mongodb://localhost:27017/myproject'
const dbName = 'myproject'
const client = new MongoClient(url)

client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)
    client.close()
})
  • ターミナルから実行します
node appCallback.js
  • Connected successfully to server と出ていればOKです。

    • ctrl + c で終わらせます
  • データをmongodbに入れてみます。

  • assert は、動作チエックようです。 期待通りの動作ではなかった場合にエラーになります。
  • insertMany は、コレクションメソッドといわれているものですが、ここでは説明しません。詳しくは公式ガイドやドットインストールさん等で確認ください。
const MongoClient = require('mongodb').MongoClient
const assert = require('assert')

const url = 'mongodb://localhost:27017/myproject'
const dbName = 'myproject'
const client = new MongoClient(url)


const insertDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection.insertMany([
        { a: 1 },
        { a: 2 },
        { a: 3 },
    ], (err, result) => {
        assert.equal(err, null)
        assert.equal(3, result.result.n)
        assert.equal(3, result.ops.length)
        console.log("Insered  documents")
        callback(result)
    })
}

client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)

    insertDocuments(db, () => {
        client.close()
    })
})

* ターミナルから再度実行します。

node appCallback.js

Connected successfully to server
Insered  documents
  • タイポミス等がなければ、エラーにならずに上記の表示が出るかと思います。
  • 実際に入っているかどうか、すぐに確かめたい方は、mongo shellで確認してみてください。
  • データの作成に、別関数を作りそれを、client.connect で使っています。関数を作らずに client.connectに insertManyメソッドつかっても構いませんが、メインである client.connect はなるべくきれいに保ちたいのであれば、別関数にしたほうがいいです。
  • ここで、この記事のテーマである、callback が使われています。
  • callback と書いてあるので、間違いなくコールバックです。(笑)

  • なぜ、コールバックが使われているかというと、ここでは client.close() をいれるためです。

  • これは、使用したデータベースを終了するコマンドです。
  • これを、外に出した状態にすると、データの書き込みが終了する前に、グローズされる可能性が大です。
client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)

    insertDocuments(db, () => {

    })
    client.close()
})
  • 実際試してみたら、問題なく書き込まれてしまいました...。
  • もう少し負荷を掛けるために、データ表示処理を追加します。
    • 以下の記述はエラーになります
const MongoClient = require('mongodb').MongoClient
const assert = require('assert')

const url = 'mongodb://localhost:27017/myproject'
const dbName = 'myproject'
const client = new MongoClient(url)


const findDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection
        .find({})
        .project({ '_id': 0})
        .toArray((err, docs) => {
            assert.equal(err, null)
            console.log("Founds the records")
            callback(docs)
        })
}

const insertDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection.insertMany([
        { a: 1 },
        { a: 2 },
        { a: 3 },
    ], (err, result) => {
        assert.equal(err, null)
        assert.equal(3, result.result.n)
        assert.equal(3, result.ops.length)
        console.log("Insered  documents")
        callback(result)
    })
}

client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)

    insertDocuments(db, () => {
        findDocuments(db, (docs) => {
            console.log(docs)
        })
    })

    client.close()

})
  • 見やすくするために、エラー内容をいじってありますが、インサートは成功していますが、find関数でアサーションエラーが出ているようです。
  • 無事エラーが出ることが、わかったので、クローズ処理を中に入れます。
  • find関数もinsert関数の中に入れているのは、確実にインサートが終わってから表示させたいからです。 外に出しても、特にエラーにもなりませんが、insert処理が終わったデータかどうかは、不確かになります。
Connected successfully to server
Insered  documents
AssertionError [ERR_ASSERTION]: { MongoError: Topology was destroyed
  • 以下の記述で、正常にデータの作成と表示ができるはずです。
const MongoClient = require('mongodb').MongoClient
const assert = require('assert')

const url = 'mongodb://localhost:27017/myproject'
const dbName = 'myproject'
const client = new MongoClient(url)


const findDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection
        .find({})
        .project({ '_id': 0})
        .toArray((err, docs) => {
            assert.equal(err, null)
            console.log("Founds the records")
            callback(docs)
        })
}

const insertDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection.insertMany([
        { a: 1 },
        { a: 2 },
        { a: 3 },
    ], (err, result) => {
        assert.equal(err, null)
        assert.equal(3, result.result.n)
        assert.equal(3, result.ops.length)
        console.log("Insered  documents")
        callback(result)
    })
}

client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)

    insertDocuments(db, () => {
        findDocuments(db, (docs) => {
            console.log(docs)
            client.close()
        })
    })
})
  • 配列で表示データが取得されていることがわかります。 配列なのでJSが得意な様々なループ処理が使えることを意味します。

  • データの更新もしてみたいと思いますが、データが増える一方なので、先に一旦データを削除してから更新処理を入れたいと思います。

const MongoClient = require('mongodb').MongoClient
const assert = require('assert')

const url = 'mongodb://localhost:27017/myproject'
const dbName = 'myproject'
const client = new MongoClient(url)


const updateOneDocument = (db, callback) => {
    const collection = db.collection('documents')
    collection.updateOne(
        { a: 1 },
        { $set: { a: 100 }}, (err, result) => {
            assert.equal(err, null)
            console.log("update the records")
            callback(result)
        })
}

const deleteManyDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection.deleteMany({}, (err, result) => {
        assert.equal(err, null)
        console.log("Delete All")
        callback(result)
    })
}

const findDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection
        .find({})
        .project({ '_id': 0})
        .toArray((err, docs) => {
            assert.equal(err, null)
            console.log("Founds the records")
            callback(docs)
        })
}

const insertDocuments = (db, callback) => {
    const collection = db.collection('documents')
    collection.insertMany([
        { a: 1 },
        { a: 2 },
        { a: 3 },
    ], (err, result) => {
        assert.equal(err, null)
        assert.equal(3, result.result.n)
        assert.equal(3, result.ops.length)
        console.log("Insered  documents")
        callback(result)
    })
}

client.connect(err => {
    assert.equal(null, err)
    console.log("Connected successfully to server")
    const db = client.db(dbName)

    deleteManyDocuments(db, () => {
        insertDocuments(db, () => {
            updateOneDocument(db, () => {
                findDocuments(db, (docs) => {
                    console.log(docs)
                    client.close()
                })
            })
        })
    })
})
  • コレクションメソッドは、もっとたくさんありますので、ガイドを参照してみてください。
  • 数珠つなぎに関数を繋げていけば、かなり複雑な処理が出来ることがわかると思いますが、ひ孫以下の処理はできたらしたくないものです。

  • 今回は、引数の処理は, docsしかありませんが、実際の処理では、各々引数があるばあいが多いかと思います。

  • あと、callback ですが初心者の内は概念的にわかりにくいのですが、return の代わりと思うと感覚が掴みやすいのかと思います。
  • 関数処理の基本は、もらったデータを処理して、また相手に返すことですので、関数の中で処理が完結してしまう特別な場合を除き結果を返すことです。

  • しかし、今回は、処理した結果をreturn で返していては、結果を返したときには、クローズ処理が先に実行されたあとになる可能性が大です。 データベースから結果をもらうのはそれなりにコスト(時間)がかかるのです。

  • そのため callback という仮引数を用意して、関数を配置するスペースを提供しているのです。 実際の引数はアロー関数なので、仮引数であるcallback は実際のところ next でも、 cb でもなんでもいいのです。

  • 長くなったので、次の記事で async編を書きたいと思います。