Flaskで素のSQLAlchemyを使う


【最終更新日】2023-05-06

FlaskアプリでFlask-SQLAlchemyを使わず単独でSQLAlchemyを使う方法を紹介します。

Flask-SQLAlchemyを使ってしまうとデータベース処理の開発・テストがWebアプリ依存のものになってしまい開発効率が低下します。実際のシステムではWebアプリケーション以外にバッチシステムもたくさんあります。

【参考URL】
1.Flaskアプリケーション用プロジェクト
プロジェクトのクラス構成
Healthcare/
├── healthcare
│   ├── __init__.py                // Flaskアプリケーション生成・初期化, データベースセッション取得等
│   ├── dao                        // テーブル定義クラス
│   │   ├── __init__.py            // 健康管理データベースのスキーマ定義
│   │   ├── blood_pressure.py      // (健康) 血圧測定テーブルクラス
│   │   ├── body_temperature.py    // (健康) 体温測定テーブルクラス
│   │   ├── nocturia_factors.py    // (健康) 夜間頻尿要因テーブルクラス
│   │   ├── person.py              // (健康) 個人情報テーブルクラス
│   │   ├── queries.py             // (健康・気象) 登録済みデータ検索クラス
│   │   ├── sleep_management.py    // (健康) 睡眠管理テーブルクラス
│   │   ├── walking_count.py       // (健康) 歩数管理テーブルクラス
│   │   └── weather_condition.py   // (気象) 天候状態テーブルクラス
│   ├── log
│   │   ├── __init__.py
│   │   ├── logconf_main.json
│   │   └── logsetting.py
│   ├── util
│   │   ├── __init__.py
│   │   ├── dateutil.py
│   │   ├── file_util.py
│   │   └── image_util.py
│   └── views
│       ├── __init__.py
│       └── app_main.py             // リクエスト処理メインクラス
├── run.py   // デバックモードでFlask, 本番モードでwaitressを起動するpythonスタートアップスクリプト
└── start.sh // 仮想環境に入りpythonスタートアップスクリプトを実行するシェルスクリプト
(1) Pythonスタートアップスクリプトを実行するシェルスクリプト
#!/bin/bash

# ./start.sh                    -> development             # スクリプト引数がなければ開発環境で起動
# ./start.sh prod | production  ->production               # スクリプト引数が "production" なら本番環境で起動

