[Flask]データベースとの連携

Python

ブログ記事を収めるデータベースを作成し、Flaskを用いて、ブラウザから記事の新規作成、一覧表示、削除を行います。

データベースは「SQLAlchemy」を通して使うことが一般的なようですが、このページではPython標準ライブラリの「sqlite3」を使用します。

SQLで基本的な操作ができることを前提としています。
またデータベースとの連携をテーマとしているので、ログイン機能の実装はしません。

ログインは次の記事で扱いました。

参考にしたページ
チュートリアル — Flask Documentation (2.0.x)

使用した環境
WSL2 Ubuntu - 20.04
Python 3.10.0
Flask 2.1.3

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

データベースモジュール

データベースの初期化・接続・閉じる、といった機能をモジュールにまとめます。

import sqlite3

from flask import g


def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect('articles.db')
        g.db.row_factory = sqlite3.Row
    return g.db

def close_db(exception=None):
    db = g.pop('db', None)

    if db is not None:
        db.close()
    
def init_db():
    con = sqlite3.connect('articles.db')
    con.execute("DROP TABLE IF EXISTS POST")
    con.execute("CREATE TABLE POST (\
        ID INTEGER PRIMARY KEY AUTOINCREMENT, \
        TITLE TEXT NOT NULL, \
        CONTENT TEXT, \
        DATE TEXT DEFAULT (DATE('now')))")
    print('データベースを初期化しました')


if __name__ == '__main__':
    init_db()

flaskから「g」という変数をインポートしています。
単体のリクエスト処理時に使えるグローバルな変数です。

名前のgは「global」の略称ですが、それはcontextの中でグローバルなデータであることを指しています。gにあるデータはcontextが終了すると消失し、それは(複数の)リクエストの間でのデータを格納するには適切な場所ではありません。(複数の)リクエストをまたいでデータを格納するには、sessionまたはデータベースを使用してください。

データの格納 「Flask Documentation」日本語訳

get_db関数で、このgの中に「db」という名前を付けて、sqlite3のコネクションオブジェクトを入れています。
アプリケーションから関数を利用します。

close_db関数の引数には、exception(例外)という名前を付けた変数を置きました。
後ほど、アプリケーションのリクエスト終了時にこの関数を実行させるようにしますが、そうするとclose_dbにひとつ引数が渡されるようになるため、そのための変数です。

init_db関数でブログ用のデータベースを初期化します。
IDとタイトル、内容、作成日時を持つようにしました。

それではこのモジュールを実行し、データベースを作成します。

$ python database.py 
データベースを初期化しました
$ tree .
.
├── articles.db
└── database.py

「articles.db」が作成されました。

アプリケーションとテンプレート作成

データベースモジュールと同じディレクトリ内にいくつか必要なものを作成します。

  • __init__.py
  • app.py
  • static/style.css
  • templates/base.html
  • templates/index.html

__init__.py」と「static/style.css」を作成しますが、中身は空です。

app.py」は、アプリケーションの作成とルーティングを行うモジュールです。

import database
from flask import Flask, redirect, render_template, request, url_for


app = Flask(__name__)
app.teardown_appcontext(database.close_db)

@app.route('/')
def index():
    return render_template('index.html')


if __name__ == '__main__':
    app.run(debug=True)

自作のdatabaseと、flaskから必要なものをインポートしておきます。

app.teardown_appcontext(database.close_db)
これはリクエスト処理の終了時、databaseのclose_db関数を実行するための文です。

次に継承元の「base.html」と「index.html」を作成します。

テンプレートの継承方法

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    {% block title%}
      <title>Document</title>
    {% endblock %}
  </head>
  <body>
  {% block content %}
  {% endblock %}
  </body>
</html>
{% extends 'base.html' %}

{% block title %}
<title>インデックス</title>
{% endblock %}

{% block content %}
<h1>indexページ</h1>
<ul>
  <li><a href="#">記事の作成</a></li>
  <li><a href="#">記事一覧</a></li>
</ul>
{% endblock %}

ディレクトリ構成は次のようになりました。

$ tree .
.
├── app.py
├── articles.db
├── database.py
├── __init__.py
├── static
│   └── style.css
└── templates
    ├── base.html
    └── index.html

2 directory, 7 files

これから追加していくHTMLは「templates」に入れていきます。

ここまでで実行して確認。

$ python app.py
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
indexページ

リンク先は次の項目で作っていきます。

記事の作成

まだひとつもブログ記事がない状態ですので、記事作成機能を作ります。

app.py」に追記します。

# 上部省略
@app.route('/create')
def create_article():
    return render_template('create.html')

@app.route('/register', methods=('GET', 'POST'))
def register_article():
    if request.method == 'GET':
        return redirect(url_for('index'))

    title = request.form['title']
    content = request.form['content']
    db = database.get_db()
    db.execute(
        "INSERT INTO POST (TITLE, CONTENT) VALUES (?, ?)",
        (title, content)
    )
    db.commit()
    return redirect(url_for('index'))

「create.html」からPOSTメソッドでregister_article関数にデータを送るつもりです。
この関数がINSERT文でテーブルにデータを挿入。

「templates」内に「create.html」を作成します。

{% extends 'base.html' %}

{% block title %}
<title>新規作成</title>
{% endblock %}

{% block content %}
<h1>記事の作成</h1>
<form action="{{ url_for('register_article') }}" method="post">
  <div>
    <label for="title">タイトル</label>
    <input type="text" name="title" id="title">
  </div>
  <div>
    <label for="content">内容</label><br>
    <textarea name="content" id="content" cols="30" rows="15"></textarea>
  </div>
  <button>作成</button>
