お手軽プレゼンス管理、リトライ! その2 (DB編)

前回に引き続き、プレゼンス管理の仕組みを作っていきます。
今回はDBをなんとかします。

イメージ

個々のユーザが検出した位置情報はDBに保存して一元管理できるようにしていきます。
また、数多ある検出されたiBeaconのうち、どのビーコンが位置ビーコンなのか定義する必要があります。 位置ビーコンは追加されたり変更されたりしそうなので、こちらもデータベースを作成した方が良さそうです。
さらに、ユーザ情報も一意でないと困るのでついでにデータベース化しましょう。

なお、DBへのアクセスはSQLクライアントから直接でも良いですが、RESTfulなAPIを作成したいと思います。

f:id:hollyhockberry:20210902164102p:plain

図で示したようにAPIはFastAPI、ユーザ位置情報はInfluxDB、ビーコンとユーザ情報はSQLiteを利用して作成します。

FastAPI

概要

FastAPIによると、

FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。

とのことです。

FastAPIには色々特徴はありますが、SwaggerUIのドキュメントが自動で作られるのが便利でかっこいいです。

インストールとちょっとしたgetting start

まずはFastAPIそのものとASGIのインストールをします。

pip3 install uvicorn fastapi

インストールが終わったら早速動かしてみます。
APIのURLを http://*********/api にしようと思っているので、そのインタフェースで"Hello World"してみましょう。

api.py

from fastapi import FastAPI

app = FastAPI(
  docs_url='/api/docs',
  redoc_url='/api/redoc',
  openapi_url='/api/openapi.json',
)

@app.get("/api")
async def hello():
    return {"message": "Hello World"}

if __name__ == "__main__":
  import uvicorn
  uvicorn.run("api:app", host="0.0.0.0", port=1234, reload=True)

上記のファイルを実行すると、ポート1234で外部に公開されます。

f:id:hollyhockberry:20210831170408p:plain

試しにブラウザで http://*********:1234/api を開くと、 hello() の戻り値で指定した通り ”Hello World"が返ってきています。(私の環境はデフォルト設定なので`http://raspberrypi.local:1234/apiです)

f:id:hollyhockberry:20210831170757p:plain

さらに、 http://*********:1234/api/docs を開くとドキュメントが作成されています。すごい!

f:id:hollyhockberry:20210831171344p:plain

実装

さて、ビーコン情報とユーザ情報のDBとDBへアクセスするAPIを作成していきます。
各テーブルのスキーマはこんな感じ。

ビーコン情報

Field Type Key
ID int Primary
位置 text
UUID text
Major値 int
Minor値 int

ユーザ情報

Field Type Key
ID text Primary
名前 text
詳細 text

ビーコン情報は UUID, Major, Minor の3つでユニークになるのでIDたりえるんですが、面倒なので自動インクリメントのIDをプライマリキーにしています。

これらの情報を読んだり書いたりするCRUDAPIを作るわけですが、 FastAPIのチュートリアル SQL (Relational) Databases - FastAPI がそのまま参考になるのでチュートリアルと異なる点だけ解説します。

ビーコン情報もユーザ情報もデータ形式が違うだけで似たようなAPIを提供するため、ディレクトリを分けて下記のような構造にします。
locationとuserのAPIを分割して実装するわけです。

 +-- App
        +--- api.py
        +--- database
        |      +-- db.py
        +--- location
        |      |- crud.py
        |      |- model.py
        |      |- router.py
        |      |- schemas.py
        +--- user
               |- crud.py
               |- model.py
               |- router.py
               |- schemas.py

分割したAPIチュートリアル Bigger Applications - Multiple Files - FastAPI によるとそれぞれ APIRounter を作成して FastAPIのインスタンスにつなげる事ができるようです。(FlaskのBlueprintみたいな感じですね!)

それぞれのrouter.pyのスケルトンとAPIRounterをFastAPIに繋げるスクリプトは下記の通り

location/router.py

from fastapi import APIRouter

router = APIRouter(tags=['locations'])

@router.get('/')
async def read_all():
  pass

@router.get('/{id}')
async def read(id: int):
  pass

@router.post('/')
async def create():
  pass

@router.put('/{id}')
async def update(id: int):
  pass

@router.delete('/{id}')
async def delete(id: int):
  pass

user/router.py

from fastapi import APIRouter

router = APIRouter(tags=['users'])

@router.get('/')
async def read_all():
  pass

@router.get('/{id}')
async def read(id: str):
  pass

@router.post('/{id}')
async def create(id: str):
  pass

@router.put('/{id}')
async def update(id: str):
  pass

@router.delete('/{id}')
async def delete(id: str):
  pass

api.py

from fastapi import FastAPI
from user.router import router as users
from location.router import router as locations

