Docker-Flask-SQLAlchemyでメモアプリを作ってみた

## TL;DR
Docker・Flask・SQLAlchemyを使って簡単なメモアプリを作ってみました。
SQLAlchemyを使ってデータベースを作成〜データを取ってきてブラウザ上で表示するところまでやりました。
本記事ではメモアプリが完成するまでの手順と、詰まったところやその解決方法を書いていきます。

以下のリポジトリにサンプルコードを公開しました。フォルダ構成や記事の中で出てこないファイルなどはこちらをご覧ください。

サンプルコード→ https://github.com/hiseumn/flask-postgresql-sample

また、参考資料に前回の記事を掲載しています。一通り動くところまでの手順はそちらに書いたため、今回は省略します。

## 必要なライブラリのインストール
### SQLAlchemy
今回はSQL文を直接書かず、ORMを使用しました。
```zsh:terminal
rye add sqlalchemy
```

### uuid
保存するメモのIDとしてUUIDを使用しました。IDを時系列でソートしたかったので、ビルトインのv4ではなくv7を使用するため、以下のライブラリをインストールしてます。
```zsh:terminal
rye add uuid6
```

## 主なファイル構成
### app.py
```python:python
from flask import Flask, render_template, request
import os
from sqlalchemy import create_engine, desc
from sqlalchemy.orm import sessionmaker
from sample_flask_memo.infra.models.memo import Memo
from uuid6 import uuid7


app = Flask(__name__)

engine=create_engine(f"postgresql+psycopg://postgres:postgres@localhost:5432/memo")
Session = sessionmaker(bind=engine)
session = Session()


@app.route("/", methods=["GET"])
def index():
    memo_list=session.query(Memo).order_by('memo').all()
    print(memo_list)
    return render_template("index.html", memo_list=memo_list)


@app.route("/add", methods=["POST"])
def add():
    memo = request.form.get("memo")

    memo=Memo(id=uuid7(), memo=memo)
    session.add(memo)
    session.commit()
    return index()


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

```
※データベースに接続するためのIDとパスワードをハードコーディングすることは、セキュリティリスクになりかねないため真似しないでください。

### index.html

入力したメモが4列で表示される構造にしました。
```html:html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
    <title>テストページ</title>
  </head>
  <body>

    <div class="container text-center">
        <div class="row">
            <div class="col">
                <div class="mb-3">
                <form action="/add" method="POST">
                    <div class="mb-3 form-check">
                      <label for="exampleFormControlTextarea1" class="form-label">登録するメモ</label>
                      <textarea class="form-control" id="exampleFormControlTextarea1" name="memo" rows="3"></textarea>
                    </div>
                    <button type="submit" class="btn btn-primary">送信</button>
                </form>
                </div>
            </div>
            </div>
        <div class="row">
          <div class="col">
            {% for memo in memo_list %}
              {%- if loop.index0 % 4 == 0 -%}
                  <div class="card">
                      <div class="card-body">
                      <h4>{{memo.id}}</h4>
                      <p>{{memo.memo}}</p>
                      </div>
                    </div>
              {%- endif %}</li>
            {% endfor %}
          </div>
          <div class="col">
            {% for memo in memo_list %}
              {%- if loop.index0 % 4 == 1 -%}
                  <div class="card">
                      <div class="card-body">
                      <h4>{{memo.id}}</h4>
                      <p>{{memo.memo}}</p>
                      </div>
                    </div>
              {%- endif %}</li>
            {% endfor %}
          </div>
          <div class="col">
            {% for memo in memo_list %}
              {%- if loop.index0 % 4 == 2 -%}
                  <div class="card">
                      <div class="card-body">
                      <h4>{{memo.id}}</h4>
                      <p>{{memo.memo}}</p>
                      </div>
                    </div>
              {%- endif %}</li>
            {% endfor %}
          </div>
          <div class="col">
            {% for memo in memo_list %}
              {%- if loop.index0 % 4 == 3 -%}
                  <div class="card">
                      <div class="card-body">
                      <h4>{{memo.id}}</h4>
                      <p>{{memo.memo}}</p>
                      </div>
                    </div>
              {%- endif %}</li>
            {% endfor %}
          </div>
        </div>
      </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

```
for文の説明
- `{% for memo in memo_list %}`は、`memo_list`というリスト内の各メモを一つずつ取り出し、それを`memo`という変数に代入して処理するループを示しています
- `<h4>{{memo.id}}</h4>`は、各メモのIDがそれぞれヘッダーに表示されます
- `<p>{{memo.memo}}</p>`は、ブラウザ上で入力したメモが表示されます
- `memo_list`から各メモを取り出して表示します。それぞれのメモはカード形式で表示されます

### memo.py
```python:python
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import Column
from uuid6 import uuid7
from sqlalchemy_utils import UUIDType
from sqlalchemy.types import Text

Base = declarative_base()
class Memo(Base):
    __tablename__ = "memo"  # テーブル名を指定
    id = Column(UUIDType(binary=False), primary_key=True, default=uuid7)
    memo = Column(Text)

    def __repr__(self):  # メモの内容を返す
        return f"{self.id} {self.memo}"
    
```
- `Base`は、後で定義する全てのモデルクラスが継承するベースクラスです
- `class Memo(Base)`は、SQLAlchemyのモデルクラスで`Base`を継承しています
- `def __repr__(self)`は、オブジェクトの文字列表現を定義し、デバッグやログ出力時に役立ちます。`self.id`と`self.memo`の内容を返します

## 詰まったところ
### 入力したデータを表示できない
#### そもそも全く別のファイルからデータを取り出そうとしていた
最初はhttp statusで304エラーが出てきてしまい、ひとつずつエラーが出てきたコードを調べながら進めていました。
しかし、入力したデータが表示されないどころか全く違うところからデータを引っ張ってきていたことがわかりました。

### データベースに接続できない
#### psycopgが必要だったが実行するまでエラーとならなかった
![スクリーンショット 2024-07-03 11.40.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802241/46bce5ba-16b6-e60e-f4a5-4cca861166d0.png)
上の画像は、psycopg2をインストールした際のエラー文です。
データベースに接続するためのドライバーが必要だったので、ターミナルで`rye add psycopg2`を入力して解決しました。

##　参考資料

https://developer.mozilla.org/ja/docs/Web/HTTP/Status/304

https://qiita.com/hiseumn/items/5992c6818a18210f84f8

以上