前回=>vte.cxによるバックエンドを不要にする開発(4.全文検索とOR検索)
今回はデータ登録における一貫性について、ホテル予約サンプルアプリケーションを元に説明します。
データ一貫性を考慮したホテル予約アプリケーションを作る
まず、ホテルを予約するシンプルなアプリケーションを作ってみましょう。
ここで大事になるのは、複数の顧客が、同じ部屋を予約しようとした場合の処理です。当然ですが各部屋は顧客一人しか予約できません。絶対に複数の顧客が同じ部屋を予約できないようにしなくてはいけません。
例えば 101 の部屋の空き状況を、二人の顧客(A と B)が同じタイミングで確認したとしましょう。そのときには 101 は空いていました。その後、A は B よりも早く 101 を予約します。その少し後に、B も 101 を予約しようとします。なぜなら B が見ている空き状況は、最初に確認したタイミングのものですから、A が予約したことが反映されていないからです。このような状況で予約ボタンを押した場合、「既に予約されているのでエラーにする」必要があります。
こういったシステムを作るために vte.cxには、idを元にした楽観的排他という仕組みがあります。
それぞれのentryには一意の識別子としてidがあり、idには更新回数であるrevisionが含まれています。(revisionは更新の都度+1されることになります) この revision を含む id を書き込み時に参照することで、既に他の顧客が更新した後なのかどうなのか、ということを判別しています。
では早速、アプリケーションの以下の項目を管理画面の新規エントリ項目追加から登録しましょう。
登録方法はこれまで行っていますのでわかりますね。
room
name
reserved(boolean)
エントリ項目一覧で以下のように表示されればOKです。(※ 前回使用したuserなどの項目が残っていても問題ありません)
また、エンドポイント管理で/hotel
というエンドポイントを追加しましょう。
(※ 前回使用した/fooが残っていても問題ありません)
登録したら、設定ファイルをダウンロードして更新しておきましょう。
npm run download:template
npm run download:typings
アプリケーションのソースコードは以下のようになります。
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useState } from 'react'
import axios from 'axios'
const App = () => {
const [rooms, setRooms] = useState<VtecxApp.Entry[]>([])
const initdata: VtecxApp.Entry[] = [
{
room: {
name: '101',
reserved: false
},
link: [
{
"___href": "/hotel/101",
"___rel": "self"
}
]
},
{
room: {
name: '102',
reserved: false
},
link: [
{
"___href": "/hotel/102",
"___rel": "self"
}
]
}
]
const putrooms = async (req:VtecxApp.Entry[]) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
await axios.put('/d/hotel',req)
getrooms()
} catch (e) {
alert('error:'+e)
}
}
const getrooms = async () => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/hotel?f')
setRooms(res.data)
} catch (e) {
alert('error')
console.log(e)
}
}
const showrooms = () => {
return rooms.map((entry,index) => {
if (entry.room&&!entry.room.reserved) {
return (
<div key={index}>
<p>{entry.room.name}
<button
onClick={() => {
reserve(entry)
}}
>
予約
</button>
</p>
</div>
)
}
})
}
const reserve = async (entry:VtecxApp.Entry) =>{
if (entry.room) {
entry.room.reserved = true
putrooms([entry])
}
}
return (
<div>
<button onClick={() => { putrooms(initdata) }}>
初期化
</button>
<button onClick={() => { getrooms() }}>
一覧
</button>
<br/>
{showrooms()}
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('container'))
これを、npm run serve:index
で起動してください(未ログインの場合は、先にnpm run serve:login
を実行してください)
すると、以下のような画面が出ますので、初期化ボタンを押してください。
ホテルの部屋一覧が表示されると思います。
このとき、サーバに登録されたデータは以下のようになっています。
部屋番号の右の予約ボタンを押すと予約が実行されてリストから消えます。
101号室と102号室をそれぞれ予約してみてください。元に戻すには初期化ボタンを押します。
楽観的排他制御の確認
次に、ブラウザのタブをもう一つ開いてアプリケーションを表示させてください。
1つ目のアプリケーション画面では初期化ボタンを押し、別のアプリケーション画面では一覧ボタンを押し、101号室を予約してください。そして、1つ目のアプリケーションに戻り、まだ表示されている101号室の予約を実行してください。
このとき、2つ目のアプリケーションが先に101号室を予約しているので、1つ目のアプリケーションはエラーになるはずです。実際に予約を実行すると以下のようなエラー画面が出てくると思います。
デベロッパーツールで詳しくエラーメッセージを見ると、{"feed" : {"title" : "Optimistic locking failed. Key = /hotel/101"}
のように出ています。
これは楽観的排他エラーが発生したことを意味します。
つまり、1つ目のアプリケーションで更新しようとしたentryのidが、既に2つ目のアプリケーションにより更新されて+1されており、一致しなかったというわけです。楽観的排他エラーになることで、ダブルブッキングをさせない空室予約が可能になるのです。
ちなみに、強制的に上書きする方法もあります。それは、PUT更新時にid項目を含めないでリクエストすることで、楽観的排他エラーを敢えて発生させずに更新することができます。
今回はここまでです。お疲れ様でした。