tags_metadata = [
  {
    'name': 'users',
    'description': 'CRUD Interfaces for database of users.',
  },
  {
    'name': 'locations',
    'description': 'CRUD Interfaces for database of location identificator.',
  }
]

app = FastAPI(
  docs_url='/api/docs',
  redoc_url='/api/redoc',
  openapi_url='/api/openapi.json',
  openapi_tags=tags_metadata,
)

app.include_router(locations, prefix='/api/location')
app.include_router(users, prefix='/api/user')

if __name__ == "__main__":
  import uvicorn
  uvicorn.run("api:app", host="0.0.0.0", port=1234, reload=True)

実行してみると、想定通り /api/location と /api/user のドキュメントができています。FastAPIにRouterがきちんとつながっているようです。

f:id:hollyhockberry:20210831182034p:plain

あとはチュートリアルを参考にAPIの中身を作っていくだけです。

データモデルとスキーマを先に示した構造に合わせて変更したスクリプトはこちらになります。

github.com

チュートリアルと同様、ORMにsqlalchemyを利用していますので、入ってない場合はインストールします。

pip3 install sqlalchemy

実行は先ほどと同様 api.py を実行します。
ちゃんと動いてるかSwaggerUIから確認してみましょう。
/api/user/{id} のPOSTメソッドを開いて Try it out をクリックするとSwaggerUIからAPIを叩くことができます。

試しに、 id=”user01" name="ichigo" description="starlight school" でユーザを作成してみます。

f:id:hollyhockberry:20210831183039p:plain

Execute すると Code 204 で成功しました。

f:id:hollyhockberry:20210831183346p:plain

ちゃんと保存されているかも確認してみます。

f:id:hollyhockberry:20210831184314p:plain

ユーザのリストが返ってくるので追加されてますね。

念の為SQLiteCLIでも確認してみます。

f:id:hollyhockberry:20210831184855p:plain

ちゃんとスターライト学園のいちごさんが登録されてます。よかった。

InfluxDB

続いて ユーザの最新位置情報を保存するDBを準備します。

こちらはSQLiteではなくInfluxDBを利用します。

www.influxdata.com

InfluxDBは時系列DBと呼ばれるタイプのDBで、その名の通り時刻情報を主キーとした構造となっています。
ログなど時系列に沿って蓄積されるデータを扱うためのDBなので、ユーザの最新位置を記録するDBとしてはもってこいなDBです。 さらに、データの保存期間(Retention Policy) を備えているので、今回のように昨日以前のデータが無用の場合わざわざクエリを発行しなくても勝手に消えてくれます。

何はともあれインストールです。

Install InfluxDB OSS | InfluxDB OSS 1.8 Documentation の通りだとなんだかうまくいかなかったので、下記のコマンドでインストールします。

curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add -
echo "deb https://repos.influxdata.com/debian buster stable" | sudo tee /etc/apt/sources.list.d/influxdb.list
sudo apt update
sudo apt install influxdb

うまくインストールできていればsystemctrl statusで active となっているはずです。

f:id:hollyhockberry:20210831212240p:plain

CLIでInfluxDBに接続してデータベースを作成してみます。

f:id:hollyhockberry:20210831215152p:plain

test という名前のデータベースを作成してみました。

また、RETENTION POLCYも設定してみます。
テスト用なので保存期間を1時間としてデフォルトのポリシーに設定します。後ほど行う登録のテスト後、1時間経ったらデータが消えていれば成功です。

CREATE RETENTION POLICY d1h ON test DURATION 1h REPLICATION 1 default

試しにデータを登録してみましょう。 試しとはいえ、本番と同じ形が望ましいのでデータ構造を決めます。 蓄積するデータはユーザ毎にあり、検出した位置ビーコンを特定する値をポストしたいのでUUIDとMajor,Minor値がフィールドとなります。

ということで key は id、Fieldはuuid, major, minor からなるデータを登録してみます。

InfluxDBはHTTPによるAPIが用意されているのでそちらを使ってみます。

curl -X POST 'http://localhost:8086/write?db=test' --data-binary 'sample,id=user01 uuid="xxxx",major=1,minor=2'

先ほど作成したDB、testのMeasurement "sample"に対して user01の位置情報を登録しています。

きちんと登録されているかCLIで確認します。

SHOW MEASUREMENTSをすると先ほど登録した"sample"ができています。

f:id:hollyhockberry:20210831221148p:plain

Measurement "sample" からSELECT * するクエリを投げてみると1項目追加されているようです。

f:id:hollyhockberry:20210831221352p:plain

InfluxDBはHTTPのAPIが用意されているのでこれ以上何かを準備しなくても良さそうです。 これでDB関連は準備完了ですかね?

思った以上に長文になったので今回はここまで。次回に続きます!