本記事は
flaskを使ってRestAPIサーバを作ってみるの「編集」について記述しています。
ここまでの流れはflaskを使ってRestAPIサーバを作ってみるを参照してください。
編集編で説明するコードは feature/chapter2-update
ブランチにあります。
flask_sv プロジェクトのディレクトリ配下で次のコマンドを実行してください。
$ git checkout feature/chapter2-update
4. 編集
次に編集です。
編集も、必要な情報はsearchと変わりませんが、どのAccountを変更するかを指定するためにアカウントIDを指定することにします。
変更が必要な項目のみを受け取り、リクエストにない項目は更新しないことにします。
受信すべきリクエストは次の様になります。
updateのリクエスト
{
"id": 320,
"account_name": "account",
"start_on": "2021-01-01 00:00:00",
"end_on": "2021-12-31 23:59:59",
"opration_account_id": アカウントID,
"created_by" : アカウントID,
"created_at" : "2020-12-31 00:00:00",
"updated_by" : アカウントID,
"updated_at" : "2020-12-31 00:00:00",
"status" : statusの数値
}
4.1. モデル層・エンティティ
コードの全文はこちらです。
def update(account_dict, operation_account_id):
account_id = account_dict.get('id')
Session = sessionmaker(bind=engine, autocommit=False)
res=False
ses = Session()
account_record = ses.query(Account).with_for_update().get(account_id)
message = ""
try:
v = account_dict.get('account_name')
if (v != None):
account_record.account_name=v
v = account_dict.get('start_on')
if (v != None):
account_record.start_on=v
v = account_dict.get('end_on')
if (v != None):
account_record.end_on=v
account_record.updated_by=operation_account_id
account_record.updated_at=strftime(datetime.datetime.now())
v = account_dict.get('status')
if (v != None):
account_record.status=v
ses.add(account_record)
#他のプロセスによるロックを待つ
#time.sleep(1)
ses.commit()
res = True
except Exception as e:
message = str(e)
ses.rollback()
res = False
finally:
ses.close()
return (res, message)
コードの解説をします。
account_idをリクエストから取得します。
def update(account_dict, operation_account_id):
account_id = account_dict.get('id')
受け取ったアカウントIDで検索をします。
Session = sessionmaker(bind=engine, autocommit=False)
res=False
ses = Session()
account_record = ses.query(Account).with_for_update().get(account_id)
message = ""
検索結果で得られたAccountオブジェクトを変更し、セッションにadd関数で戻してやることで変更のリクエストになります。
リクエストに含まれる項目だけ抜き出します。
try:
v = account_dict.get('account_name')
if (v != None):
account_record.account_name=v
v = account_dict.get('start_on')
if (v != None):
account_record.start_on=v
v = account_dict.get('end_on')
if (v != None):
account_record.end_on=v
account_record.updated_by=operation_account_id
account_record.updated_at=strftime(datetime.datetime.now())
v = account_dict.get('status')
if (v != None):
account_record.status=v
必要な項目を入れ替えたaccount_recordオブジェクトをsesに戻します。エラーがなければコミットされます。
例外が発生したらexceptブロックに飛ぶのでrollbackに切り替えられます。また、ここではExceptionが発生した時、messageにエラーメッセージを入れて、結果とメッセージのタプルに入れて返却しています。
ses.add(account_record)
#他のプロセスによるロックを待つ
#time.sleep(1)
ses.commit()
res = True
except Exception as e:
message = str(e)
ses.rollback()
res = False
finally:
ses.close()
return (res, message)
4.2. ドメイン層・ビジネスロジック
コードの全文はこちらです。
def update(account_request, operation_account_id):
"""
/account/updateで呼び出されたAPIの検索処理
Parameters
----------
account_request : json
作成するアカウント詳細
operation_account_id : int
Webアプリケーション操作アカウントのID
Returns
-------
JSON形式の処理結果
正常
異常
"""
account = convertdict(account_request)
try:
res = Account.update(account, operation_account_id)
print(f"AccountApi#update res={res[0]},{res[1]}")
if res[0] == True:
code="I0001"
message="Updated Account Succesfuly."
else:
code="E0001"
message=res[1]
except Exception as e:
code="E0009"
message=f"Updated failed {e}"
result_json = {
"body": "",
"status": {
"code" : code,
"message" : message,
"detail" : ""
}
}
return result_json
上記から呼び出されるconvertdict関数を示します。
この関数はaccountオブジェクトに必要なメンバが含まれたJSONを型を設定してDICT形式に変換します。
def convertdict(from_dict):
print(f"convertdict from_dict={from_dict}")
target_dict = {}
if ('id' in from_dict):
target_dict['id'] = int(from_dict['id'])
if ('account_name' in from_dict):
target_dict['account_name'] = str(from_dict['account_name'])
if ('start_on' in from_dict):
target_dict['start_on'] = strptime(from_dict['start_on'])
if ('end_on' in from_dict):
target_dict['end_on'] = strptime(from_dict['end_on'])
if ('created_by' in from_dict):
target_dict['created_by'] = int(from_dict['created_by'])
if ('created_at' in from_dict):
target_dict['created_at'] = strptime(from_dict['created_at'])
if ('updated_by' in from_dict):
target_dict['updated_by'] = int(from_dict['updated_by'])
if ('updated_at' in from_dict):
target_dict['updated_at'] = strptime(from_dict['updated_at'])
if ('status' in from_dict):
target_dict['status'] = int(from_dict['status'])
return target_dict
コードの解説をします。
リクエストで受け取ったJSONをドメイン層で使うデータそれぞれの型に変換したDICTデータを作ります。
作られたDICTオブジェクトでAccountモデルに対して更新を行います。
account = convertdict(account_request)
try:
res = Account.update(account, operation_account_id)
処理の結果をここではタプルという異なったデータ型を複数保持できるデータ型で受け取ることにします。
このデータ型で (処理の結果:boolean, エラーメッセージ: String) を受け取ることにします。
処理の結果で正常か異常か判断し、異常の時はエラーメッセージからエラーの内容を取得することにします。
if res[0] == True:
code="I0001"
message="Updated Account Succesfuly."
else:
code="E0001"
message=res[1]
更新時以外のエラーが出てもmessageにエラー内容を格納します。
except Exception as e:
code="E0009"
message=f"Updated failed {e}"
返却するJSONを作ります。
result_json = {
"body": "",
"status": {
"code" : code,
"message" : message,
"detail" : ""
}
}
return result_json
4.3. アプリケーション層・フロントへのIF
HTTPリクエストでアクセスされるURLと処理のマッピング、レスポンスをJSON形式で返却する処理を記載してあります。
http://サーバーのIP/account/update
にPOSTでアクセスされたらAccountApi.updateを実行して結果をJSON形式で返却します。
@api_bp.route('/account/update', methods=['POST'])
def updateAccount():
payload = request.json
response_json = AccountApi.update(payload, system_account_id)
return jsonify(response_json)
4.4. テストコードからの実行
テストコード全文
def test_account_update():
"""
"""
account = {
'account_name' : "update_account",
'start_on' : '2021-05-23 00:00:00',
'end_on' : '2030-12-31 00:00:00',
'created_by' : 999,
'created_at' : datetime.datetime.now(),
'updated_by' : 999,
'updated_at' : datetime.datetime.now(),
'status' : Status.getStatusKey("NEW")
}
# create
Account.create(account, 999) == True
search_query = {
"account_name":"update_account",
"start_on":"2021-05-23 00:00:00",
"end_on":"2030-12-31 00:00:00"
}
result = Account.search(search_query, 999)
assert result[0].account_name == account['account_name']
account_id = result[0].id
payload = {
"id": account_id,
"account_name":"update_account_modified",
"start_on":"2021-05-24 10:00:00",
"end_on":"2030-12-31 12:00:00",
"status":"2"
}
# APIから確認
url = f"http://localhost:5000/api/account/update"
headers = {'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'flask_sv/0.0.1',
'Content-type': 'application/json; charset=utf-8',
}
response = requests.post(url, headers=headers, json=payload)
# HTTP Statusコードが200であること
assert response.status_code == 200
data = json.loads(response.text)
assert data['body'] == ""
assert data['status']['code'] == "I0001"
assert data['status']['message'] == "Updated Account Succesfuly."
search_query = {
"account_name":"update_account_modified",
}
result = Account.search(search_query, 999)
assert result[0].start_on.strftime('%Y-%m-%d %H:%M:%S') == payload['start_on'] #.strftime('%Y–%m–%d %H:%M:%S')
assert result[0].end_on.strftime('%Y-%m-%d %H:%M:%S') == payload['end_on'] #.strftime('%Y–%m–%d %H:%M:%S')
assert result[0].created_by == 999
assert result[0].status == 2
コードの解説します。
テストAccountを作ります。
その元になるパラメータを定義し、Accountのcreateで作成します。
def test_account_update():
"""
"""
account = {
'account_name' : "update_account",
'start_on' : '2021-05-23 00:00:00',
'end_on' : '2030-12-31 00:00:00',
'created_by' : 999,
'created_at' : datetime.datetime.now(),
'updated_by' : 999,
'updated_at' : datetime.datetime.now(),
'status' : Status.getStatusKey("NEW")
}
# create
Account.create(account, 999) == True
次に検索をして作成されたAccountのIDを調べます。
search_query = {
"account_name":"update_account",
"start_on":"2021-05-23 00:00:00",
"end_on":"2030-12-31 00:00:00"
}
result = Account.search(search_query, 999)
assert result[0].account_name == account['account_name']
account_id = result[0].id
検索の結果得られたアカウントIDのアカウントに変更する項目のパラメータを定義してAPIからupdateを実行します。
payload = {
"id": account_id,
"account_name":"update_account_modified",
"start_on":"2021-05-24 10:00:00",
"end_on":"2030-12-31 12:00:00",
"status":"2"
}
# APIから確認
url = f"http://localhost:5000/api/account/update"
headers = {'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'flask_sv/0.0.1',
'Content-type': 'application/json; charset=utf-8',
}
response = requests.post(url, headers=headers, json=payload)
レスポンスで返された処理結果を確認します。
# HTTP Statusコードが200であること
assert response.status_code == 200
data = json.loads(response.text)
assert data['body'] == ""
assert data['status']['code'] == "I0001"
assert data['status']['message'] == "Updated Account Succesfuly."
Accountでsearchを実行し、各項目が更新されたことを確認します。
search_query = {
"account_name":"update_account_modified",
}
result = Account.search(search_query, 999)
assert result[0].start_on.strftime('%Y-%m-%d %H:%M:%S') == payload['start_on'] #.strftime('%Y–%m–%d %H:%M:%S')
assert result[0].end_on.strftime('%Y-%m-%d %H:%M:%S') == payload['end_on'] #.strftime('%Y–%m–%d %H:%M:%S')
assert result[0].created_by == 999
assert result[0].status == 2
4.5. 排他編集
上記、4.1. 編集の記述では同時に編集を行った時に片方の編集者のリクエストが正しく処理されず、変更が失われてしまう可能性があります。
どの様な流れかを示します。
編集者A, 編集者Bが同じAccount(account_id=10)を編集すると考えます。
DBへの更新はsubmitするタイミングで行われるので同時に編集していたとしても後の更新(例えば編集者B)によって上書きされてしまいます。そうすると先に更新をかけた編集者Aは更新したつもりでも更新が残らないことになります。
もし、自分が編集者であった場合、項目数の多いフォームに入力したのに変更が失われたら残念に思うでしょう。もし、変更が失われると分かっていれば編集画面に入れない方が親切ではないでしょうか。
通常のシステムでは片方で編集中はメッセージが出て編集画面に入れない様にして、同時に編集することがないように作られます。
4.5.1. モデル層・エンティティ
モデルのコードの変更内容です。
...
Session = sessionmaker(bind=engine, autocommit=False)
...
ses = Session()
account_record = ses.query(Account).get(account_id)
...
scoped_sessionを使うことでsessionを変数として代入できる様になります。
updateの中に埋め込んだので恩恵は受けられませんが、Sessionをどこかに保存しておきwith_for_update()関数でロックをかけます。
...
Session = scoped_session(sessionmaker(bind=engine, autocommit=False))
...
ses = Session()
account_record = ses.query(Account).with_for_update().get(account_id)
...
ロックをかけている間。後続の処理はDBへの更新処理がブロッックされ先に進みません。
課題 4.1
getByIdとは別に検索時にロックを行うAPIを用意して、検索時に稼働中のサーバーのメモリにSessionを保存する。updateを更新時にSessionを取り出して、継続して更新処理とコミットを行うことができるupdateを新規に作成せよ。