TT Lab
はじめる
学ぶ 学習パス コース

FastAPI — 型がそのまま契約だ

ETagで更新の上書き競合を防ぐAPI

TT Labで続きを見る

目標

2人のユーザーが同じ下書きを更新するときに、SQLの条件付き更新とHTTPのレスポンスで、消失を防ぎます。

なぜ重要なのか

正常なリクエストが1回成功しても、境界値や障害からの復旧は保証されません。このラボは、各関数の契約を小さく実装またはテストしたあと、実際の実行につなげます。コードが存在するかどうかやレポートの文言だけを見るのではなく、結果・例外・保存された状態を検査します。前のステップのコードを維持しながら、次のステップへ進んでください。

ステップ

  1. /root/work/fa-conditional-lab/service.pyで、init_db(path)は、SQLiteのファイルにnotes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)を作成してください。繰り返し呼び出しても、既存のデータを削除しません。最初の準備は、次のコマンドで行います。
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
  1. /root/work/fa-conditional-lab/service.pyで、create_note(path, note_id, title)は、バージョン1でメモをINSERTしてコミットしてください。同じidはsqlite3.IntegrityErrorで拒否し、既存のタイトルを上書きしません。
  2. /root/work/fa-conditional-lab/service.pyで、read_note(path, note_id)は、{id, title, version}の辞書またはNoneを返してください。SQLにidを文字列連結せず、バインドします。
  3. /root/work/fa-conditional-lab/service.pyで、etag(version)は、整数のバージョンを、二重引用符まで含めた文字列で返してください。例: バージョン3は、文字列'"v3"'です。
  4. /root/work/fa-conditional-lab/service.pyで、parse_match(header)は、二重引用符の中がvと正の10進数の整数1つだけのタグだけを受け取って、バージョンを返してください。None、weakタグ、ワイルドカード、リスト、v0などは、ValueErrorです。例: '"v12"' → 12。
  5. /root/work/fa-conditional-lab/service.pyで、update_note(path, note_id, title, version)は、idとversionがどちらも一致したときだけ、タイトルを更新して、バージョンを1上げてください。変更された行が1つならTrue、なければFalseです。同時のリクエストでも、勝者は1つでなければなりません。
  6. /root/work/fa-conditional-lab/service.pyで、put_note(path, note_id, title, header)は、存在しないidには404、条件の欠落には428、対応していない条件・バージョンの不一致には412、成功には204を返してください。拒否するときは、DBを変更しません。
  7. /root/work/fa-conditional-lab/service.pyで、create_app(path)は、GET /notes/{note_id}とPUT /notes/{note_id}を提供してください。GETはメモのJSONとETagを、PUTは{title: 1から100文字の文字列}とIf-Matchを受け取って、上の結果コードで空のレスポンスを返します。空のタイトルは422、存在しないGETは404です。

参考

バージョンを持つストアを作る

/root/work/fa-conditional-lab/service.pyで、init_db(path)は、SQLiteのファイルにnotes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)を作成してください。繰り返し呼び出しても、既存のデータを削除しません。最初の準備は、次のコマンドで行います。

mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab

CREATE TABLE IF NOT EXISTSを使い、with sqlite3.connect(path)でコミットの境界を決めます。

採点は、bash /opt/lab/checks/fa-conditional-lab/01-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

最初の保存のバージョンを固定する

/root/work/fa-conditional-lab/service.pyで、create_note(path, note_id, title)は、バージョン1でメモをINSERTしてコミットしてください。同じidはsqlite3.IntegrityErrorで拒否し、既存のタイトルを上書きしません。

INSERT OR REPLACEは、重複した作成を更新に変えてしまいます。このステップの契約は、新しいメモの作成です。

採点は、bash /opt/lab/checks/fa-conditional-lab/02-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

存在しないメモと存在するメモを区別する

/root/work/fa-conditional-lab/service.pyで、read_note(path, note_id)は、{id, title, version}の辞書またはNoneを返してください。SQLにidを文字列連結せず、バインドします。

fetchoneがNoneかどうかを、先に確認します。タプルのまま返さないでください。

採点は、bash /opt/lab/checks/fa-conditional-lab/03-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

強いETagを作る

/root/work/fa-conditional-lab/service.pyで、etag(version)は、整数のバージョンを、二重引用符まで含めた文字列で返してください。例: バージョン3は、文字列'"v3"'です。

HTTPのタグの二重引用符は、説明用の表記ではなく、実際のヘッダーの値の一部です。

採点は、bash /opt/lab/checks/fa-conditional-lab/04-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

対応している条件の文法だけを受け付ける

/root/work/fa-conditional-lab/service.pyで、parse_match(header)は、二重引用符の中がvと正の10進数の整数1つだけのタグだけを受け取って、バージョンを返してください。None、weakタグ、ワイルドカード、リスト、v0などは、ValueErrorです。例: '"v12"' → 12。

re.fullmatchを使うと、タグのあとに余分な文字列があっても見過ごされません。このラボは、HTTPのタグ全体の文法の実装ではありません。

採点は、bash /opt/lab/checks/fa-conditional-lab/05-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

比較と書き込みを1つのSQLにまとめる

/root/work/fa-conditional-lab/service.pyで、update_note(path, note_id, title, version)は、idとversionがどちらも一致したときだけ、タイトルを更新して、バージョンを1上げてください。変更された行が1つならTrue、なければFalseです。同時のリクエストでも、勝者は1つでなければなりません。

UPDATE ... WHERE id=? AND version=?のrowcountを見ます。SELECTのあとに無条件でUPDATEすると、競合を防げません。

採点は、bash /opt/lab/checks/fa-conditional-lab/06-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

失敗をHTTPの結果に分類する

/root/work/fa-conditional-lab/service.pyで、put_note(path, note_id, title, header)は、存在しないidには404、条件の欠落には428、対応していない条件・バージョンの不一致には412、成功には204を返してください。拒否するときは、DBを変更しません。

存在しないリソースの判定を先に行ってから、条件を解釈します。再取得で、失敗を成功のように変えてはいけません。

採点は、bash /opt/lab/checks/fa-conditional-lab/07-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

古い画面の保存を、HTTPで再現する

/root/work/fa-conditional-lab/service.pyで、create_app(path)は、GET /notes/{note_id}とPUT /notes/{note_id}を提供してください。GETはメモのJSONとETagを、PUTは{title: 1から100文字の文字列}とIf-Matchを受け取って、上の結果コードで空のレスポンスを返します。空のタイトルは422、存在しないGETは404です。

HeaderでIf-Matchを受け取り、TestClientでGETしたあと、同じタグを2回送ってみてください。最初の更新だけが適用され、新しいGETはv2になっている必要があります。

採点は、bash /opt/lab/checks/fa-conditional-lab/08-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。