【Python Flask & SQLAlchemy】初心者プログラマーのWebアプリ#5 DB登録/取得/編集/削除【CRUD】

前回Flaskのフォームからデータを送信する方法を扱いました。ただ、WEBアプリを作るならデータベース(以後DB)を使わないと便利なアプリ、サービスを作ることができません。

Python(Flask)ではSQLを書いて実行することもできなくはないですが、`SQLAlchemy`と言うORマッパーを使うのが一般的かと思います。
 
![リレーション.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/5a15ca72-67c9-8518-bf1f-4475f0147060.png)

同じPythonのフレームワークDjnagoには独自のORマッパーが標準でついているのでそれを使えばいいですが、Flaskなどの軽量フレームワークにはORマッパーがついていないので`SQLAlchemy`を導入するところから始めます。


:::note
ORマッパーはSQLを書かずにPythonなどの言語を書くだけでSQLを作成してDBのデータを取り扱えるものです。
最近のWEBアプリ開発ではORマッパーを使った開発が一般的です。
:::

この記事は昔書いたDjangoでDB使う記事をFlask用に改造して修正したものです。内容的には大体同じです。

https://qiita.com/Bashi50/items/280bc341eeec5d7f8797

# :pushpin: Pythonで作るFlaskアプリ記事一覧