</form>
{% endblock %}

index.htmlの「記事の作成」のリンク先を変更します。

<!-- 該当箇所 -->
  <li><a href="{{ url_for('create_article' )}}">記事の作成</a></li>

ここでサーバーを動かします。

$ python app.py

indexページの「記事の作成」をクリックすると、「http://127.0.0.1:5000/create」に遷移します。

フォーム画面。タイトル「テスト記事1」、内容「テスト記事1の内容です。」が入力されている。

上記の内容で埋めて、作成ボタンをクリックすると、indexページに戻りました。

記事一覧や表示させる機能は作っていないので、Pythonを対話モードで起動し、一応データベースの中身を確認しておきます。

$ python
Python 3.10.0 (default, Oct 12 2021, 16:02:08) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3
>>> con = sqlite3.connect('articles.db')
>>> cur = con.cursor()
>>> cur.execute("SELECT * FROM POST")
<sqlite3.Cursor object at 0x7fd62274f6c0>
>>> for article in cur:
...     print(article)
... 
(1, 'テスト記事1', 'テスト記事1の内容です。', '2022-09-27')
>>> con.close()

問題ありませんね。

記事一覧

次は一覧表示するための機能を追加します。

SELECT文でデータを読み込み。

# 上部省略
@app.route('/list')
def read_articles():
    db = database.get_db()
    articles = db.execute("SELECT * FROM POST")
    return render_template('list.html', articles=articles)
{% extends 'base.html' %}

{% block title %}
<title>リスト</title>
{% endblock %}

{% block content %}
<h1>記事一覧</h1>
<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>タイトル</th>
      <th>内容</th>
      <th>作成日時</th>
      <th>機能</th>
    </tr>
  </thead>
  <tbody>
    {% for article in articles %}
    <tr>
      <th>{{ article['ID'] }}</th>
      <th>{{ article['TITLE'] }}</th>
      <th>{{ article['CONTENT'] }}</th>
      <th>{{ article['DATE'] }}</th>
      <th>
        <a href="#">削除</a>
      </th>
    </tr>
    {% endfor %}
  </tbody>
</table>
{% endblock %}

テンプレートでは、変数を辞書のように使って中身にアクセスできていますが、これは「database.py」内で「g.db.row_factory = sqlite3.Row」としているためです。

<tbody>内の「機能」の列には「削除」のリンクを設けています。実際のリンク先の設定は次の項目で行います。

そして「static/style.css」で最低限の見た目を整え、

table {
    border: 1px solid;
}

th {
    padding: 5px;
}

index.htmlの「記事一覧」のリンク先を変更。

<!-- 該当箇所 -->
  <li><a href="{{ url_for('read_articles' )}}">記事一覧</a></li>
$ python app.py

サーバーを動かし、適当に記事を追加してから「記事一覧」をクリックすると、「http://127.0.0.1:5000/list」に移動します。

記事一覧表。それぞれID、タイトル、内容、作成日時、削除のリンクが表示されている。

表示できました。

記事の削除

記事の削除はDELETE文です。

# 上部省略
@app.route('/delete/<int:id>')
def delete_article(id):
    db = database.get_db()
    db.execute("DELETE FROM POST WHERE ID=?", (id, ))
    db.commit()
    return redirect(url_for('read_articles'))

「list.html」のリンク先を変更します。

<!-- 該当箇所 -->
      <th>
        <a href="{{ url_for('delete_article', id=article['ID']) }}">削除</a>
      </th>
$ python app.py

サーバーを動かし、ブラウザから「記事一覧」をクリックします。

削除リンクの上にマウスカーソルがある

テスト記事2の削除をクリックすると・・・

テスト記事2がなくなっている

該当記事が削除、「/list」にリダイレクトされました。

まとめ

最終的なディレクトリ構成

$ tree .
.
├── app.py
├── articles.db
├── database.py
├── __init__.py
├── static
│   └── style.css
└── templates
    ├── base.html
    ├── create.html
    ├── index.html
    └── list.html

2 directories, 9 files

「app.py」は複数回追記したため、完全なものを貼っておきます。

import database
from flask import Flask, redirect, render_template, request, url_for


app = Flask(__name__)
app.teardown_appcontext(database.close_db)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/create')
def create_article():
    return render_template('create.html')

@app.route('/register', methods=('GET', 'POST'))
def register_article():
    if request.method == 'GET':
        return redirect(url_for('index'))

    title = request.form['title']
    content = request.form['content']
    db = database.get_db()
    db.execute(
        "INSERT INTO POST (TITLE, CONTENT) VALUES (?, ?)",
        (title, content)
    )
    db.commit()
    return redirect(url_for('index'))

@app.route('/list')
def read_articles():
    db = database.get_db()
    articles = db.execute("SELECT * FROM POST")
    return render_template('list.html', articles=articles)

@app.route('/delete/<int:id>')
def delete_article(id):
    db = database.get_db()
    db.execute("DELETE FROM POST WHERE ID=?", (id, ))
    db.commit()
    return redirect(url_for('read_articles'))


if __name__ == '__main__':
    app.run(debug=True)

今回sqlie3を使いました。

あらたに「SQLAlchemy」を覚えるのはちょっと負担なので、使ったほうがいいのか悩み中です。

<<前の記事
フォームとの連携

次の記事>>
ログイン機能の実装

タイトルとURLをコピーしました