env_mode="development"
if [ $# -eq 0 ]; then
    :
else
   if [[ "$1" = "prod" || "$1" = "production" ]]; then 
        env_mode="production"
   fi
fi

host_name="$(/bin/cat /etc/hostname)"                          # ■ 1-1 ホスト名取得
IP_HOST_ORG="${host_name}.local"   # ADD host suffix ".local"  # ■ 1-2 Webアプリ用のホスト名決定
export IP_HOST="${IP_HOST_ORG,,}"  # to lowercase              # ■ 2-1 環境変数: Webアプリ用ホスト名 (小文字)
export FLASK_ENV=$env_mode                                     # ■ 2-2 環境変数: FLASK_ENV (Flaskアプリケーション用)
echo "$IP_HOST with $FLASK_ENV"

EXEC_PATH=
if [ -n "$PATH_HEALTHCARE" ]; then
   EXEC_PATH=$PATH_HEALTHCARE                                  # ■ 3-1 開発環境では開発PCの環境変数(PATH_HEALTHCARE)を設定
else
   EXEC_PATH="$HOME/Healthcare"                                # ■ 3-2 本番環境(ラズパイ) /home/pi/Healthcare
fi
echo "$EXEC_PATH"

. $HOME/py_venv/raspi4_apps/bin/activate                       # ■ 4-1 Python仮想環境に入る

python $EXEC_PATH/run.py                                       # ■ 4-2 Pythonスタートアップスクリプト実行

deactivate
2.Flaskアプリケーション初期化クラス
(1) Flask初期化スクリプト ※Healthcare/healthcare/__init__.py が先に実行される
(2) pythonスタートアップスクリプト Healthcare/run.py ※上記(1)実行後に起動される
import os

from healthcare import app, app_logger  # ■ app を参照しているので上記(1)__init__.pyが先に実行される

"""
This module load after app(==__init__.py)
"""

if __name__ == "__main__":
    has_prod = os.environ.get("FLASK_ENV") == "production"              # ■ 1
    # app config SERVER_NAME
    srv_host = app.config["SERVER_NAME"]                                # ■ 2-1 ホスト情報
    srv_hosts = srv_host.split(":")                                     # ■ 2-2 ホスト名とポート番号に分解
    host, port = srv_hosts[0], srv_hosts[1]
    app_logger.info("run.py in host: {}, port: {}".format(host, port))
    if has_prod:
        # Production mode
        try:
            # Prerequisites: pip install waitress
            from waitress import serve

            app_logger.info("Production start.")
            # console log for Reqeust suppress: _quiet=True  
            serve(app, host=host, port=port, _quiet=True)               # ■ 3[A]
        except ImportError:
            # Production with flask,debug False
            app_logger.info("Development start, without debug.")
            app.run(host=host, port=port, debug=False)                  # ■ 3[C]
    else:
        # Development mode
        app_logger.info("Development start, with debug.")
        app.run(host=host, port=port, debug=True)                       # ■ 3[B]
3.リクエスト処理メインクラス
(1) インポート部分
import json
from typing import Dict, Optional, Union

import sqlalchemy
from flask import Response, abort, g, jsonify, make_response, request
from sqlalchemy import Select, select
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session, scoped_session, sessionmaker
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import (BadRequest, Conflict, Forbidden,
                                 HTTPException, InternalServerError, NotFound)

from healthcare import (Cls_sess_healthcare, Cls_sess_sensors,
                        Session_healthcare, app, app_logger, app_logger_debug)
from healthcare.dao.blood_pressure import BloodPressure
from healthcare.dao.body_temperature import BodyTemperature
from healthcare.dao.nocturia_factors import NocturiaFactors
from healthcare.dao.person import Person
from healthcare.dao.queries import Selector
from healthcare.dao.sleep_management import SleepManagement
from healthcare.dao.walking_count import WalkingCount
from healthcare.dao.weather_condition import WeatherCondition
(2) 定数定義
MSG_DESCRIPTION: str = "error_message"
ABORT_DICT_BLANK_MESSAGE: Dict[str, str] = {MSG_DESCRIPTION: ""}
# アプリケーションルートパス
APP_ROOT: str = app.config["APPLICATION_ROOT"]
(3) Webアプリケーション固有の処理関数

データベース接続オブジェクトの取得とクリーンアップ方法 ※上記【参考URL】(2) のサンプルコード参照

from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()

    return g.db

@app.teardown_appcontext
def teardown_db(exception):  # ■ レスポンス返却前に必ず呼び出しされる
    db = g.pop('db', None)

    if db is not None:
        db.close()
(4) データ登録リクエスト処理【リクエストパス】/healthcare/register
@app.route(APP_ROOT + "/register", methods=["POST"])
def register():
    """
    健康管理データ(必須)と天候データ(必須)の登録
    """
    if app_logger_debug:
        app_logger.debug(request.path)

    # 登録データチェック
    personId, emailAddress, measurementDay, data = _check_postdata(request)
    
    # 健康管理データ登録 (必須)
    _insert_healthdata(personId, measurementDay, data)
    # 気象データ登録 (必須) ※気象センサーデータベース
    _insert_weather(measurementDay, data)

    # ここまでエラーがなければOKレスポンス
    return make_register_success(emailAddress, measurementDay)
(5) 登録済みデータ更新リクエスト処理【リクエストパス】/healthcare/update
@app.route(APP_ROOT + "/update", methods=["POST"])
def update():
    """
    健康管理データ(任意)または天候データ(任意)の更新
    """
    if app_logger_debug:
        app_logger.debug(request.path)

    # 更新データチェック
    personId, emailAddress, measurementDay, data = _check_postdata(request)

    # 健康管理データの更新
    _update_healthdata(personId, measurementDay, data)
    # 天候状態(気象データベース)の更新
    _update_weather(measurementDay, data)

    # ここまでエラーがなければOKレスポンス
    return make_register_success(emailAddress, measurementDay)
(6) 登録済みデータ取得リクエスト処理【リクエストパス】/healthcare/getcurrentdata
@app.route(APP_ROOT + "/getcurrentdata", methods=["GET"])
def getcurrentdata():
    """メールアドレスと測定日付から健康管理データを取得するリクエスト

    :param: request parameter: ?emailAddress=user1@examples.com&measurementDay=2023-0-03
    :return: JSON形式(健康管理DBから取得したデータ + 気象センサーDBから取得した天候)
    """
    if app_logger_debug:
        app_logger.debug(request.path)
        app_logger.debug(request.args.to_dict())

    # 1.リクエストパラメータチェック
    # 1-1.メールアドレス
    emailAddress = request.args.get("emailAddress")
    if emailAddress is None:
        abort(BadRequest.code, _set_errormessage("462,Required EmailAddress."))
    # 1-2.測定日付
    measurementDay = request.args.get("measurementDay")
    if emailAddress is None:
        abort(BadRequest.code, _set_errormessage("463,Required MeasurementDay."))

    # PersonテーブルにemailAddressが存在するか
    personId = _get_personid(emailAddress)
    if personId is None:
        abort(BadRequest.code, _set_errormessage("461,User is not found."))

    # 健康管理DBと気象センサーDBからデータ取得する
    sess_healthcare: scoped_session = get_healthcare_session()
    sess_sensors: scoped_session = get_sensors_session()
    selector = Selector(sess_healthcare, sess_sensors, logger=app_logger)
    # 健康管理データ取得
    healthcare_dict: Dict = selector.get_healthcare_asdict(emailAddress, measurementDay)
    if app_logger_debug:
        app_logger.debug(f"Healthcare: {healthcare_dict}")    
    if healthcare_dict:
        healthcare_dict["emailAddress"] = emailAddress
        healthcare_dict["measurementDay"] = measurementDay
        # 天気状態取得
        weather_dict = selector.get_weather_asdict(measurementDay)
        if app_logger_debug:
            app_logger.debug(f"Weather: {weather_dict}")    
        if weather_dict:
            healthcare_dict["weatherData"] = weather_dict
        else:
            # 天候がなければ未設定
            healthcare_dict["weatherData"] = None
        
        return make_getdata_success(healthcare_dict)
    else:
        abort(NotFound.code, _set_errormessage("Data is not found."))
メニューページへ
戻る
サーバー側のFlaskアプリのソースコードはこちら
https://github.com/pipito-yukio/personal_healthcare/tree/main/src/webapp
Pythonバッチアプリのソースコードはこちら
https://github.com/pipito-yukio/personal_healthcare/tree/main/src/batch