【Python特化】おすすめのオンラインプログラミングスクール

【Flask】Webアプリ開発におけるModelの役割

flask-mvt-model
本記事の要点
  • FlaskにおけるModelの役割を理解する
  • database(データベース)を理解する
  • 具体的にModelに記述するコードを理解する

上記をまとめ、具体的なコードやデータベースの基本知識が習得できます。

目次

FlaskにおけるModelの役割とは

Flaskは、UIを持つアプリを実装するデザインパターンとしてMVTモデル(Model/View/Template)を採用してます。

Model/View/Templateは以下の役割を持ちます。

MVTの役割
  • Model:ロジックを担当
  • View:入力を受け取りModelとTemplateを制御
  • Template:入出力を担当

一般的に、MVCモデル(Model/View/Controller)が有名ですが、MVTのViewはMVCのControllerに相当し、MVTのTemplateはMVCのViewに相当します。

具体的なルーティングとして図解を記載しておきます。

flask-mvt-model
flask-mvt-model

また、FlaskにおいてModelは以下の役割を持ちます。

Modelの具体的な役割
  • データベースを作成する
  • データベースを操作する

つまり、Modelはデータベースのテーブルを定義することになります。

データベースを構築する際に必要な知識が以下の内容になります。

Modelに必要な基本知識
  • flask-sqlalchemy
  • flask-migrate
  • SQLite

本記事は、規模の小さいアプリを想定しSQLiteを選択しています。

FlaskにおけるViewの役割

FlaskにおいてViewは以下の役割を持ちます。

Viewの具体的な役割
  • データベースを操作する
  • PRGに基づいてルーティングとテンプレートを制御する

つまり、ViewはModelとTemplateを制御することになります。

Viewを実装する際に必要な知識が以下の内容になります。

Viewに必要な基本知識
  • CRUD機能の理解
  • ルーティングの理解
  • PRG(Post/Redirect/Get)パターン
  • render_template
  • redirect
  • url_for
  • request

Viewの作成方法やデータベース/テンプレート制御を具体的に知りたい人は、「【Flask】Webアプリ開発におけるViewの役割」を一読ください。

FlaskにおけるTemplateの役割

FlaskにおいてTemplateは以下の役割を持ちます。

Templateの具体的な役割
  • ViewからTemplateへのデータの受け渡し
  • ルーティング情報に沿ったテンプレート作成(各ページ)
  • <a>タグ, <form>タグ, <button>タグが持つルートデザイン設計
  • CSSによるUI/UXを意識したデザイン調整

つまり、Templateはユーザー行動を意識したデザインを考慮することになります。

Templateを実装する際に必要な知識が以下の内容になります。

Templateの具体的な役割
  • ルーティングの理解
  • テンプレートエンジン(jinja2)の理解
  • 共通テンプレートと継承
  • データ出力のロジック設計

Templateの作成方法や共通化/継承など具体的に知りたい人は、「【Flask】Webアプリ開発におけるTemplateの役割」を一読ください。

flask-sqlalchemyとは

SQLAlchemyは、Python製のO/Rマッパー(Object Relational Mapping)です。

O/Rマッパーは、データベースとプログラミング言語間(Python)の非互換なデータを変換します。

そのため、SQLAlchemyを利用するとSQLを記述せずPythonコードでデータベースを操作できるメリットがあります。

flask-migrateとは

Migrateは、データベースをマイグレーション(データ移行)する機能です。

マイグレーションは、コード情報をもとにデータベースのテーブル作成やカラム変更などを行います。

コード情報からSQLが発行され、継続的にデータベースの更新や更新前の状態に戻すロールバックが可能です。

SQLiteとは

SQLiteは、軽量でコンパクトなオープンソースのデータベースです。

データベースサーバーとしてではなく、アプリに組み込んで利用します。

そのため、サーバーが不要でローカルにデータをファイルとして保存できます。

MVTモデルを考慮したディレクトリ構成

本記事では、サンプルとしてFlaskによるMVTモデルに沿ったToDoアプリを想定します。

Flaskは自由度が高いため、様々なディレクトリ構成を組めますが一例として参考にしてください。

