MongoDBのドキュメントをページネーションでちょっとずつ表示する

MongoDB
MongoDBのドキュメントをページネーションでちょっとずつ表示する

はじめに

VaporアプリケーションでMongoDBのドキュメントを表示させることができました。ただし、このままではドキュメント数が多くなった場合に縦スクロールが長くなり、視認性が悪くなります。また、ドキュメント数が多くなるということは通信データ量が多くなることにつながるため、ネットワーク環境によっては表示されるまで時間が掛かるかもしれません。そうなると、利用者にとってはストレスになりますよね。

そこで、ページネーションを試してみようと思います。ドキュメント数が多い場合に小出しにする仕組みです。

MongoDBから一定量ずつドキュメントを取得する

まずはMongoDBから一定量ずつドキュメントを取得します。

MongoSwiftを使ってMongoDBのドキュメントを条件無しで全件取得するとき、

let booksDocs = try! collection.find()

としました。

部分的に取得するためには、「先頭から数えて何番目のドキュメントから取得するか?」ということと「一回で取得するドキュメント数の上限はいくつか?」を指定する必要があります。これを先ほどの条件を指定するには、

let booksDocs = try! collection.find([クエリ], options: FindOptions(limit:[取得する上限数], skip: [先頭から読み飛ばす件数], session: nil)

とします。オプションが増えましたね。

5件ずつ取得する例を示します。
ページ番号をクエリパラメータで指定することと、総ドキュメント数を取得して読み飛ばすドキュメント数を計算していること以外はドキュメント全件取得する時と大きく変わりません。

router.get("api/books") { req -> Future<View> in
    let LIMIT:Int = 5

    let client = try! req.make(MongoClient.self)
    let collection = client.db("vaporapp").collection("books", withType: BookDB.self)

    let bookCount:Int = try! collection.countDocuments()

    if let pageNumber = try? req.query.get(Int.self, at: "page") {
      var books = [Book]()

      let booksDocs = try! collection.find(Document.init(), options: FindOptions(limit: Int64(LIMIT), skip: Int64((pageNumber-1)*LIMIT)), session: nil)
      for bookRaw in booksDocs {
          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", BooksData(books: books, pageNumber: pageNumber, bookCount: bookCount))
    }
}

FindOptionsで指定するパラメータの型に注意してください。また、LeafテンプレートへObjectIDを渡すのは以前ご紹介したとおりです。

Vaporアプリケーションのrouteをページネーション用にする

ドキュメントをそのまま表示するのは利用者に対しては不親切

さてドキュメントが取得できたので、Leafテンプレートに表示します。

ページネーションを行うために部分的にしかドキュメントを取得していないので、これまでのようにドキュメントだけを渡してしまうと取得した以外のドキュメントを参照することができません。

クエリパラメータに適当にページ番号を与えてやれば、他のドキュメントを取得して表示させることができます。しかし、総ドキュメント数が分からず、当て推量でページ番号を渡すことになりますので、利用者に対してはとても不親切です。

そこで、リスト表示の他に、今のページ番号と総ドキュメント数、ページ移動のリンクも作ってみます。

必要な情報をVaporアプリケーションのrouteから渡す

先ほど例で示したrouteにしれっと書いたのですが、renderメソッドの第2引数にドキュメント以外のデータもまとめて渡しています。

return try req.view().render("BookList", BooksData(books: books, pageNumber: pageNumber, bookCount: bookCount))

LeafレンダリングのためにJSONデータを渡すのですが、JSONをゴリゴリ書かずに構造体でスッキリと渡しています。Encodableプロトコルがミソです。

struct BooksData: Encodable {
    var books: [Book] //ドキュメントデータ本体
    var pageNumber: Int = 0 //ページ番号
    var bookCount: Int = 0 //ドキュメント総数
    var message: String = "" //(参考)表示すべきメッセージがあればここにいれる
}

Leafテンプレートをページネーションに対応させる

基本的にはこれまでと変わりません

Vaporアプリケーションから渡されたドキュメントをリスト表示します。ここはこれまでと同じで、ドキュメントを一つずつテーブルに入れていきます。

<table>
  #for(book in books) {
    <tr>
      <td><a href=/api/book/#(book.id)>#(book.title)</a></td>
      <td>#(book.publisher)</td>
      <td>#(book.price)</td>
    </tr>
  }
</table>

ページ番号と総ドキュメント数を表示する

ここからページネーションのための改良です。

ページ番号と総ドキュメント数を表示します。これでどれだけのドキュメントがあって、いま何ページ目に居るのかがわかりやすくなります。

#if(pageNumber > 0) {
  #(pageNumber)ページ目(全#(bookCount)件)
}

ただ単に表示させるだけでは芸がないので、ささやかながら、ページネーションの必要がないときは表示させないようにしてみました。総ドキュメント数だけは表示しても良かったかもしれない。

ページ移動のリンクを作る

現在表示しているページ番号の前後をクエリパラメータとし、生成したパスをリンクタグに設定します。
前後のページ番号は動的に生成する必要があります。なので、Javascriptを使って計算しています。

#if(pageNumber > 1) {
    <a id="prevLink">前のページ</a>
    <script type="text/javascript">
        document.getElementById("prevLink").href = "/api/books?page=" + (#(pageNumber)-1);
    </script>
}
#if(pageNumber < bookCount / 5 + 1) {
    <a id="nextLink">次のページ</a>
    <script type="text/javascript">
        document.getElementById("nextLink").href = "/api/books?page=" + (#(pageNumber)+1);
    </script>
}

1ページ目のとき、一つ前のゼロページは存在する必要が無いのでリンク自体を表示しないようにしています。最終ページの次ページについても同様に、リンク自体を表示させていません。

なお、上限数ですが、vaporアプリケーションでは定数としたのに、Leafテンプレートではマジックナンバーにしてしまいました。HTMLで定数でどうやるんだったっけ?そもそもできたっけ?

動かしてみよう!

想定通り、ページネーションが実現できています。

途中ページ

先頭ページと最終ページでリンクの非表示もできていますね。

先頭ページ
最終ページ

まとめ

MongoDBから部分的にドキュメントを取得するということと、Leafテンプレートにどのようにレンダリングさせるかという2点を押さえておけば、ページネーションが簡単にできることが分かりました。

ここからの検討課題として、ページ番号を与えるクエリパラメータに不正な値が渡された時の対応が必要です。また、ユーザビリティ的な観点では、そもそもドキュメント数がゼロ件のときに何らかのメッセージを表示させたほうが良さげだったり、任意のページを表示できるようにリンクを並べたり、最終ページと先頭ページへのリンクを表示させておいたり、いろいろと検討できますね。