- FlaskにおけるModelの役割を理解する
- database(データベース)を理解する
- 具体的にModelに記述するコードを理解する
上記をまとめ、具体的なコードやデータベースの基本知識が習得できます。
FlaskにおけるModelの役割とは
Flaskは、UIを持つアプリを実装するデザインパターンとしてMVTモデル(Model/View/Template)を採用してます。
Model/View/Templateは以下の役割を持ちます。
- Model:ロジックを担当
- View:入力を受け取りModelとTemplateを制御
- Template:入出力を担当
一般的に、MVCモデル(Model/View/Controller)が有名ですが、MVTのViewはMVCのControllerに相当し、MVTのTemplateはMVCのViewに相当します。
具体的なルーティングとして図解を記載しておきます。
また、FlaskにおいてModelは以下の役割を持ちます。
- データベースを作成する
- データベースを操作する
つまり、Modelはデータベースのテーブルを定義することになります。
データベースを構築する際に必要な知識が以下の内容になります。
- flask-sqlalchemy
- flask-migrate
- SQLite
FlaskにおけるViewの役割
FlaskにおいてViewは以下の役割を持ちます。
- データベースを操作する
- PRGに基づいてルーティングとテンプレートを制御する
つまり、ViewはModelとTemplateを制御することになります。
Viewを実装する際に必要な知識が以下の内容になります。
- CRUD機能の理解
- ルーティングの理解
- PRG(Post/Redirect/Get)パターン
- render_template
- redirect
- url_for
- request
Viewの作成方法やデータベース/テンプレート制御を具体的に知りたい人は、「【Flask】Webアプリ開発におけるViewの役割」を一読ください。
FlaskにおけるTemplateの役割
FlaskにおいてTemplateは以下の役割を持ちます。
- ViewからTemplateへのデータの受け渡し
- ルーティング情報に沿ったテンプレート作成(各ページ)
- <a>タグ, <form>タグ, <button>タグが持つルートデザイン設計
- CSSによるUI/UXを意識したデザイン調整
つまり、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-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)
#...省略...
- Pathのインポート
- Flaskのインポート
- SQLAlchemyのインポート
- Migrateのインポート
- SQLAlchemyのインスタンス化
- Flask(app)のインスタンス化
- アプリのコンフィグ設定
- SQLAlchemyとappを連携する
- Migrateとapp, db(SQLAlchemy)を連携する
インポート部分は利用するために問題なく理解できると思います。
コンフィグ設定におけるSQLALCHEMY_DATABASE_URIは、SQLiteのデータベースを出力するパスを指定しています。
flask-sqlalchemyは様々なデータベースをサポートしており、SQLALCHEMY_DATABASE_URIに他のデータベースURIを指定し変更できます。
MySQL | mysql://username:password@hostname/database |
PostgreSQL | postgresql://username:password@hostname/database |
SQLite(Linux, macOS) | sqlite:////absolute/path/to/database |
SQLite(Windows) | sqlite:///c:absolute/path/to/database |
また、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)
- SQLAlchemyをインスタンス化したdbをインポート
- db.Modelを継承したクラスを作成
- テーブル名の指定
- カラムの定義
今回ToDoアプリ開発を想定した際のTasksクラスですが、カラム内容の言及は記事の目的と異なるため行いません。
以下は、Modelの代表的なカラム定義になります。
SQLAlchemyのカラムタイプ | DBカラム | 説明 |
---|---|---|
Boolean | TINYINT | 最小値-128, 最大値127の整数(符号付きの場合) |
SmallInteger | TINYINT | 最小値-32768, 最大値32767の整数(符号付きの場合) |
Integer | INT | 最小値-2147483648, 最大値2147483647の整数(符号付きの場合) |
BigInteger | BIGINT | 最小値-9223372036854775808, 最大値9223372036854775807の整数(符号付きの場合) |
Float | FLOAT | 浮動小数点 |
Numeric | DECIMAL | 10進数 |
Text | TEXT | テキスト |
Date | DATE | 日付 |
Time | TIME | 時刻 |
DateTime | DATETIME | 日付と時刻 |
TimeStamp | TIMESTAMP | 日付と時刻(日付に00を許容しない) |
また、カラムの代表的なオプションは以下になります。
SQLAlchemyオプション | 説明 |
---|---|
primary_key | プライマリーキー |
unique | ユニークキー |
index | インデックス |
nullable | NULL許可 |
default | デフォルト値設定 |
modelの宣言
Modelとテーブルの作成ができたら、flask-project/apps/todo/__init__.pyにmodels.pyをインポートします。
import apps.todo.models
上記のコードで無事Modelの宣言ができました。
__init__.pyとは
__init__.pyには、2つの役割があります。
- 階層のモジュールを検索するマーカーの役割
- パッケージの初期化処理の役割
仕組みの詳細は、PEP420とPython公式ドキュメント「名前空間パッケージ」を一読ください。
データベースの初期化とマイグレーション
次に、データベースの初期化とマイグレーションファイルを作成します。
マイグレーションファイルは、データベースの設計書のようなものです。
マイグレーションファイルを実行することで記述してきた内容がデータベースに反映されます。
データベースの初期化とマイグレーションを実施するflask dbコマンドは以下になります。
- flask db init
- flask db migrate
- flask db upgrade
- flask db downgrade
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 | 主に検索条件を絞り込んだり並べ替えに利用する |
executer | SQL実行結果を取得するために利用する |
以下は、代表的なSQLAlchemyのquery filterになります。
関数 | 説明 |
---|---|
filter() | 複雑な条件を指定する |
filter_by() | WHERE句、レコードの取得条件を指定 |
limit() | LIMIT句、レコードの取得条件の上限を指定 |
offset() | OFFSET句、レコードの取得行数の開始位置を指定 |
order_by() | ORDER BY句、レコードのソートを指定 |
group_by() | GROUP BY句、レコードのグループ化を指定 |
また、以下は代表的なSQLAlchemyのexecuterになります。
メソッド | 説明 |
---|---|
all() | レコードを全件取得する |
first() | レコードを1件取得する |
first_or_404() | 1件またはレコードがなければ404を返す |
get() | プライマリーキーを指定してレコードを取得する |
get_or_404() | プライマリーキーを指定してレコードを取得できなければ404を返す |
count() | レコードの件数を取得する |
paginate() | ページングでレコードを取得する |
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図が示した通り、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コマンドにて、オブジェクトがどのように取得できているか確認します。
ここでは、テーブル内に以下のレコードが格納した状態で実行しています。
id | username | password_hash | |
---|---|---|---|
1 | jobcode | jobcode.ex@gmail.com | …省略… |
id | user_id | image_path |
---|---|---|
1 | 1 | …省略… |
2 | 1 | …省略… |
3 | 1 | …省略… |
4 | 1 | …省略… |
(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オブジェクトを取得できていることが分かります。
コメント