flask-project
├── .env
├── apps
│   ├── app.py
│   ├── static
│   │   └── bootstrap.min.css
│   └── todo
│       ├── __init__.py
│       ├── forms.py
│       ├── models.py
│       ├── static
│       │   └── style.css
│       ├── templates
│       │   └── todo
│       │       ├── base.html
│       │       ├── index.html
│       │       ├── todo_create.html
│       │       ├── todo_edit.html
│       │       ├── todo_done.html
│       │       └── todo_delete.html
│       └── views.py
├── local.sqlite
└── migrations

様々なディレクトリと.py, .html, .css等のファイルがありますが、app.py, models.py, __init__.pyを中心に解説します。

データベースを作成する

データベースを操作するために、データベースを作成していきます。

ここでは以下のFlask拡張機能を利用します。

Flaskの拡張機能
  • flask-sqlalchemy
  • flask-migrate

Flaskでは、sqlalchemyとmigrateを利用できるよう拡張機能がすでに用意されています。

flask-sqlalchemyのインストール

flask-sqlalchemyを利用するためには、インストールする必要があります。

pip install flask-sqlalchemy

上記のコマンドを実行することでflask-sqlalchemyをインストールできます。

flask-migrateのインストール

次に、データベースのマイグレーション機能を持つflask-migrateをインストールします。

pip install flask-migrate

上記のコマンドを実行することでflask-migrateをインストールできます。

flask-sqlalchemyとflask-migrateの利用準備

拡張機能がインストールできたら、flask-sqlalchemyとflask-migrateの利用準備を行います。

また、上述した通り本プログラムではSQLiteを想定したコードになります。

from pathlib import Path
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

# SQLAlchemyをインスタンス化する
db = SQLAlchemy()

# create_app関数を作成
def create_app():
    # Flask インスタンス生成
    app = Flask(__name__)
    # アプリのコンフィグ設定
    app.config.from_mapping(
        SECRET_KEY = "SECRET_KEY",
        SQLALCHEMY_DATABASE_URI = f"sqlite:///{Path(__file__).parent.parent/'local.sqlite'}",
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    )

    # SQLAlchemyとアプリ連携
    db.init_app(app)
    # Migrateとアプリ連携
    Migrate(app, db)

#...省略...
利用準備の流れ
  1. Pathのインポート
  2. Flaskのインポート
  3. SQLAlchemyのインポート
  4. Migrateのインポート
  5. SQLAlchemyのインスタンス化
  6. Flask(app)のインスタンス化
  7. アプリのコンフィグ設定
  8. SQLAlchemyとappを連携する
  9. Migrateとapp, db(SQLAlchemy)を連携する

インポート部分は利用するために問題なく理解できると思います。

コンフィグ設定におけるSQLALCHEMY_DATABASE_URIは、SQLiteのデータベースを出力するパスを指定しています。

flask-sqlalchemyは様々なデータベースをサポートしており、SQLALCHEMY_DATABASE_URIに他のデータベースURIを指定し変更できます。

スクロールできます
MySQLmysql://username:password@hostname/database
PostgreSQLpostgresql://username:password@hostname/database
SQLite(Linux, macOS)sqlite:////absolute/path/to/database
SQLite(Windows)sqlite:///c:absolute/path/to/database
SQLALCHEMY_DATABASE_URIの代表的なデータソース一覧

また、SQLALCHEMY_TRACK_MODIFICATIONSは、警告が出るためFalseにしています。

SQLAlchemyのConfiguration Keysを詳しく知りたい人は以下のドキュメントを参考にしてください。

最終的にSQLAlchemyをappと連携し、Migrateをappとdbに連携します。

また、Migrateに連携することでflask dbコマンドを使用することができます。

flask dbコマンドについては後述します。

データベースを操作する

利用準備ができたら、データベースを操作するために以下の項目を実施します。

データベースを操作するまでの流れ
  • modelを定義する(テーブル作成)
  • modelの宣言
  • データベースの初期化とマイグレーション

modelを定義する(テーブル作成)

SQLAlchemyは、Modelを定義しマイグレートしてデータベースにテーブルを作成します。

本ディレクトリ構成では、flask-project/apps/todo/models.pyを作成しModelを定義します。

from apps.app import db

# db.Modelを継承したTasksクラスを作成
class Tasks(db.Model):
    # テーブル名を指定
    __tablename__ = "tasks"
    # カラムを定義
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String)
    content = db.Column(db.String)
    # 0 or False:未完了, 1 or True:完了
    status = db.Column(db.Boolean, default=False)
    # 0 or False:何もない, 1 or True:ゴミ箱
    delete = db.Column(db.Boolean, default=False)