|  | 内容|
|:----:|:----:|
|**part1**|[簡単なページ作成](https://qiita.com/Bashi50/items/30065e8f54f7e8038323)|
|**part2**|[HTMLテンプレート表示](https://qiita.com/Bashi50/items/9bb5ca4a519c17caf475)|
|**part3**|["画像" "CSS" "Javascript"実装](https://qiita.com/Bashi50/items/d5bc47eeb9668304aaa2)|
|**part4**|[フォーム送信](https://qiita.com/Bashi50/items/457d3b8a2328b2b1d012)|
|**part5**|**データベースの値取得・更新← ココ**|

ついでに、SQLAlchemy使いますのでDBから取得するときにどんなふうに書くんだっけ？ってよくなるので逆引きできる記事あったらいいなーと思って書いた記事

https://qiita.com/Bashi50/items/7f2214550e80b87fb922

## ソースコード
この章のコードは以下です。確認やコピペでどうぞ

https://github.com/kohishibashi/flask/tree/part5


# 0. 準備：データベース(DB)を入れてFlaskで使う
DBはなんでもいいですが、今回はテストアプリなので`SQLite`を使用します。
引き続き仮想環境はpipenvなのでインストールするときは`pipenv`を使用します。
※ pipenv使ってないならpip使ってインストール。

```
$ pipenv install Flask-SQLAlchemy
```

設定を記述するためのファイル`config.py`に追記。
`SQLALCHEMY_DATABASE_URI`に書いた名前でsqliteのファイルが作成されます。今回は`sample_flask.db`にしようと思います。ファイル名は任意です。

```/Users/dev/flask/testapp/config.py
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///sample_flask.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
```
続いて`__init__.py`に追記。

```/Users/dev/flask/testapp/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy  # 追加

app = Flask(__name__)
app.config.from_object('testapp.config')

db = SQLAlchemy(app)  # 追加

import testapp.views
```

`__init__.py`に書いたことで、ファイル一つ一つにDBの設定を書かなくても`db`をimportして`db`を使うことでSQLAlchemyでDBを操作できるので便利に使えます。

# 1. モデルを作成
テーブルの定義を書いたり、DBにまつわる処理を記述するのがモデルです。テーブルを作成したいので定義を書いていきます。

## 1-1. テーブルを作る
データを保存するためにテーブルを作成する必要があります。
ORマッパー使わないとSQLで定義を書いて作成することになりますが、SQLAlchemyマッパーならPythonのclass(モデル)にテーブルの定義を記述すればテーブルをいい感じで作ってくれます。

データを保存する手順として

1. モデルファイルを作成してテーブルのデータ構造などを記述
2. ターミナルにてテーブル作成コマンド実行

という流れでテーブルが作れます。

:::note
DjangoだとORマッパーがついていて、使わないかもしれないテーブルも自動で作ってくれます。それが便利だったら邪魔だったりします。Flaskは軽量フレームワークです。良くも悪くも最低限しか機能がないので自分で良いように作っていくことができるのがFlaskの強みです。
:::

## 1-2. models.py作成
データを保存するためにDBにテーブルを作っていきます。

modelsフォルダを作成。今回の例ならtestappのディレクトリ配下になります。

```/Users/dev/flask/testapp/models/employee.py
from testapp import db
from datetime import datetime


class Employee(db.Model):
    __tablename__ = 'employee'
    id = db.Column(db.Integer, primary_key=True)  # システムで使う番号
    name = db.Column(db.String(255))  # 社員名
    mail = db.Column(db.String(255))  # メール
    is_remote = db.Column(db.Boolean)  # リモート勤務しているか
    department = db.Column(db.String(255))  # 部署
    year = db.Column(db.Integer, default=0)  # 社歴
    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)  # 更新日時
```
これがデータ構造です。例としていろんな社員（従業員）情報管理アプリでも作ってみます。

### データの定義
書いたコードの説明をします。説明いらないから取り合えず動かしたいだけなら[2. DBにテーブルを作成](#2-dbにテーブルを作成)に行っても大丈夫です。

以下がテーブルに登録するデータの型を定義しています。

- :white_check_mark: db.Integer: 整数型のデータ保存
- :white_check_mark: db.String: 文字列のデータ保存
- :white_check_mark: db.Boolean: TrueかFalseのデータ保存
- :white_check_mark: db.DateTime: 日時データの保存。日にちだけで良いならdb.Date使用する

`db`は`__init__.py`に記述した`db`と名前つけてimportしています。なので解説する人や記事によって`db.`は変わります。

### オプションで定義をより細かく設定

1. IDは自動採番。プライマリーキー
1. 文字列で`String(255)`のようにすると文字数を255文字に制限できる
1. オプション引数：`nullable`はデータがNULLを許可するかしないか`False` or `True`
1. オプション引数：デフォルト値を指定するなら`default`を使う。
    1. `year`なら新規登録時に指定しなかったら0を入れる
    1. `created_at`, `updated_at`は作成時の日時を設定する
1. オプション引数：`onupdate`を使うとデータが更新されたときに日時を更新できます。

# 2. DBにテーブルを作成
ちゃんと開発するならDBの変更履歴を使える`Flask-Migrate`というものをインストールして使うと良いと思います。
今回は基礎的なアプリなので一番手軽な方法でDBを作成します。

## 2.1. 手軽な方法でDB・テーブルを作成
この方法はターミナルを開いてpythonで`SQLAlchemy`のメソッドを直接実行する方法です。
今回、`employee`テーブルを作成するので、作成する`employee`モデルをimportします。

```/Users/dev/flask/testapp/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config.from_object('testapp.config')

db = SQLAlchemy(app)
from .models import employee  # 追加

import testapp.views
```

作成する employeeを読み込まないとテーブルが作成されなかったのでこのように読み込んでいます。
仮想環境のpipenvの中でコマンドを実行する場合は`pipenv shell`で仮想環境の中に入る必要があります。

```
$ pipenv shell
$ python
>>> from testapp import db
>>> db.create_all()
```

問題なければ`/Users/dev/flask/testapp/sample_flask.db`と言うファイルが作成されたと思います。
これがSQLiteのデータを保存しているファイルになります。

# 3. FlaskでDBのCRUD(登録・取得・更新・削除)を行う
DBとテーブルを作成できたのでいよいよDBを使ったWEBアプリ作成をしていこうと思います。
まず最初は新規作成を行います。

## 3.1 CRUDを作る。CRUD (Create): データ新規作成・登録
formからデータを受け取ってデータをDBに登録までを一気にやると初心者向け記事では優しくないかなと思いましたので、まずは特定のURLにアクセスすると固定の値が作成されるようにして、少しずつ改造していこうと思います。

### 3.1.1 新規データ追加ページ（仮）を作る
少しずつ改造していこうと思うので、ボタンを押したら固定の値がDBに書き込まれる機能を最初に作ります。
views.pyに以下を追加してページを追加のためのページを作成します。
ここは前回の記事の復習ですのでコードのみ書きます。

```/Users/dev/flask/testapp/views.py
@app.route('/add_employee', methods=['GET', 'POST'])
def add_amployee():
    return render_template('testapp/add_employee.html')
```

テンプレートも追加必要ですね。

```/Users/dev/flask/testapp/templates/testapp/add_employee.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>データの追加だけ行うページ</h2>
    <form action="/add_employee" method="POST">
        <button type="submit" class="btn btn-primary">従業員追加</button>
    </form>
</div>
{% endblock %}
```

今回も共通化した`layout.html`とbootstrap使ってます。

ページが正しくできたか確認します。以下にアクセスすればとりあえず画面は表示されます。

> http://127.0.0.1:5000/add_employee

![スクリーンショット 2021-12-15 0.29.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/66998513-2338-ab0e-624b-38c6c74d22fd.png)

ボタンを押すと`POSTで送信`とブラウザに表示されるかと思います。
※ 元のページに戻るときはアドレスバーを`http://127.0.0.1:5000/add_employee`と入力して移動してください。リロードだとうまくいかないので

### 3.1.2 固定値をDBに書き込む
POSTでアクセスされたらDBにデータを書き込むコードを書いてみましょう。

```/Users/dev/flask/testapp/views.py
from flask import render_template, request, redirect, url_for
.
.

from testapp import db
from testapp.models.employee import Employee
.
.
.
@app.route('/add_employee', methods=['GET', 'POST'])
def add_employee():
    if request.method == 'GET':
        return render_template('testapp/add_employee.html')
    if request.method == 'POST':
        employee = Employee(
            name='Tanaka',
            mail="aaa@aa.com",
            is_remote=False,
            department="develop",
            year=2
        )
        db.session.add(employee)
        db.session.commit()
        return redirect(url_for('index'))
```

従業員追加というボタンを押すとindexのページ（トップページ）に戻って何も起こっていないように思えますが、データが作成されています。

![スクリーンショット 2021-12-15 1.14.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/2cef0c98-2b8b-0aba-f4b9-0da8a76e29a2.png)


#### コード説明
:white_check_mark: `if request.method == 'GET'`は新規登録のためのページを表示(htmlをレンダリング)する処理のためのコードです。`add_employee.html`を元にHTML表示させる処理だけして処理終了です。

:white_check_mark: POSTで送信された場合には、その下の` if request.method == 'POST':`のブロックに入ります。
`Employee`がモデル。モデルがテーブルのカラムを持っているようなイメージ持つといいかも。各カラムの値に固定値を入れると`employee`という変数(モデルインスタンス)がDBのテーブルで言ったらレコード１行分のデータ入ってる感じになります。

:white_check_mark: データが入ったので`db.session.add(モデルインスタンス)`, `db.session.commit()`でDBに登録しています。
`add`だけだと登録されなくて、`commit`することでDBに登録できますので登録の際は`add`, `commit`忘れずに！

:white_check_mark: 処理が終わったら`index`ページにリダイレクト。
リダイレクトがないとボタンを押したけど、画面に全く変化ないので違和感たっぷりになりますので。

## 3.2 フォームからデータを受け取ったデータをDBに登録
[前回の記事](https://qiita.com/Bashi50/items/457d3b8a2328b2b1d012)でフォームから受け取ったデータを受け取って処理を書く方法の基礎に触れました。
今度は新しいページにフォーム作って、フォームから受け取ったデータをDBに登録しようと思います。

```/Users/dev/flask/testapp/templates/testapp/add_employee.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>データの追加だけ行うページ</h2>
    <form action="/add_employee" method="POST">
        <label for="name">従業員名</label>
        <input name="name"></input>

        <label for="mail">メールアドレス</label>
        <input name="mail"></input>

        <label for="name">リモートワークの有無</label>
        <input type="checkbox" name="is_remote">

        <label for="department">部署</label>
        <input name="department"></input>
        
        <label for="year">勤続年数</label>
        <input type="number" name="year" min="0">

        <button type="submit" class="btn btn-primary">従業員追加</button>
    </form>
</div>
{% endblock %}
```

全て１行になって見にくいですが入力に必要なフォームはできました。
見た目綺麗にするのは後にして先に進めます。

![スクリーンショット 2021-12-27 14.41.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/92656fc1-ce06-69ad-1ce4-9eba19b93732.png)

```/Users/dev/flask/testapp/views.py
@app.route('/add_employee', methods=['GET', 'POST'])
def add_employee():
    if request.method == 'GET':
        return render_template('testapp/add_employee.html')
    if request.method == 'POST':
        form_name = request.form.get('name')  # str
        form_mail = request.form.get('mail')  # str
        # チェックなしならFalse。str -> bool型に変換
        form_is_remote = request.form.get('is_remote', default=False, type=bool)
        form_department = request.form.get('department')  # str
        # int, データないとき０
        form_year = request.form.get('year', default=0, type=int)

        employee = Employee(
            name=form_name,
            mail=form_mail,
            is_remote=form_is_remote,
            department=form_department,
            year=form_year
        )
        db.session.add(employee)
        db.session.commit()
        return redirect(url_for('index'))
```

![スクリーンショット 2021-12-27 17.14.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/57280bd8-965b-a8fa-47e6-ded20afba4c0.png)

適当に入力してボタンを押せばデータが登録されます。

### コード説明
`request.form`を使うと`ImmutableMultiDict`という形で普通のdictっぽいものに機能が少し追加されたものの中にデータが格納されています。まあ、dictの形式でformに入力した情報が入ってると思っておいてもいいかと思います。

- dictの機能の、`.get('ここにキー')`を使ってる。`request.form['name']`みたいな書き方と基本は同じ。
    - :white_check_mark: `.get()`で取得するとデータが未入力の場合`None`になる
    - 例えば`request.form['is_remote']`とするとチェックボックスをクリックしてないとデータがカラ。なのでエラーになる。それを防ぐ
    - :white_check_mark: `default`のオプションを引数を入れることでNoneだったときに代わりにデフォルトの値を指定できる。
    - :white_check_mark: `type`のオプションを引数を入れることで指定の型に変換してくれる。strがデフォルト値なのでstr型でいいなら省略可能。int, boolなど使える
- `request.form`は`str`型。注意！
    - :white_check_mark: つまり`request.form.get('is_remote')`と取得するだけだと`'false'`と言う「文字列」になる。
    - falseにしたつもりなのに判定式書くと文字列の`'false'`はTrueとなる！ついでに`'true'`はTrue。

## 3.3 CRUDを作る。【Read】：　DBのデータ取得・読み取り
DBに入っているデータを取得してみます。先程までに登録したデータを取得してデータを表示します。
作成するのは二つのページ。
:white_check_mark: 全データ取得して表示する一覧ページ → [一覧ページ作成](##331-一覧ページ作成)
:white_check_mark: 個別のデータを取得して表示する詳細ページ → [詳細ページ作成](#332-詳細ページ作成)

### 3.3.1 一覧ページ作成
最初にEmployeeのデータ全てを取得して画面に表示する処理を書いてみます。
以下を追記。

```/Users/dev/flask/testapp/views.py
@app.route('/employees')
def employee_list():
    employees = Employee.query.all()
    return render_template('testapp/employee_list.html', employees=employees)
```

`employee_list.html`というテンプレートにデータを埋め込んで表示させます。
`employee_list.html`はないのでファイルを作成。

```/Users/dev/flask/testapp/templates/testapp/employee_list.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>従業員一覧</h2>
    <ul>
        {% for employee in employees %}
            <li>名前: {{ employee.name }} メール: {{ employee.mail }}</li>
        {% endfor %}
    </ul>
</div>
{% endblock %}
```

URLは以下にて作成してます。

> http://127.0.0.1:5000/employees

![スクリーンショット 2021-12-27 18.55.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/04503ae0-c5cb-0dbf-a7bc-f9e56ebb9e25.png)

この表示みたいに登録したデータ表示されたらOKです。

#### コード説明

- views.pyで`.query.all()`は全データ取得という意味。`employees`という名前の変数(任意の名前でOK)にDBから取得したデータを入れる。
- `employees`という名前でテンプレートに変数として渡す
- テンプレートで渡された`employees`を`for`を使って一つずつ（１レコードずつ）データを分解してデータを埋め込む
- テンプレートの共通部分は`layout.html`に記述している


#### 見た目改善 bootstrapのtable
せっかくなので他のデータもまとめて表示したいのでtableに直してみます。使っているデータは同じでbootstrapでtableの見た目を整えただけですのでviews.pyは変更いりません。

```/Users/dev/flask/testapp/templates/testapp/employee_list.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>従業員一覧</h2>
    <table class="table table-striped">
        <thead>
            <tr>
              <th scope="col">#</th>
              <th scope="col">名前</th>
              <th scope="col">メール</th>
              <th scope="col">リモート状況</th>
              <th scope="col">部署</th>
              <th scope="col">勤続年数</th>
            </tr>
          </thead>
          <tbody>
            {% for employee in employees %}
            <tr>
              <th scope="row">{{ employee.id }}</th>
              <td>{{ employee.name }}</td>
              <td>{{ employee.mail }}</td>
              {% if employee.is_remote %}
                <td>リモート</td>
              {% else %}
                <td>出勤</td>
              {% endif %}
              <td>{{ employee.department }}</td>
              <td>{{ employee.year }}年</td>
            </tr>
            {% endfor %}
          </tbody>
    </table>
</div>
{% endblock %}
```

![スクリーンショット 2021-12-27 19.13.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/440bf4f6-cef1-12ad-d5a8-7c1f83827983.png)

見た目が少しまともになりましたね。

### 3.3.2 詳細ページ作成
全データは取得できたので詳細ページを作ろうと思います。
一覧ページは全従業員のデータを表示しましたが、詳細ページでは一人の従業員の詳細情報を表示するページになります。
まあ、よくある機能というかページですが今回テーブルを1つしか作っていないので詳細ページなのに一覧と表示する内容が同じです:sweat:

まあ、そこは気にせずに作り方を紹介します。
一覧ページと作りがほぼ同じなので流用して少しだけ修正します。

`employee_detail.html`というテンプレートを作成。このテンプレートに特定の従業員の情報を表示するページを作ります。
`views.py`に下記を追記。

```/Users/dev/flask/testapp/views.py
@app.route('/employees/<int:id>')
def employee_detail(id):
    employee = Employee.query.get(id)
    return render_template('testapp/employee_detail.html', employee=employee)
```

```/Users/dev/flask/testapp/templates/testapp/employee_detail.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>従業員詳細</h2>
    <table class="table table-striped">
        <thead>
            <tr>
              <th scope="col">#</th>
              <th scope="col">名前</th>
              <th scope="col">メール</th>
              <th scope="col">リモート状況</th>
              <th scope="col">部署</th>
              <th scope="col">勤続年数</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">{{ employee.id }}</th>
              <td>{{ employee.name }}</td>
              <td>{{ employee.mail }}</td>
              {% if employee.is_remote %}
                <td>リモート</td>
              {% else %}
                <td>出勤</td>
              {% endif %}
              <td>{{ employee.department }}</td>
              <td>{{ employee.year }}年</td>
            </tr>
          </tbody>
    </table>
</div>
{% endblock %}
```

> http://127.0.0.1:5000/employees/1

![スクリーンショット 2021-12-27 19.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/71e7052f-b372-0aff-9cf2-f5525b1f1942.png)


のようにアクセスすると詳細ページ表示できます。URLの一番最後の数値がデータのIDに当たるので、変えるとこれまでDBに登録した該当のデータが表示できます。

#### コード説明

views.pyのデコレータの部分`'/employees/<int:id>`のように書くと

> http://127.0.0.1:5000/employees/1

のようにアクセスすると`id = 1`で値が入ります。

- :white_check_mark: `def employee_detail(id)`の引数の`id`に値が入る
    - `<int:id>`のように型を書いたのでint型で取得できる
- :white_check_mark: `.query.get(ここに値)`はプライマリーキーを値として与えることでデータを取得できる
    - idがプライマリーキーになっているケースが多いと思う
- :white_check_mark: `.query.all()`は全て取得(複数件取得)なのでリストのデータを取り出すような感じで、forで一つずつ取り出す必要があったが、`get()`は単一のデータなのでforなどで一つずつ取り出す必要ない

forで取り出す必要ないのでforを消しているのと、単一レコードのデータなので`employees`→`employee`の単数系にしてますがその他はほぼ同じです。

#### 補足
飛ばしてもOKです。存在しないIDを入れた場合にエラー出したいという場合があります。

> http://127.0.0.1:5000/employees/100

このIDのデータがないです。このままだとページは表示されるけどデータはないような表示なると思います。

![スクリーンショット 2021-12-28 17.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/f9918052-4269-912d-0edf-37cc1d76cc2f.png)
※ データがないと表示がおかしくなる

これが容認できないケースもあるのでアクセスするとエラーにさせることができます。
今回は詳しくはやりませんがエラーを出してエラーページを表示するということをすると完成度が上がります。

```/Users/dev/flask/testapp/views.py
@app.route('/employees/<int:id>')
def employee_detail(id):
    employee = Employee.query.get_or_404(id)
    # もしくは以下のようにデータが一つでないとエラー出すことで１つなのを補償する方法も。
    # employee = Employee.query.filter(Employee.id == id).one()
    return render_template('testapp/employee_detail.html', employee=employee)
```

`get_or_404`はデータが取得できない場合404 Not Foundを出してくれる便利機能。`.one()`を使った方法はデータが一つでない場合はエラーが出せるのでtry catchで処理を工夫するといいと思います。

## 3.4 CRUDを作る。【Update】：　DBのデータ更新・編集
以前入力した情報を編集する機能を作ります。
まずは最初に、formを設置して情報を編集できるページを作成しましょう。

### 3.4.1 編集ページ作成
以下を追記して、その後`employee_edit.html`を作成します。

```/Users/dev/flask/testapp/views.py
@app.route('/employees/<int:id>/edit', methods=['GET'])
def employee_edit(id):
    # 編集ページ表示用
    employee = Employee.query.get(id)
    return render_template('testapp/employee_edit.html', employee=employee)
```

```/Users/dev/flask/testapp/templates/testapp/employee_edit.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>データの編集ページ</h2>
    <form action="{{ url_for('employee_update', id=employee.id) }}" method="POST">
        <label for="name">従業員名</label>
        <input name="name"></input>

        <label for="mail">メールアドレス</label>
        <input name="mail"></input>

        <label for="name">リモートワークの有無</label>
        <input type="checkbox" name="is_remote">

        <label for="department">部署</label>
        <input name="department"></input>
        
        <label for="year">勤続年数</label>
        <input type="number" name="year" min="0">

        <button type="submit" class="btn btn-primary">従業員追加</button>
    </form>
</div>
{% endblock %}
```
以下にアクセスすればページが表示できると思います。

> http://127.0.0.1:5000/employees/1/edit

![スクリーンショット 2021-12-28 0.17.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/1791ed1f-ecac-b610-0819-8050afb6f4ea.png)

#### コード解説
以前作った新規追加ページに似ていますが、`<form>`の送り先を動的に変更できるように`url_for()`を使っています。
編集ページのURLは例えば`http://127.0.0.1:5000/employees/1/edit`のようになりidが「１」の人の編集ページです。

- :white_check_mark: `/employees/1/edit`のidを示す１をユーザーに合わせて送り先を変更しないと別のユーザーの情報を編集してしまうという事態になってしまいますのでここは動的に作る必要がるわけです。
- :white_check_mark: `url_for('employee_update', id=employee.id)`は第一引数にviews.pyに書いたメソッド名、第二引数はメソッドの上に書いたデコレータのrouteに`'/employees/<int:id>/edit'`と書きました。なので、`id`が必要なのでDBから取ったidを与えることで送り先と編集データが一致します。


#### 検証ツールで送り先確認
検証できる人は一応確認すると理解が深まるかなと思います。
検証ツールで確認してみるとこのページのURL `http://127.0.0.1:5000/employees/1/update`の「１」の数字に合わせてフォームでの送り先が動的に変化します。
![スクリーンショット 2021-12-28 1.35.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/3c02eed8-0bda-2217-9f2c-09701b110a36.png)

> http://127.0.0.1:5000/employees/1/edit
> http://127.0.0.1:5000/employees/2/edit

などで試してみてください。
chromeならF12押してElementの項目にて画像の左上のスマホアイコンの隣のポインターみたいなアイコン押して該当箇所を画面上でクリックすれば確認できます。


### 3.4.2 編集ページ改善
今の状態だと編集ページのくせに、以前の入力したデータが表示されていないのでとても不便です。
ということで初期値として以前DBに登録したデータを表示させるようにします。

```/Users/dev/flask/testapp/templates/testapp/employee_edit.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-4">
    <h2>データの編集ページ</h2>
    <form action="{{ url_for('employee_update', id=employee.id) }}" method="POST">
        <label for="name">従業員名</label>
        <input name="name" value="{{ employee.name }}"></input>

        <label for="mail">メールアドレス</label>
        <input name="mail" value="{{ employee.mail }}"></input>

        <label for="name">リモートワークの有無</label>
        <input 
            type="checkbox"
            name="is_remote"
            {% if employee.is_remote %}checked{% endif %}
        >

        <label for="department">部署</label>
        <input name="department" value="{{ employee.department }}"></input>
        
        <label for="year">勤続年数</label>
        <input type="number" name="year" min="0" value="{{ employee.year }}">

        <button type="submit" class="btn btn-primary">従業員追加</button>
    </form>
</div>
{% endblock %}
```

- :white_check_mark: 入力フォームの場合は`value`を使います(この値がフォームの値になる)。
    - views.pyで`employee = Employee.query.get(id)`の箇所でDBからデータは取得してテンプレートで`employee`で使えるようにしていた。`render_template('testapp/employee_edit.html', employee=employee)`の第二引数。
    - `employee`に各データがはいっているのでこれを使ってデータを埋め込むことで表示が可能です。
- :white_check_mark: checkboxはTure or Falseではチェックの切り替えができなかったので`{% if employee.is_remote %}checked{% endif %}`のようにすることでTrueだった場合にはチェックボックスにチェックが入るようにしています。
(...もう少しかっこよく書ける方法あったら教えてください。)

### 3.4.3 更新機能実装
編集ページはできたので送られてきたデータを更新するメソッドを書きます。
以下を追記します。

```/Users/dev/flask/testapp/views.py
@app.route('/employees/<int:id>/update', methods=['POST'])
def employee_update(id):
    employee = Employee.query.get(id)  # 更新するデータをDBから取得
    employee.name = request.form.get('name')
    employee.mail = request.form.get('mail')
    employee.is_remote = request.form.get('is_remote', default=False, type=bool)
    employee.department = request.form.get('department')
    employee.year = request.form.get('year', default=0, type=int)

    db.session.merge(employee)
    db.session.commit()
    return redirect(url_for('employee_list'))
```

これで完成したので

> http://127.0.0.1:5000/employees/1/edit

でデータを更新してみます。
![スクリーンショット 2021-12-28 1.29.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/9de7dd06-22e4-543d-ff10-e871a8a19874.png)

問題なかったら一覧画面にリダイレクトされてデータが更新されたのが確認できると思います。
![スクリーンショット 2021-12-28 1.58.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/04402285-597c-d1ad-6d85-7879dc6ef954.png)

#### コード解説

- :white_check_mark: テンプレートで`<form action="{{ url_for('employee_update', id=employee.id) }}" method="POST">`の箇所に書いた動的に生成したURLのidが`def employee_update(id):`の引数のidに入る。
- :white_check_mark: `employee = Employee.query.get(id)`でidを指定して更新するデータを取得
- :white_check_mark: 各データを`employee.name = request.form.get('name')`のようにデータを更新していきます。`request.form.get()`は新規登録と同じなので説明は省略。
- :white_check_mark: データを入れたら、`db.session.merge(employee)`のようにmergeとすることでデータを更新できる。そして`commit()`をしてDBに更新したデータを登録。
- :white_check_mark: 更新が終わったら`return redirect(url_for('employee_list'))`で従業員一覧ページにリダイレクト。
    - テンプレートでも使った`url_for`。`employee_list`のようにメソッド名を指定するとURLを作成してくれる関数

## 3.5 CRUDを作る。【Delete】：　DBのデータ削除
最後にデータの削除です。これはこれまでのものがわかっていれば簡単に理解できると思います。

一覧ページで削除ボタンを押したら削除するというのが一般的かと思いますので一覧ページに削除ボタンをつけようと思います。

ページはないのでviews.pyにはPOSTの処理だけ追記します。

```/Users/dev/flask/testapp/views.py
@app.route('/employees/<int:id>/delete', methods=['POST'])  
def employee_delete(id):  
    employee = Employee.query.get(id)   
    db.session.delete(employee)  
    db.session.commit()  
    return redirect(url_for('employee_list'))
```

一覧ページの該当箇所を編集。勤続年数の下と`<td>{{ employee.year }}年</td>`の下にボタンと送信機能を追加してます。

```/Users/dev/flask/testapp/templates/testapp/employee_list.html
    <table class="table table-striped">
        <thead>
            <tr>
              <th scope="col">#</th>
              <th scope="col">名前</th>
              <th scope="col">メール</th>
              <th scope="col">リモート状況</th>
              <th scope="col">部署</th>
              <th scope="col">勤続年数</th>
              <th scope="col"></th>
            </tr>
          </thead>
          <tbody>
            {% for employee in employees %}
            <tr>
              <th scope="row">{{ employee.id }}</th>
              <td>{{ employee.name }}</td>
              <td>{{ employee.mail }}</td>
              {% if employee.is_remote %}
                <td>リモート</td>
              {% else %}
                <td>出勤</td>
              {% endif %}
              <td>{{ employee.department }}</td>
              <td>{{ employee.year }}年</td>
              <td>
                <form action="{{ url_for('employee_delete', id=employee.id) }}" method="POST">  
                  <input class="btn btn-danger btn-sm" type="submit" value="削除">  
                </form>
              </td>
            </tr>
            {% endfor %}
          </tbody>
    </table>
```

![スクリーンショット 2021-12-28 2.29.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/d4132aa6-2195-df14-b56b-bbfb9121f61d.png)

削除ボタンを一番後ろにつけました。以前作成した新規登録ページにてID 3の斉藤さんを試しに追加したのでこれを削除してみます。

![スクリーンショット 2021-12-28 2.31.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/0b53b2e7-6761-bcf3-5481-49a43ac660d9.png)


ボタンを押したらデータが消えました。本当は削除ボタンを押したら、モーダルなんかを出して「削除してもいいですか？」とか出した方がいいですが、この記事は基礎的な内容なのでそれは機会があればということで。


## 4. 最後に表示とリンクを少し整える
これまでURL直打ちで対応していたのでURLを追加するのと、デザインがテキトーなところがあるので修正して完了にします。
デザインは既に導入した`bootstrap 5`を使います。

### 4.1. 一覧画面に詳細画面遷移リンクをつける【bootstrap】
まずは名前をクリックしたら詳細ページにリンクできるようにする

```/Users/dev/flask/testapp/templates/testapp/employee_list.html
              <td>
                <a class="text-decoration-none" href="{{ url_for('employee_detail', id=employee.id) }}">
                  {{ employee.name }}
                </a>
              </td>
```

![スクリーンショット 2021-12-28 18.01.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/ea84c432-bdcf-74b6-f8d5-4bbde882cdf4.png)

押したら遷移できるか確認してください。

### 4.2. 詳細画面に編集画面ボタンをつける【bootstrap】
情報の編集ページに遷移できるリンクやボタンなどが今はないので不便です。
なので詳細画面に「編集ページに遷移できるボタン」を設置しようと思います。

tableの右上にリンクのボタンを設置しようと思います。

```/Users/dev/flask/testapp/templates/testapp/employee_detail.html
.
.
<div class="container mt-4">
    <div class="row mb-4">
      <div class="col-9">
        <h2>従業員詳細</h2>
      </div>
      <div class="col-3">
        <a class="btn btn-outline-primary" href="{{ url_for('employee_edit', id=employee.id) }}" role="button">情報編集</a>
      </div>
    </div>
    <table class="table table-striped">
.
.

```

![スクリーンショット 2021-12-28 19.28.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/2fbc5843-dc3f-97c4-a914-9feb7626bf1b.png)

情報編集ボタンを押すと遷移できるか確認してみてください。

### 4.3. bootstrapナビバーに遷移リンクをつける
放置していたbootstrapで作ったナビバーに遷移リンクをつけます。
リンクをつけただけです。

```/Users/dev/flask/testapp/templates/layout.html
    <nav class="navbar navbar-expand-lg navbar-light" style="background-color: #e3f2fd;">
        <div class="container-fluid">
          <a class="navbar-brand" href="{{ url_for('employee_list') }}">従業員管理</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
            <div class="navbar-nav">
              <a class="nav-link" href="{{ url_for('add_employee') }}">新規登録</a>
              <a class="nav-link" href="{{ url_for('sample_form') }}">ジャンケンアプリ</a>
            </div>
          </div>
        </div>
      </nav>
```

![スクリーンショット 2021-12-28 18.20.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/acf9df27-9939-24b4-9f00-9c040abb3e4f.png)

この場合動的にURLを作る必要もないので`url_for()`使わなくても`href="/employees"`みたいに、ベタ書きでもいいのですが、views.pyでURLを変更した時にこちらは変更忘れてページにアクセスできなくなるということがなくなるので使うと良いと思います。

### 4.4. 新規登録ページ・編集ページを綺麗にする
formが１行になって非常にわかりにくかったので少し綺麗にしました。見た目良くするためにcheckboxをradioに変えてますがそれ以外はbootstrapのデザインを適用させただけです。

長くなったので折り畳んでます↓

<details><summary>新規作成画面デザイン修正コード</summary><div>

```/Users/dev/flask/testapp/templates/testapp/add_employee.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-5 mb-5 d-flex justify-content-center">
	<div class="card px-1 py-4" style="width: 400px;">
		<div class="card-body">
			<h6 class="card-title mb-3">新規登録ページ</h6>
			<form action="/add_employee" method="POST">
				<div class="row">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="name" class="form-control" type="text" placeholder="名前"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="mail" class="form-control" type="text" placeholder="メールアドレス"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<div class="btn-group" role="group" aria-label="is_remote toggle">
								<input type="radio" class="btn-check" name="is_remote" id="is_remote1" autocomplete="off" value="false" checked>
								<label class="btn btn-outline-primary" for="is_remote1">出社</label>
								<input type="radio" class="btn-check" name="is_remote" id="is_remote2" autocomplete="off" value="true">
								<label class="btn btn-outline-primary" for="is_remote2">リモート</label>
							</div>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="department" class="form-control" type="text" placeholder="部署"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="year" min="0"  class="form-control" type="number" placeholder="勤続年数"></input>
						</div>
					</div>
				</div>
				<div class="row mt-3">
					<div class="col text-center">
						<button type="submit" class="btn btn-primary">従業員追加</button>
					</div>
				</div>
			</form>
		</div>
	</div>
</div>
{% endblock %}
```
</div></details>

![スクリーンショット 2021-12-28 19.06.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/b24ee8c8-6d9f-08dd-e89d-07f5fe3b9fd9.png)


<details><summary>情報編集画面デザイン修正コード</summary><div>

```/Users/dev/flask/testapp/templates/testapp/employee_edit.html
{% extends "layout.html" %}

{% block content %}
<div class="container mt-5 mb-5 d-flex justify-content-center">
	<div class="card px-1 py-4" style="width: 400px;">
		<div class="card-body">
			<h6 class="card-title mb-3">データの編集ページ</h6>
			<form action="{{ url_for('employee_update', id=employee.id) }}" method="POST">
				<div class="row">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="name" value="{{ employee.name }}" class="form-control" type="text" placeholder="名前"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="mail" value="{{ employee.mail }}" class="form-control" type="text" placeholder="メールアドレス"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<div class="btn-group" role="group" aria-label="is_remote toggle">
								<input type="radio" class="btn-check" name="is_remote" id="is_remote1" autocomplete="off" value="false" {% if not employee.is_remote %}checked{% endif %}>
								<label class="btn btn-outline-primary" for="is_remote1">出社</label>
								<input type="radio" class="btn-check" name="is_remote" id="is_remote2" autocomplete="off" value="true" {% if employee.is_remote %}checked{% endif %}>
								<label class="btn btn-outline-primary" for="is_remote2">リモート</label>
							</div>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="department" value="{{ employee.department }}" class="form-control" type="text" placeholder="部署"></input>
						</div>
					</div>
				</div>
				<div class="row mt-2">
					<div class="col-sm-12">
						<div class="form-group">
							<input name="year" min="0" value="{{ employee.year }}" class="form-control" type="number" placeholder="勤続年数"></input>
						</div>
					</div>
				</div>
				<div class="row mt-3">
					<div class="col text-center">
						<button type="submit" class="btn btn-primary">従業員追加</button>
					</div>
				</div>
			</form>
		</div>
	</div>
</div>
{% endblock %}
```
</div></details>


![スクリーンショット 2021-12-28 19.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/2eb0925d-dd5b-dbfb-1de9-1835203825d7.png)

# :pushpin: Pythonで作るFlaskアプリ記事一覧

|  | 内容|
|:----:|:----:|
|**part1**|[簡単なページ作成](https://qiita.com/Bashi50/items/30065e8f54f7e8038323)|
|**part2**|[HTMLテンプレート表示](https://qiita.com/Bashi50/items/9bb5ca4a519c17caf475)|
|**part3**|["画像" "CSS" "Javascript"実装](https://qiita.com/Bashi50/items/d5bc47eeb9668304aaa2)|
|**part4**|[フォーム送信](https://qiita.com/Bashi50/items/457d3b8a2328b2b1d012)|
|**part5**|**データベースの値取得・更新← ココ**|

## ソースコード
この章のコードは以下です。確認やコピペでどうぞ

https://github.com/kohishibashi/flask/tree/part5