VaporアプリケーションからMongoDBのデータを更新したり削除したりしてみる

MongoDB
VaporアプリケーションからMongoDBのデータを更新したり削除したりしてみる

はじめに

VaporアプリケーションからMongoDBへデータを追加したり取得したりすることができるようになりました。かなり勢い任せで危なっかしいですがね。今回はそれらのデータを更新してみたり削除してみたり。ドドドッと進んでいきます。
今回も別のVapor3アプリケーションを作って、FluentSQLite用に作ったものと比べつつ進めていく方針で進めていきます。

ドキュメントの更新

FluentSQLiteの場合

router.post("api/book",Book.parameter) { req -> Future<Book> in
    return try req.parameters.next(Book.self).flatMap { book in
        var renewBook = Book(title: book.title, publisher: "ヌッコ出版", price: 900)
        renewBook.id = book.id
        return renewBook.update(on: req)
    }
}

FluentSQLiteの場合はrouter.post("api/book",Book.parameter) {~}としておくだけで、http://localhost/api/book/3とするとIDが3のレコードを特定してくれます。あとはそのIDをキーとして中身を更新していきます。

よくあるRDBのアプローチですね。SQLでUpdate文を発行するのと同じ理屈です。

MongoDBの場合

router.post(Book.self, at:"api/book") { (req, book) -> Future<HTTPStatus> 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: HTTPStatus.self) { book in
        let query: Document = try Document.init(fromJSON: "{\"title\":\"" + book.title + "\"}")
        let updatedBook: Document = try Document.init(fromJSON: "{\"$set\": {\"price\":" + String(Double(book.price) * 1.1) + "}}")
        _ = try! collection.updateOne(filter: query, update: updatedBook)
        return .ok
    }
}
更新前
Nukkoの本2のお値段が「720」です。
POSTで送ったJSON
お値段「800」
更新後、Nukkoの本2のお値段が「800」の1.1倍が入っています。
端数が出ていますが、本筋の話ではないので今は無視します。

やっていることは単純だけれども複雑に見えてしまう……
ひとまず更新処理はできますが、ツッコミどころが少々。

  • 今更ですが、プロトコルはpostかputのどちらが適切なのか。
  • queryで複数のドキュメントが対象となった時どうなのか。

queryで必ず1件に絞り込めるようにするか、updateManyメソッドで複数一気に更新するかの対応をするのが良いかもしれません。

それと、特筆すべき点はHTTPStatusです。FluentSQLiteでは更新したデータを表示しましたが、MongoDBでは更新できたらHTTPステータスの.okを返すようにしてみました。いわゆる200 OKというやつです。

詰めの甘さは否めません。HTTPステータスを返すようにするなら、更新失敗の時などは分岐して適切なステータスを返すべきです。HTTPステータス周りは不慣れな領域なので掘り下げてみたい。

ドキュメントの削除

FluentSQLiteの場合

router.delete("api/book",Book.parameter) { req -> Future<Book> in
    return try req.parameters.next(Book.self).delete(on: req)
}

更新の時と同様、http://localhost/api/book/3とするとIDが3のレコードを特定してくれます。そのIDをキーとしてレコードを削除しています。

これまたよくあるRDBのアプローチ。SQLiteの場合は1ファイル1データベースになっていることが多いので、SQLで特定のファイル(データベース)と主キー値を指定してDelete文を発行するイメージです。

MongoDBの場合

router.delete("api/book",String.parameter) { req -> HTTPStatus 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 + "\" }")
    _ = try! collection.deleteOne(query)
    return .ok
}
削除前の状態
「nukko」が消えています

今回はパラメータを渡して削除対象のドキュメントを指定してみました。「nukko」です。ドキュメントがちゃんと削除されています。見た通りのあっさりした処理です。こう見るとFluentSQLiteは楽ちんだなぁ…と感じます。

ここでもちょっと詰めが甘いですけどね。削除失敗した時や削除すべきドキュメントがなかった時など、それぞれに適切な応答を返すようにしておきたいですね。

なお、ここではdeleteOneメソッドを使っています。queryで指定した条件に該当するドキュメントが複数の時はどうなるんでしょうね。おそらくドキュメントのうち「任意の」1つが削除されると思います。用意されているメソッド群を見る限り、「任意の」を制御できなさそうなので、素直に1件だけに絞り込める条件をqueryに指定するか、deleteManyメソッドに切り替えるようにするのが良さそうです。コードを書く側の責任というところでしょうか。

まとめ

RDBに慣れきってしまっているので、更新のやり方に違和感が残っています。そもそもMongoDBはおらなんとかさんのようなRDBとは性質や用途が異なるので、考え方自体を変えてみるのが良いかもしれません。
ドキュメントの中身を更新するよりも、一旦削除して追加するとか。主キーでレコードを特定する、といったアプローチもMongoDBのようなNoSQLには通用しにくいですね。

ドキュメントの作りをRDB風にしてしまって、MongoDBを「なんちゃってRDB」に仕立て上げてしまうのも一つの対応方法ですけど、それはどうだろうかと思います。DBとハサミは使いようということで柔軟に行きたいですね。

あぁついでに、updateOneupdateManyメソッドの他にreplaceOneメソッドを見つけました。それでなくとも違和感で困惑しているのに、使い分けに困るものを投下するMongoDB。掘り下げ必須ですね。

取り組みたい課題

今更かよ、という項目もありますが、復習ということでご勘弁を。

  • NoSQLの設計思想を知る(RDBの扱い方と何が違うのか)
  • PUTPOSTの使い分け
  • HTTPステータスの使い方
  • updateOnereplaceOneの違い

それと、今回はブラウザではなくMongoExpressを使っています。なぜか。GETできなくなったからです。この原因も探らないと……