Modelによるテーブル作成の流れ
  1. SQLAlchemyをインスタンス化したdbをインポート
  2. db.Modelを継承したクラスを作成
  3. テーブル名の指定
  4. カラムの定義

今回ToDoアプリ開発を想定した際のTasksクラスですが、カラム内容の言及は記事の目的と異なるため行いません。

以下は、Modelの代表的なカラム定義になります。

スクロールできます
SQLAlchemyのカラムタイプDBカラム説明
BooleanTINYINT最小値-128, 最大値127の整数(符号付きの場合)
SmallIntegerTINYINT最小値-32768, 最大値32767の整数(符号付きの場合)
IntegerINT最小値-2147483648, 最大値2147483647の整数(符号付きの場合)
BigIntegerBIGINT最小値-9223372036854775808, 最大値9223372036854775807の整数(符号付きの場合)
FloatFLOAT浮動小数点
NumericDECIMAL10進数
TextTEXTテキスト
DateDATE日付
TimeTIME時刻
DateTimeDATETIME日付と時刻
TimeStampTIMESTAMP日付と時刻(日付に00を許容しない)
Modelの代表的なカラム定義

また、カラムの代表的なオプションは以下になります。

スクロールできます
SQLAlchemyオプション説明
primary_keyプライマリーキー
uniqueユニークキー
indexインデックス
nullableNULL許可
defaultデフォルト値設定
カラムの代表的なオプション

modelの宣言

Modelとテーブルの作成ができたら、flask-project/apps/todo/__init__.pyにmodels.pyをインポートします。

import apps.todo.models

上記のコードで無事Modelの宣言ができました。

__init__.pyとは

__init__.pyには、2つの役割があります。

__init__.pyの役割
  • 階層のモジュールを検索するマーカーの役割
  • パッケージの初期化処理の役割

仕組みの詳細は、PEP420とPython公式ドキュメント「名前空間パッケージ」を一読ください。

データベースの初期化とマイグレーション

次に、データベースの初期化とマイグレーションファイルを作成します。

マイグレーションファイルは、データベースの設計書のようなものです。

マイグレーションファイルを実行することで記述してきた内容がデータベースに反映されます。

データベースの初期化とマイグレーションを実施するflask dbコマンドは以下になります。

flask dbコマンド
  • flask db init
  • flask db migrate
  • flask db upgrade
  • flask db downgrade

以下のコマンド例は環境変数FLASK_APPを適切に設定している場合になります。

flask db init

flask db initコマンドは、データベースを初期化します。

ここでは、flask db initコマンドを実行するとappsディレクトリ配下にmigrationsディレクトリが作成されます。

(venv) PS C:\Users\sugir\Documents\flask-project> flask db init
Creating directory 'C:\\Users\\sugir\\Documents\\flask-project\\migrations' ...  done
Creating directory 'C:\\Users\\sugir\\Documents\\flask-project\\migrations\\versions' ...  done
Generating C:\Users\sugir\Documents\flask-project\migrations\alembic.ini ...  done
Generating C:\Users\sugir\Documents\flask-project\migrations\env.py ...  done
Generating C:\Users\sugir\Documents\flask-project\migrations\README ...  done
Generating C:\Users\sugir\Documents\flask-project\migrations\script.py.mako ...  done
Please edit configuration/connection/logging settings in 'C:\\Users\\sugir\\Documents\\flask-project\\migrations\\alembic.ini' before proceeding.

flask db migrate

flask db migrateコマンドは、データベースのマイグレーションファイルを作成します。

flask db migrateコマンドを実行すると、Model定義をもとにmigrations/versions配下にPythonファイルでデータベースに適用する前の情報が生成されます。

(venv) PS C:\Users\sugir\Documents\flask-project> flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'tasks'
Generating C:\Users\sugir\Documents\flask-project\migrations\versions\41235d6da4ec_.py ...  done

flask db upgrade

flask db upgradeコマンドは、マイグレーション情報を実際にデータベースに反映します。

ここでは、flask db upgradeコマンドを実行するとtasksテーブルが生成されます。

(venv) PS C:\Users\sugir\Documents\flask-project> flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 41235d6da4ec, empty message

flask db downgrade

flask db downgradeコマンドは、マイグレートしたデータベースを適用前の状態に戻せます。

(venv) PS C:\Users\sugir\Documents\flask-project> flask db downgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade 41235d6da4ec -> , empty message

