VaporアプリケーションからMongoDBにデータを書き込んだり読み出したりしてみる

MongoDB
VaporアプリケーションからMongoDBにデータを書き込んだり読み出したりしてみる

はじめに

VaporアプリケーションからMongoDBまでつながるところまでたどり着きました。と言っても、エラーが出ないから上手くできているだろう程度の判断ですので、APIを発行しながら調整していくことになりそうです。

始める前に謝っておきます。FluentSQLite用に作ったRouterをMongoDB用に書き換えていけばいいやと思っていました。ただそうは上手く物事は運ばないものです。
別のVapor3アプリケーションを作って、FluentSQLite用に作ったものと比べつつ悩んで進めていく方針で進めていきます。

ドキュメントの追加

まずはなにはともあれドキュメントを追加してみます。できるだけFluentSQLite用のに少し手を加えるぐらいで対応させていきたい!

これがFluentSQLite用に作ったものです。

router.post(Book.self, at: "api/book") { (req, book) -> Future<Book> in
    return book.save(on: req)
}

これをMongoDB用にしてみます。結構変わりますね。FluentSQLite先輩の偉大さが分かります。

router.post(Book.self, at: "api/book") { (req, book) -> Future<Book> in
    let client = try! req.make(MongoClient.self)
    let collection = client.db("vaporapp").collection("books", withType: Book.self)
    return try req.content.decode(Book.self).map(to: Book.self) { book in
        _ = try! collection.insertOne(book)
        return book
    }
}

最初の2行はMongoDBのコレクションにアクセスするためのものです。Vapor3アプリケーションが起動するときにConfigureでMongoDBへのアクセスを確保しています(と思います)。アクセスルートを確保して、どのコレクションにつなぐかを指定しています。コード上そのようなイメージです。

残るは、map初心者の私めには若干ハードルの高めの3行目になります。
データはPostmanを使ってJSONで送るので、リクエストに含まれるBookコンテンツをmapでばらしています。ばらしたものを順次MongoDBに投入していっています。returnを返さないといけないので、ばらしたコンテンツ(book)を返しています。説明になってねえな。

ここではinsertOneメソッドを使いましたが、insertManyメソッドもあります。複数のドキュメントを一気に追加できます。Postするときにデータを1件だけJSONで渡していますが、複数件のデータをまとめて送ることができそうですね。

ドキュメントの取得

続きまして、ドキュメントの取得です。まずはFluentSQLite版を。FluentSQLite先輩、フィルタからのソートと、チェーンでキメて自由自在です。

router.get("api/books",String.parameter) { req -> Future<[Book]> in
    let publisher = try req.parameters.next(String.self).lowercased()
    return Book.query(on: req).filter(\.publisher, ._like, "%" + publisher + "%").sort(\.title, .descending).all()
}

翻ってMongoDB。

router.get("api/books",String.parameter) { req -> [Book] in
    let client = try! req.make(MongoClient.self)
    let collection = client.db("vaporapp").collection("books", withType: Book.self)
    let title = try req.parameters.next(String.self).lowercased()
    let query:Document = try Document.init(fromJSON: "{\"title\":\"" + title + "\" }")
    return try! collection.find(query, options: nil, session: nil).dropLast(0)
}

冒頭2行は同じですので割愛。

まず、パラメータを取得します。これは理解できます。続いて検索条件クエリを作るのですが、なんとも回りくどい感じがします。JSONをバイナリにしたBSONにする必要があるのですが、BSON型ではなく、なぜかDocument型。素直にJSONでクエリを渡せたら良いですよね。

シメはドキュメントの検索です。今回は諸々を端折っているので2つ目3つ目の引数にnilを入れています。findメソッドの引数1つ目にクエリを入れると絞り込み検索を行った後のドキュメント一式のカーソルを返してくれます。ここから絞り込んだドキュメント全部をもらいたいのですが、ここでdropLast(0)ですよ。

パッケージのバージョンが0.3.0だとtoArrayメソッドが使えないので、

− ループを回して配列に入れていく
− 末尾のドキュメントゼロ個を削除して配列を返してもらう

の2択になろうかと思います。ループを回すほうが王道だと思いますが、いかんせんコードが長くなるので、その辺りをサボるという意味合いも含めて、邪道のdropLast(0)を選択しました。

私が使いこなせてないだけですが、なんだろうこのもっさり感。もしかしたら、パッケージのバージョンを1.0.0-rc1にするとしっくり来るのかもしれないです。それと、個人的な件ですが、どうも「カーソル」のイメージがつかみにくい……

課題

Vaporアプリケーション起動時のフロー

ドキュメントを追加したり取得したりするときに、息をするかの如く自然とreq.make(MongoClient.self)を実行しています。なぜこれでMongoDBクライアントが取得できるのか?

これを解決するためには、Vaporのライフサイクル的なことを理解する必要がありそうです。サーバ側とローカル側の間でどのようなやり取りが行われているかも押さえておかねば!

あいまい検索でドキュメントの取得ってできないのかな?

できるはずなんですよ。だって、MongoDBにクエリを発行するに当たってあいまい検索ができるのですもの。ざっくりですが、部分一致のときは検索したい文言の両端に/をつけておけば良いのです。

ところが、それに倣って

let query:Document = try Document.init(fromJSON: "{\"title\":/\"" + title + "\"/ }")

とすると、「クエリが何かおかしいよ?」と言われます。確かに違和感はありますけども… なんでや……?

「なにかおかしいよ?」

まとめ

MongoDBへデータの追加を行い、それを取得できました。ひとまずは満足です。
ただ、なんともしっくりこない。Vaporの仕組みやmap/flatMap、Futureを理解すればもっと素直な書き方ができるかもしれません。

猪突猛進で好きなところから取り組んでいるので、物事の順序がちぐはぐになっているところがあります。なので、間違いや勘違いがあるかもしれないですが、随時軌道修正しながら落ちついて進めていこうと思います。