flask-sqlalchemy/flask-migrate/mysql-connector-pythonでMySQL連携を実装する
はじめに
PythonのWebアプリケーションフレームワークFlaskを使って、「消耗品買い物リスト」を作ってみるシリーズ第10回目です。
今回はMySQLとの連携をした上でのページ作成/表示機能を作ります。
前回まで
- 0回 : Flaskで簡単なWebサービスを作ってみる
- 1回 : スプレッドシート+Google Apps Scriptでプロトタイプを作る
- 2回 : FigmaでUI制作をしてみたが早々に次のフェーズに進んだ件
- 3回 : DockerでFlaskが動き、簡単なテストが通る状態を作る
- 4回 : [スクショ付き]AWSにVPCを構築し、EC2を立てる
- 5回 : EC2でDockerコンテナを起動し、Flaskを動かす
- 6回 : CircleCIでFlaskアプリの自動テストができるようにする
- 7回 : CircleCIでIP制限のあるEC2インスタンスに自動デプロイできるようにする
- 8回 : ユーザーストーリーマッピングで初期開発機能を洗い出す
- 9回 : [Flask]uuidを生成して専用URLを作成する
今回やること
前回までで新規ページの作成とページ表示の原型を作ったので、DBへの書き込み・読み込み部分を実装していきます。
- 新規ページを作成すると、テーブルにレコードが追加される
- uuidでアクセスすると、テーブルから該当するuuidのレコードを検索し、結果を返す
DBはMySQLを使用します。SQLiteとどちらにするか迷いましたが、私自身がMySQLに慣れていること、規模が大きくなるとMySQLに移行するであろうこと、得た知識を今後活用しやすそうなことから、MySQLを使用することにしました。バージョンは8.0です。
流れ
- docker-composeでFlask+MySQL環境を構築する
- docker-composeをインストール
- ディレクトリ構成を見直す
- docker-compose.ymlを書く
- MySQLの初期設定ファイルを作成
- MySQLベースイメージ取得
- docker-composeをbuild + up
- SQLAlchemy/Flask-SQLAlchemy/MySQLConnectorのインストール
- インストール + requirements.txtに追記
- アプリ構成の見直し
- 接続設定
- モデル作成
- user_pagesテーブルのスキーマ
- Migrate
- 登録/表示の実装
docker-composeでFlask+MySQL環境を構築する
まずはDB連携ができるようにします。
ここまでのコンテナはFlaskサーバ用の一つだけだったので、MySQLコンテナを立てます。
docker-composeをインストール
インストールします。
$ curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.21.2, build a133471
ディレクトリ構成を見直す
docker-composeを使うので、Flask用、DB用のディレクトリとDockerfileを作成します。また、後ほどFlask内の構成も見直します。
以下のような構成にしました。
.
├── app
│ ├── Dockerfile
│ ├── requirements.txt
│ └── src
├── docker-compose.yml
├── mysql
│ ├── Dockerfile
│ ├── logs
│ ├── my.cnf
│ ├── mysql_data
│ └── sqls
│ └── initialize.sql
.
docker-compose.ymlを書く
docker-compose.yml
version: '3'
services:
db:
build: ./mysql/
volumes:
- ./mysql/mysql_data:/var/lib/mysql # データの永続化
- ./mysql/sqls:/docker-entrypoint-initdb.d # 初期データ投入
- ./mysql/logs:/var/log/mysql # ログ記録
environment:
- MYSQL_ROOT_PASSWORD=password
appserver:
build: ./app/
ports:
- 8080:5000
volumes:
- ./app/src:/workdir
environment:
TZ: Asia/Tokyo
FLASK_APP: run.py
FLASK_ENV: development
command: flask run -h 0.0.0.0
tty: true
links:
- db
mysql/Dockerfile
FROM mysql:8.0
EXPOSE 3306
ADD ./my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
app/Dockerfile
# ベースイメージ作成
FROM python:3.7
# requirements.txtをコピー
COPY requirements.txt .
# pipアップグレード
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# 作業ディレクトリ指定
WORKDIR /workdir
# 公開ポート指定
EXPOSE 8080
MySQLの初期設定を書く
mysql/my.cnf
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin
# タイムゾーンの設定
default-time-zone = SYSTEM
log_timestamps = SYSTEM
# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password
# エラーログの設定
log-error = /var/log/mysql/mysql-error.log
# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0
# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log
# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4
# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4
mysql/sqls/initialize.sql
CREATE DATABASE app;
use app;
MySQLベースイメージ取得
docker pull mysql
docker-composeをbuild + up
ここまで書いて、起動します。
# docker-compose.ymlのあるディレクトリに移動した上で
$ docker-compose build
$ docker-compose up -d
# 起動確認(UpとなっていればOK)
$ docker-compose ps
docker-compose起動までの紆余曲折は、別記事(docker-composeでFlask+MySQL環境構築してハマった話)にまとめています。
SQLAlchemy/Flask-SQLAlchemy/MySQLConnectorのインストール
無事コンテナを起動できました。次はFlaskサーバに必要なパッケージをインストールしていきます。
インストール + requirements.txtに追記
以下のパッケージをインストールします。
- SQLAlchemy : ORマッパー。SQLを書かずにDBスキーマやDB操作ができるもの。
- Flask-SQLAlchemy : SQLAlchemyをFlaskで使いやすいようするパッケージ
- MySQLConnector : SQLAlchemyでMySQLに接続するためのコネクタ
- Flask-Migrate : DBのマイグレーションを行ってくれるパッケージ
pip installで各パッケージをインストールしていきますが、最終的にpip freezeしてrequirements.txtに書き出し、コンテナをビルドし直します。
もちろん、pip installせずに直接requirements.txtにインストールするパッケージを記載することもできますが、pip install XXXとしたときに何が入るかわからなかったので、今回はpip installでインストールする形にしました。
# インストール
$ pip install SQLAlchemy
$ pip install flask-sqlalchemy
$ pip install mysql-connector-python
$ pip insatll flask-migrate
最終的なrequirements.txt
以下のようになりました。
alembic==1.4.3
astroid==2.4.2
click==7.1.2
Flask==1.1.2
Flask-Migrate==2.5.3
Flask-SQLAlchemy==2.4.4
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
lazy-object-proxy==1.4.3
Mako==1.1.3
MarkupSafe==1.1.1
mccabe==0.6.1
mysql-connector-python==8.0.22
protobuf==3.13.0
pylint==2.5.3
python-dateutil==2.8.1
python-editor==1.0.4
six==1.15.0
SQLAlchemy==1.3.20
toml==0.10.1
typed-ast==1.4.1
Werkzeug==1.0.1
wrapt==1.12.1
アプリ構成の見直し
アプリ構成を見直します。具体的には、以下のようなことをしました。
- tbl(to-buy-list)フォルダをつくり、ソースはそこに入れていくようにした
- 起動用スクリプト(run.py)とアプリ本体(tbl/app.py)を分けた
app/直下のディレクトリ構成
.
├── Dockerfile
├── requirements.txt
├── shells
│ ├── deploy.sh
│ └── sample.sh
└── src
├── migrations
├── run.py
├── tbl
│ ├── app.py
│ ├── config.py
│ ├── database.py
│ ├── models
│ │ ├── __init__.py
│ │ └── models.py
│ └── templates
│ ├── application.html
│ ├── complete.html
│ ├── index.html
│ └── user_page.html
└── tests
└── test_app.py
接続設定
Flaskサーバ->MySQLへの接続設定をしていきます。
# このへんを触ります
├── tbl
│ ├── app.py
│ ├── config.py
│ ├── database.py
Dockerのネットワークを調べて、HOST情報を追加する
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
7eff8a1341cb bridge bridge local
a710bda8998d host host local
9ced2f2c77d5 none null local
6c4409c5f00d to-buy-list_default bridge local
# to-buy-list_defaultが今回作ったコンテナなので、その中身を確認
$ docker network inspect to-buy-list_default
[
{
"Name": "to-buy-list_default",
(省略)
"Containers": {
"c4f97f85d99cb6b1484a82c1e7727b091fb80690e2574638c2f765fe977ba804": {
"Name": "to-buy-list_appserver_1",
"EndpointID": "4a8064bfce34036728be13a50107c86d70f967c7363288b8aa4c511c9b1a7fa8",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
},
"d57745ca561effb401d72cc50780f035c3321a937374f6fb75b6d9ae80e7b540": {
"Name": "to-buy-list_db_1",
"EndpointID": "a82abfe7a0344cc7ac4558351732e02f7b452c750c6c0611d31d8a4872c163e3",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "to-buy-list",
"com.docker.compose.version": "1.21.2"
}
}
]
DBコンテナのIPがIPv4Addressにあたります。このIPを次項のconfig.pyで使います。
"Name": "to-buy-list_db_1",
"EndpointID": "a82abfe7a0344cc7ac4558351732e02f7b452c750c6c0611d31d8a4872c163e3",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
config.py
import os
class DevelopmentConfig:
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://{user}:{password}@{host}/{database}?charset=utf8mb4'.format(
**{
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', 'password'),
'host': os.getenv('DB_HOST', '172.18.0.2/16'),
'database': os.getenv('DB_DATABASE', 'app')
}
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False
Config = DevelopmentConfig
database.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
def init_db(app):
db.init_app(app)
Migrate(app, db)
app.py
from flask import Flask, render_template, request, redirect, url_for
from markupsafe import escape
from tbl.database import init_db, db
import tbl.models as models
def create_app():
app = Flask(__name__)
app.config.from_object('tbl.config.Config') # config.pyの設定を取得
init_db(app)
return app
app = create_app()
(省略)
これで接続設定はひとまず完了です。続いてモデルを作っていきます。
モデル作成
専用URLを持ったユーザーページのモデルを作成します。
models/models.pyというスクリプトに、UserPageクラスの実装をしていきます。
user_pagesテーブルのスキーマ
テーブルのスキーマは以下のようになります。
カラム名 | 型 | 概要 |
---|---|---|
id | int | 連番 |
uuid | varchar | 専用URL用の文字列 |
status | varchar | ページの状態(active/archived) |
created_at | datetime | 作成日 |
updated_at | datetime | 更新日 |
新規ページ作成時は新たにレコードが振られる。page/XXXにアクセスした場合はuuidカラムがXXXのレコードを取得する。という処理になります。
- models/models.py
from datetime import datetime
from tbl.database import db
import uuid as _uuid
class UserPage(db.Model):
__tablename__ = 'user_pages'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column(db.String(255), index=True, unique=True, nullable=False)
status = db.Column(db.String(10), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __init__(self):
self.uuid = str(_uuid.uuid4())
self.status = 'active'
def searchBy(uuid):
return db.session.query(UserPage)\
.filter(UserPage.uuid == uuid)\
.one()
- models/init.py
from .models import UserPage
__all__ = [
UserPage,
]
- app.py
import tbl.models as models # appがロードされたときに、UserPageクラスもロード
...
Migrate
モデルを作成したので、Flask-Migrateを使ってMigrationします。
これにより、Modelsに記述された内容をもとにテーブルが作成されます。
# 初期化
$ flask db init
# migrationの準備
$ flask db migrate
# migration実行
$ flask db upgrade
DBコンテナに接続して確認してみると、テーブルが作成されています。
mysql> desc user_pages;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
| status | varchar(10) | NO | | NULL | |
| uuid | varchar(255) | NO | UNI | NULL | |
+------------+--------------+------+-----+---------+----------------+
以降、Models.UserPageの中身を修正してテーブル定義を変更した時などは、
flask db migrate -> flask db upgrade を行ってテーブルの更新をするようにします。
ここまででモデルの作成とテーブル定義ができたので、次で登録/表示を実装していきます。
登録/表示の実装
基本的には前回9回で作った処理の中身を、UserPageクラスで置き換えていきます。
- app.py
(省略)
@app.route('/')
def index():
title = "to buy list"
return render_template('index.html', title=title)
@app.route('/page/<uuid>/')
def show_user_page(uuid):
title = 'Your page'
# show user_page from uuid
uuid = '%s' % escape(uuid)
page = select_page_info(uuid)
return render_template('user_page.html', title=title, page=page)
def select_page_info(uuid):
"""uuidから対象ページのデータを取得
Return:
Page : ページクラスのインスタンス
"""
user_page = models.UserPage.searchBy(uuid)
return user_page
@app.route('/page/create')
def create_new_page():
"""新規ページを作成する
"""
# レコード生成
user_page = models.UserPage()
uuid = user_page.uuid
try:
db.session.add(user_page)
db.session.commit()
title = "新規リストの作成に成功しました"
# ページ表示
return render_template('complete.html', title=title, uuid=uuid)
except Exception as e:
print(e)
raise e
- templates/complete.html
{% extends "application.html" %}
{% block body %}
<div>
<h1>新規リストの作成完了</h1>
<div class="field">
<a href='{{ uuid }}'>Open the created page.</a>
</div>
</div>
{% endblock %}
/page/createした時の挙動を、作成完了ページを表示し、作成したページのリンクを貼るようにしています。
動作確認
無事遷移しました。
最後に
これでページの作成と表示がDBを使ってできるようになりました。
「専用URLの発行」という機能はこれで完了になります。
次回からは、消耗品アイテムの作成/更新の機能を作っていきます。
ディスカッション
コメント一覧
まだ、コメントがありません