VSCodeでデータベースを確認する

VSCodeでデータベースの内容を確認できるように設定する方法です。

VSCodeの左サイドバーにある拡張機能アイコンをクリックし、検索バーで「SQLite」を検索します。

作成されたデータベースファイル(ここではlocal.sqlite)を右クリックし、「Open Database」を選択すると「SQLITE EXPLORER」が左下に表示されます。

データベースの作成と操作が実行できるようになりました。

本アプリのModel以外は割愛しましたが、以下の画像のようにデータベースを利用できていること分かると思います。

SQLAlchemyの使い方|基本的なデータ操作

SQLAlchemyを利用してSQLを実行するには、大別するとquery filterとexecuterの2種類があります。

スクロールできます
query filter主に検索条件を絞り込んだり並べ替えに利用する
executerSQL実行結果を取得するために利用する
SQLAlchemyの基本的なデータ操作

以下は、代表的なSQLAlchemyのquery filterになります。

スクロールできます
関数説明
filter()複雑な条件を指定する
filter_by()WHERE句、レコードの取得条件を指定
limit()LIMIT句、レコードの取得条件の上限を指定
offset()OFFSET句、レコードの取得行数の開始位置を指定
order_by()ORDER BY句、レコードのソートを指定
group_by()GROUP BY句、レコードのグループ化を指定
代表的なSQLAlchemyのquery filter

また、以下は代表的なSQLAlchemyのexecuterになります。

スクロールできます
メソッド説明
all()レコードを全件取得する
first()レコードを1件取得する
first_or_404()1件またはレコードがなければ404を返す
get()プライマリーキーを指定してレコードを取得する
get_or_404()プライマリーキーを指定してレコードを取得できなければ404を返す
count()レコードの件数を取得する
paginate()ページングでレコードを取得する
代表的なSQLAlchemyのexecuter

SQLAlchemyの使い方|リレーションシップ

Model(モデル)にrelationship(リレーションシップ)を指定することで、モデルオブジェクトから関連するテーブルのオブジェクトを取得できます。

ここでは、モデル例としてusersテーブルとuser_imagesテーブルの関係があると仮定します。

usersテーブルとuser_imagesテーブル

一例として、以下のディレクトリ構成があると仮定します。

flask-project
├── .env
├── apps
│   ├── app.py
│   ├── config.py
│   ├── static
│   │   └── bootstrap.min.css
│   ├── auth
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   ├── models.py
│   │   ├── static/css
│   │   │   └── style.css
│   │   ├── templates
│   │   │   └── auth
│   │   │       ├── base.html
│   │   │       ├── index.html
│   │   │       ├── login.html
│   │   │       └── signup.html
│   │   └── views.py
│   ├── image
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   ├── models.py
│   │   ├── static/css
│   │   │   └── style.css
│   │   ├── templates
│   │   │   └── image
│   │   │       ├── base.html
│   │   │       ├── index.html
│   │   │       └── upload.html
│   │   └── views.py
|   └── images
├── local.sqlite
└── migrations

ログイン認証アプリと画像一覧表示アプリを結合しましたが、作り方や詳細な解説が気になる人は以下の記事を一読ください。

また、リンク記事からuserテーブルとuser_imagesテーブルとして作成したModel定義の一部を以下に記載します。

# db.Modelを継承したUserクラス作成
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, index=True)
    email = db.Column(db.String, unique=True, index=True)
    password_hash = db.Column(db.String)

Userモデルは、ユーザー管理を行うモデルになります。

# db.Modelを継承したUserImageクラス作成
class UserImage(db.Model):
    __tablename__ = "user_images"
    id = db.Column(db.Integer, primary_key=True)
    image_path = db.Column(db.String)

UserImageモデルは、ログインユーザーが画像をアップロードした際の画像URLを保存するモデルです。

ER図は、IE表記で記載しています。

ER図が示した通り、usersテーブルのidをuser_imagesテーブルのuser_idとしてリレーションします。

1ユーザーは何度でも画像アップロードできるため、1対多の関係になります。

UserImageモデルのuser_idは、usersテーブルのidカラムを外部キーとして設定します。

外部キーを設定するには、db.ForeignKeyに"テーブル名.カラム名"を指定します。

# db.Modelを継承したUserImageクラス作成
class UserImage(db.Model):
    __tablename__ = "user_images"
    id = db.Column(db.Integer, primary_key=True)
    # user_idはusersテーブルのidカラムを外部キーとして設定
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
    image_path = db.Column(db.String)

