はじめに
MongoDBのドキュメントを一覧表示やページネーションの分割表示、一件ずつ表示をやってきました。あとは更新と削除をしてみようと思います。
ところがそう簡単には行かなかったわけです。物事はなかなかうまく進まないものです。
FormタグでGETとPOST以外のメソッドが使えない
FormタグにおいてHTTPメソッドはmethod属性に指定します。
<form method="POST" action=(パス)>
(中略)
</form>
この属性に指定できるのはなんとGETとPOSTのみ。Vaporアプリケーションではドキュメントの削除にDELETEメソッド、更新にPUTメソッドまたはPATCHメソッドを使っているので、どのように頑張ってもそのままではrouteを呼び出すことができません。
method_overrideで対応できるかどうか
Vaporアプリケーションをできるだけ変えず、Formタグのmethod属性にPUTやDELETEメソッドを指定できれば嬉しいです。しかしながら、Vaporアプリケーションに対しては、method_overrideを使ってメソッドを書き換える方法は通用しないようです。
試してみましたが、うまく書き換えられず、method属性に指定したGETかPOSTでAPIが発行されてしまいます。
仕方がないのでPOSTでなんとかしてみる
できるだけVaporアプリケーションを改変せずに対応したかったのですが、致し方がないです。
POSTメソッドでPUTとDELETEのrouteに対応させてみます。
方針
個人的な趣味ですが、APIパスにdeleteやupdateを入れたくないんです。例えば、
/api/delete/book/123456
/api/update/book/123456
APIのくせにSQLのような書き方になってしまうのが、どうにも馴染めないです。パスにはドキュメントの情報のみで、どういう処理をするかはGETやPOST、PUTなどのHTTPメソッドで指定してあげたいところです。
しかしながら、HTTPメソッドがGETかPOSTに限定されている以上、APIパスのどこかに更新か削除かを識別するための情報を入れる必要があります。
ですので、少々お茶を濁す形になりますが、クエリパラメータに入れることで対応をしたいと思います。
Leafテンプレートに更新と削除ボタンを作る
APIを呼び出す側であるLeafテンプレートに、更新と削除ボタンを作ります。
<!DOCTYPE html>
<html>
<head>
<title>Book Detail</title>
</head>
<body>
<h1>Book Detail</h1>
<form method="POST" action="/api/book/#(book.id)?q=update" name="form_1" id="form_1">
(中略)
<input type="submit" value="修正する">
</form>
<form method="POST" action="/api/book/#(book.id)?q=delete" name="form_2" id="form_2">
(中略)
<input type="submit" value="削除する">
</form>
</body>
</html>
Formタグ2つで冗長ですね。Formタグ1つにするには、Submitボタン2つを設置してjavascriptで処理分岐させる方法もあります。今回は、Leafテンプレートに極力ロジックを書きたくなかったので、あえて分けています。
この辺りのことは各人のお好みになります。
APIを作っていく
Leafテンプレートで削除と更新のAPIを指定しました。
/api/book/[本のID]?q=update
/api/book/[本のID]?q=delete
本の詳細を表示するパスにクエリパタメータを付けて更新または削除をするというイメージです。できるだけAPIっぽさを残してみた(多分残っていると思う)。
これをPOSTメソッドでAPIを発行するので、これを受けるrouteを作ります。クエリパラメータの値により処理を分岐する方法です。分岐内の処理はDELETEメソッドやPUTメソッドのAPIとほぼ同じです。
router.post(Book.self,at:"api/book",String.parameter) { (req, book) -> Future<HTTPStatus> 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()
if let method = try? req.query.get(String.self, at: "q") {
switch method {
case "delete":
return try req.content.decode(Book.self).map(to: HTTPStatus.self) { book in
let query:Document = try Document.init(fromJSON: "{\"_id\": {\"$oid\":\"" + id + "\" }}")
do {
try collection.deleteOne(query)
return .ok
} catch {
return .noContent
}
}
case "update":
return try req.content.decode(Book.self).map(to: HTTPStatus.self) { book in
let query: Document = try Document.init(fromJSON: "{\"_id\": {\"$oid\": \"" + book.id + "\"}}")
let book:Book = BookDB(_id: ObjectId.init(book.id), title: book.title, publisher: book.publisher, price: book.price)
_ = try! collection.replaceOne(filter: query, replacement: book)
return .ok
}
default:
(クエリパラメータがdelete,update以外の時の処理)
}
}
else {
(クエリパラメータが認識できない時の処理)
}
}
なお、HTTPステータスは再考の必要ありです。また、ベタ書きしていますが、メソッド切り分けしておくのが良いと思います。DELETEメソッドやPUTメソッドのAPIでも似たようなコードを書きますので。
MongoDBのObjectIDによるドキュメントの検索についてはこちらを参照してください。
恒例行事!動かしてみる。
更新してみる
任意の本の詳細を開きます。
内容を適宜書き換えて修正するボタンをクリック。
クリックした後はHTTPステータスが返ってくるだけなので、画面が真っ白になります。更新処理にフォーカスを当てているので作り込みは甘いです。実際は非同期処理をして、更新が終われば一覧を表示したりするほうが良いでしょう。
手動で一覧を表示し、先ほどの本の詳細を見てみます。更新されていることが分かります。
削除してみる
任意の本の詳細を開きます。
削除するボタンをクリック。クリックした後は更新の時と同様に真っ白になります。
手動で一覧を表示し、先ほどの本がなくなっていることが分かります。
まとめ
無事MongoDBのドキュメントを更新したり削除したりすることができました。
それにしても、Formタグでmethod属性に指定できるのがGETとPOSTだけというのはともかく、Vaporアプリケーションに対してmethod_overrideによるHTTPメソッドの書き換えができないとは思わなかった。