MongoDBドキュメントを1件検索してLeafテンプレートに表示してみよう

MongoDB
MongoDBドキュメントを1件検索してLeafテンプレートに表示してみよう

はじめに

リスト表示ができたので、ドキュメント1件だけを表示する詳細ページも作ってみたいと思います。

本のリストに個別の情報を表示するページへのリンクに設けて、詳細を表示してみます。

Leafテンプレートでリンクを作る

基本的にはHTMLなので、リンクはAタグで作ることができます。

<a href="[リンク先のパス]">〇〇</a>

さて、リンク先のパスにVaporアプリケーションから渡されたデータを含めるにはどうしたら良いでしょうか。例えば、APIのパスが/api/bookで、この後に続けて本を特定するデータをつなげるという状況です。

Vaporアプリケーションから渡されたデータを表示するには#([キー])を使います。この#([キー])はレンダリングされたときに実際の値に展開されます。ですので、先ほどの例のAPIパスを使って、私は最初このように書きました。

<a href="/api/book/"+#([キー])>〇〇</a>

いつもSwiftやC#などを使っているので手癖が出てしまいましたね。文字列連結といえば+や&ですよね。ちなみにSQLでは||ですね。

ところが、このように書くと、リンク先のパスは/api/book/だけになってしまいます。HTMLでは文字列連結は+ではなかったんですね。

正解はこのようになります。

<a href=/api/book/#([キー])>〇〇</a>

ちなみにダブルクォーテーションでくくってしまうと、レンダリング時に展開されずに#([キー])がそのまま表示されてしまいします。

ドキュメントを特定する値を用意する

ドキュメントの中から使えそうな値を探す

ドキュメントを特定するにはユニークな値、例えばUUIDを各ドキュメントに割り当てておきます。または、ドキュメント新規作成時にMongoDBが自動で採番してくれるObjectIDの値を使います。

おすすめは前者です。ドキュメント作成時に都度UUIDを割り当てていくのがラクに管理できると思います。

ちなみに私はドキュメント作成時にUUIDなどのユニークな値を割り当てなかったため、自動で採番されたObjectIDの値を使います。各ドキュメント内のすべての値を使って特定するという方法もありますが、データの更新で値が変わったりする可能性があるため、悪手だと思います。

ObjectIDの扱いは少し厄介

このObjectID、扱いが少々厄介です。MongoDBからデータとして取得することは可能です。しかしながら、JSONへのパースが失敗します。つまり、そのままではLeafテンプレートへデータをレンダリングすることができないのです。

これを解消するためには、ObjectIDをただの文字列にしてあげる必要があります。加えて、Leafテンプレートへデータを渡すときはクラスか構造体か列挙型にする必要があります。

回りくどい形になりますが、構造体を2種類作り、データを変換した後Leafテンプレートへ渡していきます。

import MongoSwift

//MongoDBのデータを扱うための構造体
struct BookDB: Content {
    var _id: ObjectId?
    var title: String
    var publisher: String
    var price: Int
}

//Leafテンプレートでデータを扱うための構造体
struct Book: Codable {
    var id: String
    var title: String
    var publisher: String
    var price: Int
}

この2つを使ってデータの取得とLeafテンプレートへデータ渡しを行います。

router.get("api/books") { req -> Future<View> in
    let client = try! req.make(MongoClient.self)
    let collection = client.db("vaporapp").collection("books", withType: BookDB.self)

    var books = [Book]()

    let bookDocs = try! collection.find()
    for bookRaw in bookDocs {
        let book = Book(id: bookRaw._id?.hex ?? "", title: bookRaw.title, publisher: bookRaw.publisher, price: bookRaw.price)
        books.append(book)
    }

    return try req.view().render("BookList", ["books":books])
}

Leafテンプレートの作成と修正

リンクを作る

リスト表示をした時に作ったLeafテンプレートを少し修正します。

<td><a href=/api/book/#(book.id)>#(book.title)</a></td>

本のタイトルをリンクにしました。各ドキュメントのObjectIDをパスに付けました。リンクパスを/api/book/#(book.id)としましたので、後ほどroutes.swiftにAPIを作ります。

1件だけ表示するページを作る

今後データの更新や削除で使うことを見越して、新規追加のフォームを作った時のLeafテンプレートを流用します。新規作成時は各テキストボックスは空でしたが、今回は情報を表示しておきます。

<!DOCTYPE html>
<html>
    <head>
        <title>Book Detail</title>
    </head>
    <body>
        <h1>Book Detail</h1>
        <form method="" action=">
          <input type="hidden" name="id" value="#(book.id)">
          <label>書名</label>
          <br>
          <input type="text" name="title" value="#(book.title)" placeholder="書名を入力">
          <br>
          <label>出版社</label>
          <br>
          <input type="text" name="publisher" value="#(book.publisher)" placeholder="出版社を入力">
          <br>
          <label>金額</label>
          <br>
          <input type="number" name="price" value="#(book.price)">
          <br>
        </form>
    </body>
</html>

リンクをクリックした時のAPIを作る

先ほどパスを/api/book/#(book.id)と設定したので、これに対応するrouteを作ります。

router.get("api/book",String.parameter) { req -> Future<View> in
    let client = try! req.make(MongoClient.self)
    let collection = client.db("vaporapp").collection("books", withType: BookDB.self)

    let id = try req.parameters.next(String.self).lowercased()

    let query:Document = try Document.init(fromJSON: "{\"_id\": {\"$oid\": \"" + id + "\"}}")

    let bookDocs = try! collection.find(query, options: nil, session: nil)

    var books = [Book]()

    for bookRaw in bookDocs {
        let book = Book(id: bookRaw._id?.hex ?? "", title: bookRaw.title, publisher: bookRaw.publisher, price: bookRaw.price)
        books.append(book)
    }

    return try req.view().render("BookDetail", ["book":books[0]])
}

ここでの肝は次の2点です。

  • 絞り込みの条件でObjectIDを使うときは_idにObjectIDをそのまま渡すのではなく、{"_id": {"$oid": "[id]"}}とする
  • LeafテンプレートでレンダリングできるようにするためObjectIDを文字列にする

なお、最後でbooks[0]なんて書いていますが、良い子の皆様は真似しちゃダメですよ。

実行してみる

まずはリスト表示です。タイトルがリンクになっています。

本のリスト表示

リンクをクリックすると、その本の詳細が表示されます。

クリックした本の詳細が表示されます

まとめ

リンクにより、個別の情報を表示するページを開いてみることができました。

ObjectIDの扱いに一癖あったものの、性質を把握していれば対応可能だと思います。DB設計や構造体(orクラス)でのデータの取り回しなどを考える際に、この点を考慮しておくと良いと思います。ここで示したコードも、考慮できていればもっとシンプルに書けたはず!