This chapter shows some common tasks and how to solve them with CouchDB using best practices and easy-to-follow step-by-step instructions.
この章では、いくつかの共通したタスクとそれをどのようにCouchDBで解決するかをベストプラクティスと、順を追った手順での方法を紹介します。
Banks are serious business. They need serious databases to store serious transactions and serious account information. They can’t lose any money. Ever. They also can’t create money. A bank must be in balance. All the time.
銀行は容易ではないビジネスです。重要な口座情報を、本格的なトランザクションで処理し保管するために本格的なデータベースを必要とします。少しでも金額を失うことは許されませんし、勝手に作ることも許されません。銀行は常に調和状態でなければなりません。
Conventional wisdom says a database needs to support transactions to be taken seriously. CouchDB does not support transactions in the traditional sense (although it works transactionally), so you could conclude CouchDB is not well suited to store bank data. Besides, would you trust your money to a couch? Well, we would. This chapter explains why.
昔からの見識では、厳粛さを取るならトランザクションをサポートすることがデータベースには必要であると言われます。CouchDBは伝統的な仕組みのトランザクションをサポートしません(ですが、トランザクションぽく動きます)、よってCouchDBは銀行のデータを保管するには適していないということがいえます。couchを信じてお金を預けることができるでしょうか?ええ、できます。この章はそれがなぜかを説明します。
Say you want to give $100 to your cousin Paul for the New York cheesecake he sent to you. Back in the day, you had to travel all the way to New York and hand Paul the money, or you could send it via (paper) mail. Both methods were considerably inconvenient, so people started looking for alternatives. At one point, banks offered to take care of the money and make sure it arrived at Paul’s bank safely without headaches. Of course, they’d charge for the convenience, but you’d be happy to pay a little fee if it could save a trip to New York. Behind the scenes, the bank would send somebody with your money to give it to Paul’s bank—the same procedure, but another person was dealing with the trouble. Banks could also batch money transfers; instead of sending each order on its own, they could collect all transfers to New York for a week and send them all at once. In case of any problems—say, the recipient was no longer a customer of the bank (remember, it used to take weeks to travel from one coast to the other)—the money was sent back to the originating account.
いとこのポールがあなたにニューヨークチーズケーキを送りたいといったので、$100をあげたいとします。ずっと前は、ニューヨークに行ってポールにお金を手渡しするか、手紙で送ることができました。どちらも不便なので、人々は他の方法を探し始めます。一つは、銀行が安全かつ確実に、手数料なしでポールの口座にお金を届ける方法を提供します。もちろん、便利にお金を移動できます。ニューヨークへの旅行代金を節約でき、ちょっとした手数料を払えばよいので嬉しいでしょう。この背景には、ポールの銀行口座にあなたのお金を送るために、銀行が誰かに送っています。同様な手続きで、他の誰かはトラブルを処理しています。銀行は、それぞれの取引をそれぞれ送るのではなく、一括で送ります。一週間分のニューヨークへの送信をすべて集めて一度に送ることができます。何か問題が起きたら、受領書が銀行のお客さんのもとに届くまでもなく(覚えておいてください、一つの海岸から他のところへ移動するのに数週間はかかります)。お金は元の口座に送り返されます。
Eventually, the modern banking system was put in place and the actual sending of money back and forth could be stopped (much to the disdain of highwaymen). Banks had money on paper, which they could send around without actually sending valuables. The old concept is stuck in our heads though. To send somebody money from our bank account, the bank needs to take the notes out of the account and bring them to the receiving account. But nowadays we’re used to things happen instantaneously. It takes just a few clicks to order goods from Amazon and have them placed into the mail, so why should a banking transaction take any longer?
結果的に、現代の銀行システムは実際の返金の送付や外部への送金を停止することができる機能を持っています(多くの強奪を退けられる)。銀行は紙としてお金を持っており、それらは実際に送られること無く送金されます。古い考えは私たちの知性の中では全く堅苦しいものになるのです。自分たちの銀行口座から誰かにお金を送金するためには、銀行が口座から紙幣を取り出し、受け取り先の口座へ送る必要があります。しかし、現在では即座にここで起きているようなことをやっています。Amazonから物をたったの数回のクリックで購入し、それが郵送で送られてきます。それなのになぜ、銀行でのトランザクションはそれでもなお必要とされるのでしょうか?
Banks are all electronic these days (and have been for a while). When we issue a money transfer, we expect it to go through immediately, and we expect it to work in the way it worked back in the day: take money from my account, add it to Paul’s account, and if anything goes wrong, put it back in my account. While this is logically what happens, that’s not quite how it works behind the scenes, and hasn’t since way before computers were used for banking.
最近では銀行はすべて電子化されています(そしてそれはしばらくは変わらないでしょう)。すぐに送られることを望んでいる送金でも、失敗した時には、一日のうちに返金されるような動きを期待しています。自分の口座からお金を取り出し、ポールの口座に追加し、そしてもし何か良くないことが起きたら、自分の口座に戻るような動作です。これは起きたことへの論理的な理解で、裏でどのようなことが行われているのか全く分かりません。コンピュータが銀行で使われる前からもそうです。
When you go to your bank and ask it to send money to Paul, the accountant will start a transaction by noting down that you ordered the sending of the money. The transaction will include the date, amount, and recipient. Remember that banks always need to be in balance. The money taken from your account cannot vanish. The accountant will move the money into an in-transit account that the bank maintains for you. Your account balance at this point is an aggregation of your current balance and the transactions in the in-transit account. Now the bank checks whether Paul’s account is what you say it is and whether the money could arrive there safely. If that’s the case, the money is moved in another single transaction from the in-transit account to Paul’s account. Everything is in balance. Notice how there are multiple independent transactions, not one big transaction that combines a number of actions.
あなたが銀行へ行き、ポールにお金を送ることを伝えたとき、銀行員はあなたから送金を依頼されたことを書き留めて、トランザクションを開始します。トランザクションには、日付や金額、領収書が含まれます。銀行は常に調和を必要としていることを思い出してください。あなたの口座から取り出されたお金は消えてはいけません。銀行員はあなたの情報を管理しつつ、転送中口座にお金を移動させます。この時点でのあなたの口座の額は、現在の口座の金額と転送中口座のトランザクションの金額の合計です。このとき、銀行はポールの口座が存在しているかどうか、お金が安全にそこへ到着したかどうかを確認します。もし問題なければ、別のトランザクションでお金を転送中口座からポールの口座へ移動させます。すべては調和の中にいます。多くの複数の独立したトランザクションが存在していることに注意してください、たくさんの処理をまとめた一つの大きなトランザクションがあるわけではありません。
Now let’s consider an error case: say Paul’s account no longer exists. The bank finds this out while performing the batch operation of all the in-transit transactions that need to be performed. A second transaction is generated that moves the money back from the in-transit account to your bank account. Note that the transaction that moved the money out of your account is not undone. Rather, a second transaction that does the reverse action is created.
別のエラーの場合を考えてみましょう。あなたがポールへ$100送金しようとして、十分な金額が無かったとします。これは、銀行がお金を減らすトランザクションを生成する前に、銀行員(もしくはソフトウェア)によってチェックされます。説明すると、銀行は処理が発生しなかったふりをすることはできません。それは全ての処理がログに記録されなければならないからです。返金処理のために、明確に取り消し処理が実行され、すでに存在するトランザクションを取り消したり、削除することはできません。
Here’s another error case: say you don’t have sufficient funds to send $100 to Paul. This will be checked by the accountant (or software) before the bank creates any money-deducting transaction. For accountability, a bank cannot pretend an action didn’t happen; it has to record every action minutely in a log. Undoing is done explicitly by performing a reverse action, not by reverting or removing an existing transaction. “Accountants don’t use erasers” is a quote from Pat Helland, a senior architect of transactional systems who worked at Microsoft and Amazon.
別のエラーの場合を考えてみましょう。あなたがポールへ$100送金しようとして、十分な金額が無かったとします。これは、銀行がお金を減らすトランザクションを生成する前に、銀行員(もしくはソフトウェア)によってチェックされます。説明すると、銀行は処理が発生しなかったふりをすることはできません。それは全ての処理がログに記録されなければならないからです。返金処理のために、明確に取り消し処理が実行され、すでに存在するトランザクションを取り消したり、削除することはできません。"銀行員は消しゴムを使わない"はMicrosoftとAmazonで働いていたトランザクションシステムのシニアアーキテクトであるPat Hellandの言葉です。
To rehash, a transaction can succeed or fail, but nothing in between. The only operation that CouchDB guarantees to have succeed or fail is a single document write. All operations that comprise a transaction need to be combined into a single document. If business logic detects that an error occurred (e.g., not enough funds), a reverse transaction needs to be created.
繰り返すと、トランザクションは成功や失敗はできますがそれ以外はありません。CouchDBが成功や失敗を保証している処理は一つのドキュメントの書き込みのみです。トランザクションを含む全ての処理は一つのドキュメントに集約されている必要があります。もしビジネスロジックがエラーが発生したこと(資金が足りないなど)を検出するなら、元に戻すトランザクションが作られなければなりません。
Let’s look at a CouchDB example. We mentioned earlier that your account balance is an aggregated value. If we stick to this picture, things become downright easy. Instead of updating the balance of two accounts (yours and Paul’s, or yours and the in-transit account), we simply create a single transaction document that describes what we’re doing and use a view to aggregate your account balance.
CouchDBの例を見てみましょう。あなたの口座の額は値の合計であることを前で述べました。もし、私たちがこの状況を考えるなら、ことはとても単純になるでしょう。二つの口座(あなたの口座とポールの口座、または転送中口座)の額を更新するかわりに、私たちは単純に処理することを記載した一つのトランザクションドキュメントを作成し、あなたの口座の額を合計するようなビューを使います。
Let’s consider a bunch of transactions:
トランザクションのまとまりを考えてみましょう。
... {"from":"Jan","to":"Paul","amount":100} {"from":"Paul","to":"Steve","amount":20} {"from":"Work","to":"Jan","amount":200} ...
Single document writes in CouchDB are atomic. Querying a view forces an update to the view index with all changes to all documents. The view result is always consistent with the data in our documents. This guarantees that our bank is always in balance. There are many more transactions, of course, but these will do for illustration purposes.
CouchDBでの一つのドキュメントの書き込みはアトミックです。ビューを要求すると、すべてのドキュメントに対して、すべての変更のビューインデックスの更新を強制的に行います。ビューの結果はドキュメントのデータと共に常に一貫性があります。これは、銀行が常に調和が取れていることを保証しています。たくさんのトランザクションがあっても、もちろん、これらは意図したとおりに動きます。
How do we read the current account balance? Easy—create a MapReduce view:
どのように現在の口座の金額を読めばよいでしょうか?簡単です、MapReduceビューを作ります。
function(transaction) { emit(transaction.from, transaction.amount * -1); emit(transaction.to, transaction.amount); }
function(keys, values) { return sum(values); }
Doesn’t look too hard, does it? We’ll store this in a view balance
in a _design/account
document. Let’s find out Jan’s balance:
難しくは見えないでしょう?_design/account
ドキュメントのbalance
ビューとして保存します。Janの金額を見てみましょう。
curl 'http://127.0.0.1:5984/bank/_design/account/_view/balance?key="Jan"'
CouchDB replies:
CouchDBは以下のように返します。
{"rows":[ {"key":null,"value":100} ]}
Looks good! Now let’s see if our bank is actually in balance. The sum of all transactions should be zero:
良さそうです!さて、私たちの銀行が実際に調和がとれているか確認しましょう。すべてのトランザクションの合計はゼロであるべきです。
curl http://127.0.0.1:5984/bank/_design/account/_view/balance
CouchDB replies:
CouchDBは以下のように返します。
{"rows":[ {"key":null,"value":0} ]}
This should explain that applications with strong consistency requirements can use CouchDB if it is possible to break up bigger transactions into smaller ones. A bank is a good enough approximation of a serious business, so you can be safe modeling your important business logic into small CouchDB transactions.
ここまで、大きなトランザクションをより小さなトランザクションに分解できる時、強い一貫性を必要とするアプリケーションがCouchDBを使うことができることを説明しています。銀行は重大でしっかりとしたビジネスの例としては十分でしょう。よって、重要なビジネスロジックに小さなCouchDBトランザクションを安全に利用することができると言えます。
Views let you sort things by any value of your data—even complex JSON keys are possible, as we’ve seen in earlier chapters. Sorting by date is very useful for allowing users to find things quickly; a name is much easier to find in a list of names that is sorted alphabetically. Humans naturally resort to a divide-and-conquer algorithm (sound familiar?) and don’t consider a large part of the input set because they know the name won’t show up there. Likewise, sorting by number and date helps a great deal to let users manage their ever-increasing amounts of data.
ビューはあなたのデータの何かの値で、値をソートします。前の章で見てきたように、規則的で複雑なJSONキーが利用可能です。日付でのソートは値をすぐに見つけるための方法としてとても便利ですし、名前を使えば、アルファベット順で並べられた名前のリストから見つけることも簡単になります。人は分割統治法(聞き覚えはありますか?)で自然に再並び替えします。そして、入力セットの大部分を考慮しません。なぜなら、名前がその部分に現れることは無いことを知っているからです。同じように、数字や日付で並べることは、増え続けるデータを管理する上での良い取り扱い方法の手助けになります。
There’s another sorting type that is a little more fuzzy. Search engines show you results in order of relevance. That relevance is what the search engine thinks is most relevant to you given your search term (and potential search and surfing history). There are other systems trying to infer from earlier data what is most relevant to you, but they have the near-to-impossible task of guessing what a user is interested in. Computers are notoriously bad at guessing.
他の並び替え方法として、もう少し曖昧なものもあります。サーチエンジンは関連のある結果を返します。この関連はサーチエンジンがあなたが検索した単語(そしてポテンシャルサーチや履歴検索など)に最も関連のありそうなものを考えた結果です。あるシステムではあなたに最も関連のありそうなデータも新しい方から提示しようとするものもあります。しかし、それらは利用者が興味を持っていることを推測するという不可能に近いことをしようとしています。コンピュータは推測することが苦手であることはよく知られています。
The easiest way for a computer to figure out what’s most relevant for a user is to let the user prioritize things. Take a to-do application: it allows users to reorder to-do items so they know what they need to work on next. The underlying problem—keeping a user-defined sorting order—can be found in a number of other places.
利用者にとって最も関連性があるものを提示するためにコンピュータができる最も簡単な方法は、利用者に優先順位を決めさせることです。ToDoアプリケーションを考えてみます。アプリケーションでは利用者にToDoリストの並び替えを許可しています。そのため、次に何をやる必要があるかをアプリケーションは知っています。ここでの問題(ユーザ定義の並び替え順番を保つこと)はたくさんの他のアプリケーションでも確認することができます。
Let’s stick with the to-do application example. The naïve approach is pretty easy: with each to-do item we store an integer that specifies the location in a list. We use a view to get all to-do items in the right order.
ToDoアプリケーションをもう少し見てみましょう。 とても簡単な方法は、リストの保管位置を示す数字をつけてToDoリストを保管することです。正しい順番でToDoリストを取得するにはビューを使います。
First, we need some example documents:
まずは、いくつかのサンプルのドキュメントが必要ですね。
{ "title":"Remember the Milk", "date":"2009-07-22T09:53:37", "sort_order":2 } { "title":"Call Fred", "date":"2009-07-21T19:41:34", "sort_order":3 } { "title":"Gift for Amy", "date":"2009-07-19T17:33:29", "sort_order":4 } { "title":"Laundry", "date":"2009-07-22T14:23:11", "sort_order":1 }
Next, we create a view with a simple map function that emits rows that are then sorted by the sort_order
field of our documents. The view’s result looks like we’d expect:
次に、ドキュメントのsort_order
フィールドの値を使って並べられるように、行をemitする単純なmap関数をもったビューを作成します。ビューの結果は期待したようになります。
function(todo) { if(todo.sort_order && todo.title) { emit(todo.sort_order, todo.title); } }
{ "total_rows": 4, "offset": 0, "rows": [ { "key":1, "value":"Laundry", "id":"..." }, { "key":2, "value":"Remember the Milk", "id":"..." }, { "key":3, "value":"Call Fred", "id":"..." }, { "key":4, "value":"Gift for Amy", "id":"..." } ] }
That looks reasonably easy, but can you spot the problem? Here’s a hint: what do you have to do if getting a gift for Amy becomes a higher priority than remembering the milk? Conceptually, the work required is simple:
そこそこ簡単に思えますが、どうやって問題を解決するのでしょうか?ヒントは、"もし、'Gift for Amy'を、'Remember the Milk'よりも高い優先度にしたいとき、どうしなければならないか"を考えます。概念的にはやることは簡単です。
sort_order
of “Remember the Milk.”sort_order
を設定します。 sort_order
of “Remember the Milk” and all items that follow by one.sort_order
をインクリメントします。Under the hood, this is a lot of work. With CouchDB you’d have to load every document, increment the sort_order
, and save it back. If you have a lot of to-do items (I do), then this is some significant work. Maybe there’s a better approach.
内部的には、たくさんのやることがあります。CouchDBで全てのドキュメントを読み込まなくてはならず、sort_order
をインクリメントし、保存して戻します。もし、大量の(やろうとしている)ToDoがある場合、いくつかの重要な動作があり、おそらくより良い方法があります。
The fix is simple: instead of using an integer to specify the sort order, we use a float:
修正は簡単です。並び順を指定する為に整数を使う代わりに 、小数点数を使います。
{ "title":"Remember the Milk", "date":"2009-07-22T09:53:37", "sort_order":0.2 } { "title":"Call Fred", "date":"2009-07-21T19:41:34", "sort_order":0.3 } { "title":"Gift for Amy", "date":"2009-07-19T17:33:29", "sort_order":0.4 } { "title":"Laundry", "date":"2009-07-22T14:23:11", "sort_order":0.1 }
The view stays the same. Reading this is as easy as the previous approach. Reordering becomes much easier now. The application frontend can keep a copy of the sort_order
values around, so when we move an item and store the move, we not only have available the new position, but also the sort_order
value for the two new surrounding items.
先ほどと同じビューがあるとします。読み込みは前の方法と同じように単純です。並び替えはもっと簡単になります。アプリケーションフロントエンドはsort_order
の値のコピーを保持し続けることができます。新しい場所にするだけでなく、 二つのToDoのsort_order
の値を変更するような、ToDoの移動や、移動を保存したりする場合にもそうです。
Let’s move “Gift for Amy” so it’s above “Remember the Milk.” The surrounding sort_order
s in the target position are 0.1
and 0.2
. To store “Gift for Amy” with the correct sort_order
, we simply use the median of the two surrounding values: (0.1 + 0.2) / 2 = 0.3 / 2 = 0.15
.
"Gift for Amy"を"Remember the Milk"の上に移動させましょう。移動先の周りのsort_order
は0.1
と0.2
です。正しいsort_order
で"Gift for Amy"を保管するために、二つの周りの値の中間値を使います。(0.1+0.2)/2=0.3/2=0.15
です。
If we query the view again, we now get the desired result:
もしビューへの再問い合わせをすると、望ましい結果を得られます。
{ "total_rows": 4, "offset": 0, "rows": [ { "key":0.1, "value":"Laundry", "id":"..." }, { "key":0.15, "value":"Gift for Amy", "id":"..." }, { "key":0.2, "value":"Remember the Milk", "id":"..." }, { "key":0.3, "value":"Call Fred", "id":"..." } ] }
The downside of this approach is that with an increasing number of reorderings, float precision can become an issue as digits “grow” infinitely. One solution is not to care and expect that a single user will not exceed any limits. Alternatively, an administrative task can reset the whole list to single decimals when a user is not active.
この方法での良くないところは、並び替えの数が増えていくと、小数点数の精度が無限に成長する数字として問題になることです。一つの解決方法は無視して、1人の利用者があらゆる限界を超えないことを期待することです。他の方法は、利用者がアクティブでなくなった時に、管理タスクがリスト全体を 一つの小数点にリセットすることです。
The advantage of this approach is that you have to touch only a single document, which is efficient for storing the new ordering of a list and updating the view that maintains the ordered index since only the changed document has to be incorporated into the index.
この方法の良いところは、一つのドキュメントだけに触れればよいことです。これは、新しい順番でリストを保存することや、変更のあったドキュメントがインデックスに組み込まれてからインデックスの並びを管理するためにビューをアップデートするために有効な方法です。
This recipe explains how to paginate over view results. Pagination is a user interface (UI) pattern that allows the display of a large number of rows (the result set) without loading all the rows into the UI at once. A fixed-size subset, the page, is displayed along with next and previous links or buttons that can move the viewport over the result set to an adjacent page.
このレシピではどのようにビューの結果をページとして構成するかを説明します。ページ構成によりユーザインタフェースでたくさんの行(結果の集まり)を、一度にユーザインタフェースにすべての行を読み込むことなく表示することを可能にします。小さく分けられたサブセット(ページ)は、表示領域を結果の集まり上の隣りのページへ移動させるためのnextとpreviousのリンクやボタンに挟まれた形で表示されます。
We assume you’re familiar with creating and querying documents and views as well as the multiple view query options.
ここでは、あなたがドキュメントの作成やクエリ、複数のビュークエリオプションやビューに精通していると想定しています。
To have some data to work with, we’ll create a list of bands, one document per band:
バンドのリストを作るためのいくつかのデータを、バンド毎に一つのドキュメントとして用意します。
{ "name":"Biffy Clyro" } { "name":"Foo Fighters" } { "name":"Tool" } { "name":"Nirvana" } { "name":"Helmet" } { "name":"Tenacious D" } { "name":"Future of the Left" } { "name":"A Perfect Circle" } { "name":"Silverchair" } { "name":"Queens of the Stone Age" } { "name":"Kerub" }
We need a simple map function that gives us an alphabetical list of band names. This should be easy, but we’re adding extra smarts to filter out “The” and “A” in front of band names to put them into the right position:
バンド名のアルファベット順のリストを得るため、簡単なmap関数が必要です。これは簡単ですが、私たちは特別な機能として、バンド名の先頭の"The"と"A"を外し、バンド名を正しい順番に持ってくるようにしました。
function(doc) { if(doc.name) { var name = doc.name.replace(/^(A|The) /, ""); emit(name, null); } }
The views result is an alphabetical list of band names. Now say we want to display band names five at a time and have a link pointing to the next five names that make up one page, and a link for the previous five, if we’re not on the first page.
ビューの結果は、バンド名のアルファベット順になります。ここで、一つの画面にはバンド名を5つ表示し、もし最初の1ページ目でなければ一つのページに表示できるよう前後の5つの名前に移動するためのリンクをそれぞれ作ります。
We learned how to use the startkey
, limit
, and skip
parameters in earlier chapters. We’ll use these again here. First, let’s have a look at the full result set:
私たちはどのようにstartkey
、limit
やskip
パラメータを使うかを前の章で学びました。ここではそれらを使います。まず、結果の全体を見てみましょう。
{"total_rows":11,"offset":0,"rows":[ {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null}, {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null}, {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null}, {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null}, {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null}, {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null}, {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null}, {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null}, {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age","value":null}, {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null}, {"id":"fd590d4ad53771db47b0406054f02243","key":"Tool","value":null} ]}
The mechanics of paging are very simple:
ページの構成はとてもシンプルです。
Or in a pseudo-JavaScript snippet:
または擬似的なJavaScriptのスニペットはこうです。
var result = new Result(); var page = result.getPage(); page.display(); if(result.hasPrev()) { page.display_link('prev'); } if(result.hasNext()) { page.display_link('next'); }
Don’t use this method! We just show it because it might seem natural to use, and you need to know why it is a bad idea. To get the first five rows from the view result, you use the ?limit=5
query parameter:
この方法は使わないでください!ここでは、使い方として自然に見えるかもしれないため、紹介しているだけで、なぜこの方法が良くないかを知って欲しいからです。ビューの結果から始めの5行を取得するために、?limit=5
クエリパラメータを利用します。
curl -X GET http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5
The result:
結果はこのようになります。
{"total_rows":11,"offset":0,"rows":[ {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null}, {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null}, {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null}, {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null}, {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null} ]}
By comparing the total_rows
value to our limit
value, we can determine if there are more pages to display. We also know by the offset
member that we are on the first page. We can calculate the value for skip=
to get the results for the next page:
total_rows
の値とlimit
の値を比較することで、表示するベージがまだあるかを決めることが出来ます。そして、offset
の値で、それが始めのページなのかを知ることもできます。以下のような方法で、次のページの結果を取得するためのskip=
の値を計算することができます。
var rows_per_page = 5; var page = (offset / rows_per_page) + 1; // == 1 var skip = page * rows_per_page; // == 5 for the first page, 10 for the second ...
So we query CouchDB with:
よってCouchDBへのクエリはこのようになります。
curl -X GET 'http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5&skip=5'
Note we have to use '
(single quotes) to escape the &
character that is special to the shell we execute curl
in.
curl
での特殊文字である&
文字をエスケープするために、'
(シングルクオート)を使わなければならないことに注意してください。
The result:
結果はこうなります。
{"total_rows":11,"offset":5,"rows":[ {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null}, {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null}, {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null}, {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age","value":null}, {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null} ]}
Implementing the hasPrev()
and hasNext()
method is pretty straightforward:
hasPrev()
とhasNext()
メソッドを実装するのがとっても分かりやすいです。
function hasPrev() { return page > 1; } function hasNext() { var last_page = Math.floor(total_rows / rows_per_page) + (total_rows % rows_per_page); return page != last_page; }
This all looks easy and straightforward, but it has one fatal flaw. Remember how view results are generated from the underlying B-tree index: CouchDB jumps to the first row (or the first row that matches startkey
, if provided) and reads one row after the other from the index until there are no more rows (or limit
or endkey
match, if provided).
この方法は、単純で直感的に見えます。しかし、一つの致命的な欠陥があります。B-tree indexのもとで、どのようにビューの結果が生成されているか思い出して下さい。CouchDBは始めの行に移動します(startkey
が提供されれば合致したところの始めの行)そして、インデックスの後ろの行から行が無くなるまで(もし提供されたなら、limit
やendkey
が合致したところまで)読み込みます。
The skip
argument works like this: in addition to going to the first row and starting to read, skip
will skip as many rows as specified, but CouchDB will still read from the first row; it just won’t return any values for the skipped rows. If you specify skip=100
, CouchDB will read 100 rows and not create output for them. This doesn’t sound too bad, but it is very bad, when you use 1000
or even 10000
as skip
values. CouchDB will have to look at a lot of rows unnecessarily.
skip
引数は後述のように動作します。始めの行への移動に加え、読み込みを開始します。skip
は指定された数の行を読飛ばします。しかし、CouchDBはまだ始めの行から読み込もうとします。読飛ばされた行の値を返すことはありません。もし、skip=100
を指定した場合、CouchDBは100行を読み込み、それらの出力をしません。これは、とても悪いようには聞こえませんが、ものすごく良くありません。あなたが1000
や10000
をskip
の値として与えた時よくないことが起きます。CouchDBは不要に多くの行を探さなければならなくなります。
As a rule of thumb, skip
should be used only with single digit values. While it’s possible that there are legitimate use cases where you specify a larger value, they are a good indicator for potential problems with your solution. Finally, for the calculations to work, you need to add a reduce function and make two calls to the view per page to get all the numbering right, and there’s still a potential for error.
大まかにいえば、skip
は単一の数値だけで利用されるべきです。大きな値を指定するような正当なユースケースがあるなら、潜在的な問題の解決策の良い指標です。最後に、動作の計算のために、reduce関数を追加する必要があり、すべての正しい番号付けを得るため、二つのコールをページ毎のビューに作る必要があります。
The correct solution is not much harder. Instead of slicing the result set into equally sized pages, we look at 10 rows at a time and use startkey
to jump to the next 10 rows. We even use skip
, but only with the value 1
.
正しい解決方法は、すごく難しいものではありません。結果全体を同じサイズのページに分割する代わりに、一回で10行を検索し、startkey
を次の10行を得るために使います。私たちはskip
を使いますが、値は常に1
です。
Here is how it works:
どのように動作するかを示します。
rows_per_page
+ 1 rows from the viewrows_per_page
+ 1 の行数をリクエストしますrows_per_page
rows, store + 1 row as next_startkey
and next_startkey_docid
rows_per_page
分の行を表示し、+1の行はnext_startkey
とnext_startkey_docid
として保管しますstartkey
and next_startkey
startkey
とnext_startkey
を保持しますnext_*
values to create the next link, and use the others to create the previous linknext_*
の値を使い、次へのリンクを作成します。そして、もう一方を使い前へのリンクを作りますThe trick to finding the next page is pretty simple. Instead of requesting 10 rows for a page, you request 11 rows, but display only 10 and use the values in the 11th row as the startkey
for the next page. Populating the link to the previous page is as simple as carrying the current startkey
over to the next page. If there’s no previous startkey
, we are on the first page. We stop displaying the link to the next page if we get rows_per_page
or less rows back. This is called linked list pagination, as we go from page to page, or list item to list item, instead of jumping directly to a pre-computed page. There is one caveat, though. Can you spot it?
次のページを探す種はとても簡単です。1ページ用に10行リクエストする代わりに、11行リクエストします。しかし、10行だけを表示し、11行目の値は次のページのためのstartkey
として利用します。前のページへのリンクを作成するには、次のページへ行くための現在のstartkey
を持ち運ぶぐらい簡単です。もし前のstartkey
が無いなら、そこは最初のページです。もしrows_per_page
が得られるか戻る行が無い場合は、次のページへのリンクを表示することを行いません。これはLinked Listページネーションと呼ばれます。ページからページへの移動やリストからリストへの移動を、事前計算したページへ直接移動する代わりに行います。ですが、覚え書きがあります。検討はつきますか?
CouchDB view keys do not have to be unique; you can have multiple index entries read. What if you have more index entries for a key than rows that should be on a page? startkey
jumps to the first row, and you’d be screwed if CouchDB didn’t have an additional parameter for you to use. All view keys with the same value are internally sorted by docid
, that is, the ID of the document that created that view row. You can use the startkey_docid
and endkey_docid
parameters to get subsets of these rows. For pagination, we still don’t need endkey_docid
, but startkey_docid
is very handy. In addition to startkey
and limit
, you also use startkey_docid
for pagination if, and only if, the extra row you fetch to find the next page has the same key as the current startkey
.
CouchDBのビューのキーはユニークでなくても構いません。複数のreadインデックスを持つことも可能です。もし今の行よりもインデックスを持つなら、何をページにするべきでしょうか?startkey
は始めの行に移動します。そして、もし利用する追加のパラメータをCouchDBが持っていなければ、きっと困惑するでしょう。同じ値のキーを持つビューは、docid
で並べられます。docid
はビューとして作られたドキュメントのIDです。得られるビューの中から、startkey_docid
とendkey_docid
を使い、部分的に得ることができます。ページ構造を作るなら、まだendkey_docid
は必要ではありませんが、startkey_docid
はとても有効です。startkey
とlimit
に加えて、startkey_docid
をページ構成に利用し、次のページを見つけるための特別な行が現在のstartkey
のような同じキーを持ちます。
It is important to note that the *_docid
parameters only work in addition to the *key
parameters and are only useful to further narrow down the result set of a view for a single key. They do not work on their own (the one exception being the built-in _all_docs
view that already sorts by document ID).
重要なことは、*_docid
パラメータは*key
パラメータとの組み合わせでしか動作しないことと、一つのキーで取得されるビューの結果からより狭めて取得するときにのみ便利であることです。この*_docid
パラメータだけでは動作しません(一つの例外として、_all_docs
ビューはすでにドキュメントIDで並べられています)。
The advantage of this approach is that all the key operations can be performed on the super-fast B-tree index behind the view. Looking up a page doesn’t include scanning through hundreds and thousands of rows unnecessarily.
この方法の利点は、全てのキー操作がビューに対する高速なB-treeインデックス上で動作できることです。ページを見つけるために、数百、数千の行を走査することはありません。
One drawback of the linked list style pagination is that you can’t pre-compute the rows for a particular page from the page number and the rows per page. Jumping to a specific page doesn’t really work. Our gut reaction, if that concern is raised, is, “Not even Google is doing that!” and we tend to get away with it. Google always pretends on the first page to find 10 more pages of results. Only if you click on the second page (something very few people actually do) might Google display a reduced set of pages. If you page through the results, you get links for the previous and next 10 pages, but no more. Pre-computing the necessary startkey
and startkey_docid
for 20 pages is a feasible operation and a pragmatic optimization to know the rows for every page in a result set that is potentially tens of thousands of rows long, or more.
Linked Listページネーションの一つの欠点は、ページ番号やページ毎の行から特定のページの行を事前計算できないことです。指定のページへ移動することは正しく動きません。直感では、このことに直面したら、"Googleでさえそれはやってない!"、そしてそれを避けようとする傾向があります。Googleは常に一つ目のページに10以上のページ結果が見つかったようなふりをします。もしかすると2ページ目をクリック(大抵のほとんどの人が実際はするでしょう)してからGoogleはページセットを減らして表示しているかもしれません。もし結果を通じてページを移動する場合、前後10ページを表示するリンクを使います。しかし、それ以上の方法はありません。事前計算は20ページのために必要なstartkey
やstartkey_docid
が、実現可能な処理で、潜在的に数万やそれ以上の行の結果の集まりの中で、すべてのページのための行を知るためにプログラム可能な最適化方法です。
If you really do need to jump to a page over the full range of documents (we have seen applications that require that), you can still maintain an integer value index as the view index and take a hybrid approach at solving pagination.
もし本当にドキュメントの全範囲での直接移動が必要な場合(私たちはそのようなアプリケーションを見たことがあります)、ビューインデックスとして数値のインデックスを管理することやページネーションを解決するハイブリッドな方法を取ることができます。