Flask+SQLiteでSQL Alchemy構築+CRUD手順

# 前提
* 今西さんのyoutubeのflask解説がとても分かりやすかったので自分なりに第一回～第二回をまとめたものになります。
  (※リンクは参考を参照)
* 別途pipインストール必要になるもの
①flask
②flask_sqlalchemy
③pytz

* 別途sqliteのインストール必要(winの場合)
[①sqliteインストール方法](https://www.javadrive.jp/sqlite/install/index1.html)
[②sqliteのpathを通す方法](https://hasethblog.com/it/programming/sql/6291/)
[③VSCode上でsqliteを確認する方法](https://qiita.com/ritya/items/098835a96f3fcf7c6661)

# DB 新規作成手順
①プロジェクト直下にapp.pyを作成
②app.pyに以下を記載

```app.py
from datetime import datetime
# pythonでtimezone扱う場合のインポート
import pytz

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

# アプリケーションをインスタンス化
app = Flask(__name__)

# "sqlite:///blog.db"の部分を "sqlite:///hogehoge.db"と任意のDB名に変更
# 下記はinstanceというfolderが作られて、更にその中にblogというDBが作成されるという意味
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"

# インスタンス化したアプリケーションをSQLAlchemyに渡す
db = SQLAlchemy(app)

# modelを作成(別途modelsフォルダに切り出してもよい)
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(30), nullable=False)
    body = db.Column(db.String(300), nullable=False)
    # 世界協定時から日本の時刻に変更してdefault日時を登録するためにpytz.timezone使用する
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now(pytz.timezone("Asia/Tokyo")))

# 一覧画面へのルーティング
@app.route("/")
def index():
    return render_template("index.html")
```
③環境変数にFlskコマンドを使用するためのパスを通す
```
# Linuxの場合

$ export FLASK_APP=app    # app.pyを作成している場合はapp
$ export FLASK_ENV=development    # debug_modeがonになり、編集したファイルがサーバーを再起動せずに反映されるようになる
$ flask run
 * Running on http://127.0.0.1:5000/
```
 
```
# Win(CMD)の場合

> set FLASK_APP=app   # app.pyを作成している場合はapp
> set FLASK_ENV=development    # debug_modeがonになり、編集したファイルがサーバーを再起動せずに反映されるようになる
> flask run
 * Running on http://127.0.0.1:5000/
```

※この手順がうまくいくと flask ~ のコマンドが使用可能になる(例)flask shell など

④app.pyがおいてある階層にて下記実行
```
$ flask shell
>>> db.create_all()
```
または
```
>>> from app import db
>>> db.create_all()
```
※ db.create_all()にて[RuntimeError: Working outside of application context.]エラーが吐かれた際は下記参照
> https://stackoverflow.com/questions/73961938/flask-sqlalchemy-db-create-all-raises-runtimeerror-working-outside-of-applicat

⑤カレントディレクトリにinstanceフォルダが作成され、その中にblog.dbが作成されていればDB作成完了
※sqliteの中身の確認方法については前提の部分を参照のこと

# SQL Alchemyを使用した新規登録(CREATE)

①app.pyに createルーティングを追加
```app.py
from flask import Flask, render_template, request, redirect
.
.  省略
.

# /create のルーティングに GETかPOSTメソッドで来たら下記関数に入る
@app.route("/create", methods=["GET", "POST"])
def create():
    # POSTメソッドの場合の処理
    if request.method == "POST":
        # formから渡された各データを取得
        title = request.form.get("title")
        body = request.form.get("body")

        # 先で作成したmodelのクラス内にフォームから持ってきた各データをそれぞれ設定する
        post = Post(title=title, body=body)
        
        # セッションを設定したデータで追加してコミットを行う
        db.session.add(post)
        db.session.commit()
        return redirect("/")

    # GETメソッドの場合の処理
    else:
        return render_template("create.html")
```
app.pyに渡されるフォーム実装(参考)
```templates/create.html
{% extends "base.html" %}
{% block content %}
<h1>新規登録</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" name="title">

    <label for="body">内容</label>
    <input type="text" name = "body">

    <input type="submit" value="新規登録">
</form>
{% endblock %}
```

# SQL Alchemyを使用した参照(READ)

一覧画面(/index)のルーティングを追加
```app.py
@app.route("/", methods=["GET", "POST"])
def index():
    # "/"にGETメソッドで渡された場合の処理
    if request.method == "GET":
        # Postクラスからの全データをリスト形式で取得するORM
        posts = Post.query.all()
        # 取得したpostsという全データをindex.htmlに渡す
        return render_template("index.html", posts=posts)
```

③一覧画面のhtmlをDBからのデータを表示するように追加
```templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>
<a href="/create" role="button">新規作成画面</a>
<!-- app.pyから渡ってきたpostsをfor文で回し、一つずつ中のデータを取得・設定する -->
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <p>{{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

④flask run で確認すると記事を新規登録後に一覧画面で反映されていることが確認できる。

# SQL Alchemyを使用した更新(UPDATE)

①一覧画面の各記事内に編集のリンク先を追加する
```templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>
<a href="/create" role="button">新規作成画面</a>
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <!-- それぞれの記事内でidのルーティングをさせるように追記 -->
    <a href="/{{ post.id }}/update" role="button">編集</a>
    <p>{{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

②app.pyに updateルーティングを追加
```app.py
# 文字列は受け付けずint型のみ受け付けるようにする
@app.route("/<int:id>/update", methods=["GET", "POST"])
def update(id):
    # idを指定してデータを取得するORM
    post = Post.query.get(id)
    # GETの場合の処理
    if request.method == "GET":
        return render_template("update.html", post=post)
    # POSTの場合の処理(後程追加)
    else:
        return redirect("/")
```

③app.pyからレンダリングされるupdate.htmlを作成
```templates/update.html
{% extends "base.html" %}
{% block content %}
<h1>編集画面</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <!-- ページを開いたときにデフォルトで既に登録されている値を表示する為 -->
    <input type="text" name="title" value={{ post.title }}>

    <label for="body">内容</label>
    <!-- ページを開いたときにデフォルトで既に登録されている値を表示する為 -->
    <input type="text" name = "body" value={{ post.body }}>

    <input type="submit" value="更新">
</form>
{% endblock %}

```

④先ほどのapp.pyの/updateルーティングにPOST時の処理を追加する
```app.py
# 文字列は受け付けずint型のみ受け付けるようにする
@app.route("/<int:id>/update", methods=["GET", "POST"])
def update(id):
    # idを指定してデータを取得するORM
    post = Post.query.get(id)
    # GETの場合の処理
    if request.method == "GET":
        return render_template("update.html", post=post)
    # POSTの場合の処理
    else:
        # インスタンス化しているデータにformから受け取った各データを上書き
        post.title = request.form.get("title")
        post.body = request.form.get("body")

        # updateの場合はdb.session.add()は不要
        db.session.commit()
        return redirect("/")
```

⑤flask run で確認すると記事毎の編集ボタンクリック→更新後に一覧画面で更新が反映されていることが確認できる。

# SQL Alchemyを使用した削除(DELETE)

①一覧画面の各記事内に削除のリンク先を追加する
```templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>
<a href="/create" role="button">新規作成画面</a>
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <a href="/{{post.id}}/update" role="button">編集</a>
    <a href="/{{post.id}}/delete" role="button">削除</a>
    <p>{{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

②app.pyにルーティングを追加
```app.py
# 文字列は受け付けずint型のみ受け付けるようにする
@app.route("/<int:id>/delete", methods=["GET"])
def delete(id):
    # idを基にデータを取得するORM
    post = Post.query.get(id)
    # idを指定して取ってきた投稿をdeleteしてcommit
    db.session.delete(post)
    db.session.commit()
    # htmlを返すわけではないのでリダイレクトでOK
    return redirect("/")
```

③flask run で確認すると削除ボタンクリック時に記事が削除され一覧画面が再表示されることが確認できる。


# 最終ソースコード
```app.py
from datetime import datetime
import pytz

from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"

db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(30), nullable=False)
    body = db.Column(db.String(300), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now(pytz.timezone("Asia/Tokyo")))

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "GET":
        posts = Post.query.all()
        return render_template("index.html", posts=posts)

@app.route("/create", methods=["GET", "POST"])
def create():
    if request.method == "POST":
        title = request.form.get("title")
        body = request.form.get("body")

        post = Post(title=title, body=body)
        
        db.session.add(post)
        db.session.commit()
        return redirect("/")

    else:
        return render_template("create.html")

@app.route("/<int:id>/update", methods=["GET", "POST"])
def update(id):
    post = Post.query.get(id)
    if request.method == "GET":
        return render_template("update.html", post=post)
    else:
        post.title = request.form.get("title")
        post.body = request.form.get("body")

        db.session.commit()
        return redirect("/")

@app.route("/<int:id>/delete", methods=["GET"])
def delete(id):
    post = Post.query.get(id)
    db.session.delete(post)
    db.session.commit()
    return redirect("/")
```

```templates/base.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {% block content %}
    {% endblock %}
</body>
</html>
```

```templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>
<a href="/create" role="button">新規作成画面</a>
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <a href="/{{post.id}}/update" role="button">編集</a>
    <a href="/{{post.id}}/delete" role="button">削除</a>
    <p>{{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

```templates/create.html
{% extends "base.html" %}
{% block content %}
<h1>新規登録</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" name="title">

    <label for="body">内容</label>
    <input type="text" name = "body">

    <input type="submit" value="新規登録">
</form>
{% endblock %}
```

```templates/update.html
{% extends "base.html" %}
{% block content %}
<h1>編集画面</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" name="title" value={{ post.title }}>

    <label for="body">内容</label>
    <input type="text" name = "body" value={{ post.body }}>

    <input type="submit" value="更新">
</form>
{% endblock %}
```


# 参考
[今西さんのflask解説のyoutube(超わかりやすい)](https://www.youtube.com/watch?v=VtJ-fGm4gNg&t=439s)
[flask-alchemyの公式document](https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/quickstart/)
[flaskの公式document](https://flask.palletsprojects.com/en/2.0.x/quickstart/)