次に、usersテーブルにリレーションを定義します。

# db.Modelを継承したUserクラス作成
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, index=True)
    email = db.Column(db.String, unique=True, index=True)
    password_hash = db.Column(db.String)
    # backrefを利用してrelation情報を設定
    user_images = db.relationship("UserImage", backref="users")

リレーションシップには様々なオプションがあるため、主なオプションを記載します。

スクロールできます
オプション名説明
backref別モデルに対して双方向のリレーションを張る
lazy関連オブジェクトを遅延して取得するオプション。
デフォルトはselectで他オプションはimmediate, joined, subquery, dynamicなどがある
order_byソートするカラムを指定する
リレーションシップの主なオプション

詳しい内容は、以下のSQLAlchemy公式ドキュメントを一読ください。

backrefによるオブジェクトの取得方法

backrefを利用すると、上記例からUserモデルからUserImageモデル、あるいはUserImageモデルからUserモデルへオブジェクトを使いアクセスできます。

ここでは、テーブルの関係として1対1、1対多、多対1に対して有効ですが、多対多のテーブルに対して利用できません。

本記事では取り上げていませんが、多対多のテーブルを扱う場合はsecondaryやsecondaryjoinを利用します。

以下は、上記例で記載したUserモデルとUserImageモデルでオブジェクトの取得方法を解説します。

各モデルからのオブジェクトの取得方法
  • UserオブジェクトからUserImageオブジェクトを取得する
  • UserImageオブジェクトからUserオブジェクトを取得する

UserオブジェクトからUserImageオブジェクトを取得する

UserオブジェクトからUserImageオブジェクトを取得する場合、user.user_imagesと記述します。

このコードから、Userオブジェクトのuser_idを持つUserImage情報が取得できます。

backrefのデフォルトのオプションはselectになっています。

そのため、user.user_imagesが呼び出された際に一度だけSQLが実行されます。

1対多の関係のため、user.user_imagesで取得できる値はUserImageオブジェクトのリストになります。

flask shellコマンドにて、オブジェクトがどのように取得できているか確認します。

ここでは、テーブル内に以下のレコードが格納した状態で実行しています。

スクロールできます
idusernameemailpassword_hash
1jobcodejobcode.ex@gmail.com…省略…
usersテーブル
スクロールできます
iduser_idimage_path
11…省略…
21…省略…
31…省略…
41…省略…
user_imagesテーブル
(venv) PS C:\Users\sugir\Documents\flask-project> flask shell
Python 3.12.3 (tags/v3.12.3:f6650f9, Apr  9 2024, 14:05:25) [MSC v.1938 64 bit (AMD64)] on win32
App: apps.app
Instance: C:\Users\sugir\Documents\flask-project\instance
>>> from apps.app import db
>>> from apps.auth.models import User
>>> from apps.image.models import UserImage
>>> user = User.query.first()
>>> print(user.user_images)
[<UserImage 1>, <UserImage 2>, <UserImage 3>, <UserImage 4>]

上記のコードから、取得したユーザーにリレーションを張ったUserImageオブジェクト4つがリストとして取得できていることが分かります。

UserImageオブジェクトからUserオブジェクトを取得する

UserImageオブジェクトからUserオブジェクトを取得するには、user_images.userと記述します。

このコードから、UserImageオブジェクトのuser_idを持つUser情報を取得できます。

多対1の関係のため、Userはリストではなく1ユーザーのオブジェクトになります。

また、user_imagesテーブルにusersテーブルのuser_idを保持するレコードが存在する必要があります。

flask shellコマンドにて、オブジェクトがどのように取得できているか確認します。

(venv) PS C:\Users\sugir\Documents\flask-project> flask shell
Python 3.12.3 (tags/v3.12.3:f6650f9, Apr  9 2024, 14:05:25) [MSC v.1938 64 bit (AMD64)] on win32
App: apps.app
Instance: C:\Users\sugir\Documents\flask-project\instance
>>> from apps.app import db
>>> from apps.auth.models import User
>>> from apps.image.models import UserImage
>>> user_image = UserImage.query.first()
>>> print(user_image.users)
<User 1>

特定のUserImageオブジェクトのuserオブジェクトを取得できていることが分かります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

sugiのアバター sugi SUGI

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code運営中

コメント

コメントする

目次