PR
PR

【Python_study_Day19】ユーザ登録機能に向けたデータベース設計と構築

記事内に広告が含まれています。

Pythonの学習19日目です。

前回はPodmanとpodman-composeを使って、開発環境の構築を行いました。

今回は、Webアプリの要である「ユーザ登録」と「ログイン機能」を実装するために必要な、データベースの構築(設計)を進めていきます。

データベース設計のアプローチ

ユーザ情報や日々の記録をどのように保存するか、以下のステップで整理しながら設計していきます。

  • 保存するデータの種類(エンティティ)の洗い出し
  • 各データに必要な項目(カラム)の定義

データの種類(エンティティ)の洗い出し

まず、今回作成するサービスで「何を保存する必要があるか」を考えます。

要件を確認した結果、以下の2つのデータ(テーブル)が必要だと分かりました。

  • ユーザ情報(users):サービスを使用するユーザ自身の情報
  • 作業時間(time_logs):ユーザーが行った作業内容と時間の記録

項目(カラム)を決める

データの種類(エンティティ)の洗い出しが終わったら、次はそれぞれに含まれる項目(カラム)を具体的に決めていきます。

ユーザ情報テーブル(users)

ユーザを管理するためのテーブルです。パスワードはセキュリティを考慮し、そのまま保存するのではなくハッシュ化して保存します。

  • ユーザID(主キー): id
  • ユーザ名:username
  • メールアドレス:email (重複禁止)
  • パスワード:password_hash
  • 登録日: created_at

作業時間(time_logs)

日々の学習や作業を記録するためのテーブルです。

  • ログID(主キー): id
  • 誰の記録(外部キー): user_id (usersテーブルのidへリンク)
  • 作業内容: task_name (Python学習、英語学習等)
  • カテゴリ: category (勉強、仕事等)
  • 開始日時:start_time
  • 終了日時:end_time
  • メモ:note (メモを記録できるようする)
外部キーの命名について

time_logs テーブルで、誰の記録かを紐付けるために user_id というカラムを用意しました。

ここでポイントなのが命名規則です。

外部キーを決める際は、リンク先の「テーブル名」ではなく、プログラム上で扱う「モデル名(クラス名)」を基準にするという慣習(暗黙のルール)がよく使われようです。

  • テーブル名:users
  • モデル名:User (単数形・大文字始まりが一般的)

そのため、「指し示す相手のモデル名(単数) + _id」 というルールで user_id と命名しています。

フレームワークによってはこの命名規則に従うことで自動的にリレーションを認識してくれるものもあるため、覚えておくと便利です。

ここまでの疑問点:主キー、外部キー、命名のルール

データベース設計を進める中で出てきた、重要な用語と疑問点についてまとめておきます。

主キー(Primary Key / プライマリーキー)とは?

データを特定するための「背番号」のようなものです。

そのテーブルの中で、行(データ)を一つに特定するために絶対に必要な項目のことを指します。

主キーに設定する項目には、必ず守らなければならない3つのルールがあります。

  • 重複の禁止:同じIDは存在できない
  • 空っぽの禁止:必ず値が入っていなければならない / NULL不可
  • 途中変更の禁止:一度決めたら原則変えない

多くの場合、id という名前のカラムを作成し、自動的に連番が割り当てられるように設定するのが定石です。

外部キー(Foreign Key / フォーリンキー)とは?

別のテーブルの「主キー」と紐付けるための、架け橋となる項目です。

今回の例では、time_logs(作業記録)テーブルから、users(ユーザ)テーブルの特定のユーザを指し示すために使われます。

「id」という名前がテーブル間で重複しているけど大丈夫?

各テーブルを設計する際、Gemini先生から「どちらのテーブルも主キーは id にしましょう」と提案されました。 ここで「テーブルごとにuser_idやlog_idのように名前を変えなくて良いの?」という疑問が湧きました。

結論から言うと、今回の開発環境(Python + Flask + SQLAlchemy)においては、単に「id」とするのが一般的です。

その理由は、Pythonのコードで書いた時の「読みやすさ」にあります。

SQLAlchemyのようなORM(Object Relational Mapper)を使うと、データベースのデータはPythonの「オブジェクト(クラスのインスタンス)」として扱われます。

もし、テーブル定義で user_id という名前にしてしまうと、Pythonコードは以下のようになってしまいます。

user = User.query.get(1)
print(user.user_id) # 「ユーザー情報の、ユーザーID」…ちょっとくどい

一方、シンプルに id とした場合、非常に自然なコードになります。

user = User.query.get(1)
print(user.id)  # 「ユーザー情報のID」と自然に読める

User クラスを使っている時点で「ユーザの情報であること」は自明なので、プロパティ名は単に id や name とする方が、コードがシンプルで読みやすくなるのです。

データベースの作成

洗い出した項目をもとに、具体的なデータ型や「必須項目かそうでないか」を定義しました。

これを「スキーマ定義」と呼びます。ここがしっかり決まっていると、後のプログラミングがとても楽になります。

使用するデータ型について

今回は以下の4つの型を使用します。

  • Integer: 数字(IDや個数など)
  • String: 短い文字列(名前、メールアドレスなど)
  • Text: 長い文章(メモ、本文など)
  • DateTime: 日付と時刻

これらを使って、2つのテーブルを定義します。

ユーザ情報テーブル (users)

サービスを利用するユーザを管理するテーブルです。

ログインに必要な情報はすべてここで管理します。

項目名 物理名(カラム名) データ型 必須 説明
ユーザID id Integer 主キー。ユーザーの背番号。自動で1, 2, 3...と増える。
ユーザ名 username String ユーザー名(表示用)。
メールアドレス email String ログイン用ID。システム内で重複禁止。
パスワード password_hash String 暗号化(ハッシュ化)されたパスワード。
登録日 created_at DateTime アカウント作成日時。

作業時間テーブル (time_logs)

日々の作業記録を保存するテーブルです。

項目名 物理名(カラム名) データ型 必須 説明
ログID id Integer 主キー。ログの管理番号。
ユーザID user_id Integer 外部キー。「誰の記録か」を示すためusersテーブルのidを保存。
作業内容 task_name String タスク名(例:Python学習)。
カテゴリ category String - カテゴリ(例:勉強、仕事)。任意入力。
開始日時 start_time DateTime 作業を開始した日時。
終了日時 end_time DateTime - 作業終了日時。作業中は空っぽ (Null)になるため必須ではない。
メモ note Text - 自由記述のメモ。長文対応のためText型。
ポイント

終了日時の扱い end_time(終了日時)の「必須」が空欄になっているのがポイントです。

「作業開始」ボタンを押した時点では、まだ作業は終わっていないため終了日時は決まっていません。

そのため、「空っぽ(Null)の状態を許容する」設定にしておく必要があります。

データベース操作の準備

データベースの設計図(スキーマ)ができたら、次はそれをPythonで操作するための下準備を行います。

具体的には、以下の2つのファイルを作成していきます。

  • extensions.py: 便利な道具(ライブラリ)を管理する場所
  • models.py: データベースの設計図をPythonコードにしたもの

拡張機能ファイル(extensions.py)

このファイルの役割は、「データベースやログイン管理などのライブラリ(道具)を、いつでも使える状態で準備しておくこと」です。

# ライブラリのインポート
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager

# インスタンス(空箱)の作成
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()

各ライブラリの解説

ここでインスタンス化している3つのライブラリは、Webアプリ開発の「三種の神器」とも言える重要なツールです。

SQLAlchemy:
データベースと会話するための「通訳」ツール
これがないと、生のSQLを書くことになる
Migrate:
データベースの構造を変更(マイグレーション)するのために使用
「flask db」コマンドの実体
LoginManager:
ログイン・ログアウトを管理するのに使用
「今のユーザーは誰?」「ログインしてる?」という状態を管理してくれる

なぜ「引数なし」で作成するのか?

dbという名前の「実体(オブジェクト)」を、「db = SQLAlchemy()」といったように引数なしで作成しています。

通常であれば SQLAlchemy(app) のように、アプリ本体(app)を渡して紐付けたくなりますが、ここではあえて「空箱」の状態で作っています。

これは、後で __init__.py の中で db.init_app(app) という命令を使って、後付けでアプリ本体と合体させるためです。

そのため、「migrate = Migrate()」や「login_manager = LoginManager()」も同様の理由で、引数なしで記述しています。

循環参照(Circular Import)の回避

なぜ、わざわざ「空箱作成」と「後付け合体」という手順を踏むのでしょうか?

それは、「ファイル同士がお互いを呼び出し合って動かなくなる事故(循環参照)」を防ぐためです。

もしextensions.pyを作らず、すべてを __init__.py で行おうとすると、以下のような膠着状態(デッドロック)が起こりやすくなります。

  • __init__.py: 「アプリを作るぞ! あ、データベースを使うために models.py を読み込まなきゃ」
  • models.py: 「モデルを作るぞ! あ、データベースの設定が必要だから __init__.py を読み込まなきゃ」
  • Python: 「__init__ を読むにはmodelsが必要で、modelsを読むには__init__が必要で……終わりがない!!エラー!!!」

これがお互いの完成を待って永遠に動かなくなる「循環参照」です。

これを防ぐために、extensions.py という「第三の場所」を作ります。

  • extensions.py: 「誰にも依存しない、空のデータベースを管理する道具(db)だけ置いておくよ。」
  • models.py: 「extensions.py から db を借りるね。(__init__.py には用はないよ)」
  • __init__.py: 「extensions.py から db を借りて、アプリ本体と合体させるね。」

こうすることで、依存の矢印がグルグル回らず、上から下へとスムーズに流れるようになります。

これが extensions.py を作成する最大の理由です。

モデル (models.py) の作成:データベースの設計図

データベースはSQLという言語しか理解できないため、このままではPythonとスムーズにやり取りができません。

そこで、「Pythonのこのコードは、データベースのこの箱のことだよ!」と翻訳してあげる役割が必要になります。

これが「モデル」であり、この仕組みを ORM (Object Relational Mapping) と呼びます。

このモデルを作成(定義)することで、「Flask-Migrate」がそれを読み取り、実際のSQLに変換してテーブルを作成してくれます。

モデルがない場合は、Pythonのコードに直接SQLを記述することになります。

from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app.extensions import db, login_manager

# --- 1. ユーザーモデル (usersテーブル) ---
class User(db.Model, UserMixin):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    email = db.Column(db.String(120), unique=True, index=True)
    password_hash = db.Column(db.String(256))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # リレーション設定:ユーザーからログを参照できるようにする
    logs = db.relationship('TimeLog', backref='author', lazy='dynamic')

    # パスワードをセットする時は暗号化する
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    # パスワードのチェック機能
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return f'<User {self.username}>'

# Flask-Login用:IDからユーザーを読み込む関数
@login_manager.user_loader
def load_user(id):
    return User.query.get(int(id))


# --- 2. タイムログモデル (time_logsテーブル) ---
class TimeLog(db.Model):
    __tablename__ = 'time_logs'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 外部キー
    
    task_name = db.Column(db.String(140))
    category = db.Column(db.String(64))
    start_time = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    end_time = db.Column(db.DateTime, nullable=True)
    note = db.Column(db.Text, nullable=True)

    def __repr__(self):
        return f'<TimeLog {self.task_name}>'

以下はコードの解説です。

usersテーブルの定義

class User(db.Model, UserMixin):
    __tablename__ = 'users'

Userというクラスを作成し、__tablename__ = 'users' で実際のデータベース上のテーブル名を指定しています。

  • db.Model: これを継承することで、単なるPythonのクラスではなく「データベースのモデル」であることをSQLAlchemyに伝えます。
  • UserMixin: Flask-Loginが「このユーザーはログイン中か?」などを管理するために必要な便利機能を自動で追加してくれます。

カラム(項目)の設定

以下の部分で、テーブル内に作成するカラム(項目)を設定しています。

  • id = db.Column(db.Integer, primary_key=True)
  • username = db.Column(db.String(64), unique=True, index=True)
  • email = db.Column(db.String(120), unique=True, index=True)
  • password_hash = db.Column(db.String(256))
  • created_at = db.Column(db.DateTime, default=datetime.utcnow)

設定は カラム名 = db.Column(データ型, オプション...) の書式で行います。

データ型の説明
型の名前 コードの書き方 意味
整数 db.Integer 1, 2, 100 などの数字
短い文字 db.String(文字数) 文字数制限ありの文字
長い文字 db.Text 制限なしの長文
日時 db.DateTime 日付と時刻
真偽値 db.Boolean True か False
小数 db.Float 1.5, 3.14 などの小数
主なオプション
オプション 書き方 意味
主キー primary_key=True そのテーブルの主役(ID)にする
必須 nullable=False 空っぽ(Null)を禁止する
重複禁止 unique=True 他と同じ値を入れたらエラーにする
初期値 default=値 何も入力されなかったらこの値を入れる
外部キー db.ForeignKey('...') 別のテーブルと紐付ける
索引 index=True 検索を爆速にする目印をつける

リレーション(関係性)の設定

logs = db.relationship('TimeLog', backref='author', lazy='dynamic')

この1行ががあるおかげで、本来なら複雑なSQL(JOINなど)を書かないとできない操作が、Pythonだけで直感的にできるようになります。

TimeLog

紐付ける相手のクラス名(モデル名)を指定します。

backref='author'

これを設定すると、相手(TimeLog)側にも「著者は誰か」を知るためのショートカットが作成されます。

これにより、log.author と書くだけで、そのログを書いたUserの情報にアクセスできるようになります。

lazy='dynamic'

れを設定しない場合、user.logs を実行した瞬間に、そのユーザーの全記録をリストとして取得してしまいます。

ログが10,000件あるとメモリを圧迫します。

  • メモリを多く使用し圧迫する
  • 特定期間のログだけが必要な場合も、一度全てのログを取得してから必要な部分を選別する必要がある。

lazy='dynamic' を設定すると、データそのものではなく「クエリ(検索するための準備)」が返ってきます。 これにより、user.logs.filter(...) のように後から条件を追加したり、必要な分だけ取得したりすることが可能になります。

これにより、user.logs.filter(...) のように後から条件を追加したり、必要な分だけ取得したりすることが可能になります。

パスワードのハッシュ化

def set_password(self, password):
        self.password_hash = generate_password_hash(password)

セキュリティのため、パスワードはそのまま保存しません。werkzeug.securityのgenerate_password_hash関数を使い、暗号化(ハッシュ化)して保存します。

def check_password(self, password):
        return check_password_hash(self.password_hash, password)

チェックする際もcheck_password_hashを使い、入力されたパスワードと保存されたハッシュ値が合致するかを安全に判定します。

開発者用の表示設定

def __repr__(self):
        return f'<User {self.username}>'

__repr__ (representation) は、主に開発者がデバッグやログ確認を行う際、オブジェクトの中身を一目で分かりやすくするための「親切設計」です。

もしこの設定がない場合、Pythonのインタラクティブシェルやログでユーザーオブジェクトを表示すると、以下のような無機質な情報が表示されてしまいます。

  • __repr__ がない場合: <User object at 0x7f8b1c2d3e4f> (メモリアドレスが表示され、どのユーザーか分からない)
  • __repr__ がある場合: <User ユーザ名> (ユーザー名が表示され、誰のデータかすぐに分かる)

このコードは、オブジェクト(インスタンス)が「文字列として表現されるとき」に、どのような形式で表示するかを定義しています。

  • f'...': f-string(フォーマット済み文字列リテラル)を使用して、簡潔に文字列を作成しています。
  • <User ...>: 慣習として、クラス名を括弧で囲み、その中に識別情報を入れる形式がよく好まれます。
  • self.username: オブジェクトが持っている username 属性の値(例: 'tamohiko')を埋め込んでいます。

ユーザ読み込み関数(load_user)

@login_manager.user_loader
def load_user(id):
    return User.query.get(int(id))

Flask-Loginが「ログイン中のユーザー情報」をセッションから復元するために使う関数です。

  • @login_manager.user_loader: 「これがユーザー読み込み用の関数だよ」と教えるデコレータです。
  • int(id): セッション内のIDは文字列として渡されることがあるため、念のため整数(int)に変換してからデータベースを検索します。

app/__init__.py:アプリの心臓部を作る

このファイルは、Flaskアプリケーションのインスタンスを作成し、データベースやログイン機能などの各種設定をまとめて行っています。

import os
from flask import Flask
# extensionsから読み込むのがポイント
from app.extensions import db, migrate, login_manager

def create_app():
    app = Flask(__name__)

    # --- 環境変数から設定を読み込む ---
    # docker-compose.yml で設定した DATABASE_URL がここに入ってくる
    app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
    app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    # --- 拡張機能の初期化 ---
    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    login_manager.login_view = 'auth.login' # ログイン画面の場所指定(あとで実装予定)

    # --- モデルの読み込み ---
    # これを書かないとFlaskはデータベースのテーブルを作成しない
    from app import models

    # --- 仮のトップページ ---
    @app.route('/')
    def index():
        return "Database Setup Complete!"

    return app

重要なポイントの解説です。

拡張機能のインポート

from app.extensions import db, migrate, login_manager

「_init__.py」で「db = SQLAlchemy(app)」と書いてしまいがちですが、そうすると他のファイルと相互にインポートし合う「循環インポートエラー」が起きやすくなります。

拡張機能を別ファイル (extensions.py) に切り出して読み込むことで、この問題を回避しています。

アプリケーションファクトリパターン (create_app)

def create_app():
    app = Flask(__name__)
    ### 中略 ###
    return app

app = Flask(__name__) ファイルの先頭でグローバル変数として書くのではなく、関数の中に閉じ込めています。

この方式を 「アプリケーションファクトリパターン」 と呼びます。

これにより、テスト時や異なる設定で複数のアプリを起動したい場合に、柔軟に対応できるようになります。

メリット1:テスト時の「汚染」を防ぐ(独立性)

appがグローバル変数(プログラム全体で1つだけの存在)だとすると、テストを行う際に以下のような問題が起きます。

  1. テストAで「ユーザーを1人追加」する。
  2. その状態がappに残ったまま、次のテストB(ユーザーが0人の状態確認)が開始される。
  3. テストBは「ユーザーが0人の状態」を確認したいのに、テストAの影響で失敗してしまう。

ファクトリ関数(create_app)の場合は、テストを実行するたびに app = create_app() を呼び出して「新品のアプリ」を作ることができ、常にクリーンな状態でテストできるため、バグを見つけやすくなります。

  1. テストを実行するたびに app = create_app() を呼び出す。
  2. テストA用のappと、テストB用のappは完全に別のインスタンスになる。
  3. そのため、常にクリーンな状態でテストできるため、バグを見つけやすくなる。
メリット2:環境ごとの設定切り替えが簡単(柔軟性)

開発、テスト、本番では、読み込みたい設定(データベースの場所など)が異なります。

関数にしておけば、引数を渡すだけで設定を切り替えたアプリを作成できます。

# 開発用のアプリを作る
app_dev = create_app('dev_config')

# テスト用のアプリを作る(本番DBを壊さないように別のDBを使う設定など)
app_test = create_app('test_config')

もしグローバル変数で書いていると、この切り替えを行うためにコード自体を書き換えたり、複雑な分岐処理を入れたりする必要が出てきてしまいます。

環境変数とDockerへの対応

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')

os.environ.getを使って、環境変数から値を取得しています。

これにより、Pythonコードの中にパスワードを直接書く必要がなくなり、セキュリティが向上します。

また、Dockerの設定ファイル(docker-compose.yml)側で接続先を変更できるため、運用も楽になります。

拡張機能の初期化

db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)

作成したappインスタンスを、それぞれの拡張機能に紐付けて「使える状態」にしています。

login_manager.login_view = 'auth.login'

login_view の設定は、未ログインユーザーが保護されたページにアクセスした際、自動的にログイン画面へ転送するためのものです。

モデルの読み込み (from app import models)

from app import models

ここでmodelsをインポートしないと、Flask-Migrate(Alembic)がデータベースのモデル定義を認識できず、「マイグレーションファイルが作成されない」「テーブルが作られない」というトラブルが発生します。

アプリが起動するタイミングで、モデルの存在を知らせるために記述しています。

データベースの構築

いよいよ、先ほど作成したモデル(設計図)をもとに、実際のデータベースを構築していきます。

ここで一つ重要なポイントがあります。

データベース操作に必要なライブラリ((Flask-Migrateなど)は、自分のパソコン(ホストOS)ではなく、Podmanで作成した「コンテナの中」にインストールされています。

そのため、ホスト側のターミナルから直接コマンドを打つのではなく、一度コンテナの中に「ログイン」してから操作を行う必要があります。

コンテナへの接続手順

まず、docker-compose.yml ファイルが存在するディレクトリまで移動し、以下のコマンドを実行します。

podman-compose exec コンテナ名 bash
  • podman-compose exec: 起動中のコンテナ内でコマンドを実行する命令です。
  • ンテナ名: docker-compose.yml で定義したサービス名(コンテナ名)です。
  • bash: コンテナ内で起動したいシェルです。

今回は「app」コンテナの中で「bash」を起動します。

$ podman-compose exec app bash

画面のプロンプトが以下のように変われば、コンテナへのログインは成功です。

root@e984c9539da4:/app# 

e984c9539da4の部分はコンテナIDとなるので、環境によって異なります。

  • root: 現在のユーザー(管理者権限)
  • e984c9539da4: コンテナID(※この文字列は環境や起動ごとに異なります)
  • /app: コンテナ内の現在いるディレクトリ

マイグレーションフォルダの作成(初回のみ)

データベースのバージョン管理を開始するために、まずはFlask-Migrate(内部ではAlembicというツールが動いています)の初期化を行います。

「flask db init」コマンドを実行して、マイグレーション情報を保存するための専用フォルダを作成しましょう。 ※この操作は、プロジェクトのセットアップ時に一度だけ行います。

コマンドを実行すると、以下のようなメッセージが表示され、必要なファイル群が自動生成されます。

root@e984c9539da4:/app# flask db init
  Creating directory /app/migrations ...  done
  Creating directory /app/migrations/versions ...  done
  Generating /app/migrations/alembic.ini ...  done
  Generating /app/migrations/env.py ...  done
  Generating /app/migrations/README ...  done
  Generating /app/migrations/script.py.mako ...  done
  Please edit configuration/connection/logging settings in /app/migrations/alembic.ini before proceeding.

これで、カレントディレクトリに 「migrations」フォルダが作成されました。

今後、テーブルを作成したり変更したりするたびに、その変更内容を記録したファイルがこのフォルダの中に保存されていきます。

migrations/
├── README
├── alembic.ini     # 設定ファイル
├── env.py          # マイグレーション実行時のスクリプト
├── script.py.mako  # マイグレーションファイルのテンプレート
└── versions        # 変更履歴ファイルの保存場所

マイグレーションファイルの作成(変更の検出)

続いて、「flask db migrate」コマンドを実行してマイグレーションファイルを作成します。

このコマンドは、Pythonコード(models.py)で定義したモデルの内容と、現在のデータベースの状態を比較し、その差分(足りないテーブルや変更点)を自動的に検出してくれます。

「-m」オプションの後ろには、Gitのコミットメッセージのように「今回の変更内容」を記述します。

今回は最初なので "Initial migration"(初期マイグレーション)としています。

root@e984c9539da4:/app# flask db migrate -m "Initial migration"
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'users'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_email' on '('email',)'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '('username',)'
INFO  [alembic.autogenerate.compare] Detected added table 'time_logs'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_time_logs_start_time' on '('start_time',)'
  Generating /app/migrations/versions/0db7d32200c7_initial_migration.py ...  done

表示されたログに、「Detected added table ...」 といった表示があれば、作成したモデルが正しく認識されています。

コマンド完了後、「migrations/versions」ディレクトリの中に、実行時のログに記述されている「0db7d32200c7_initial_migration.py」といった名前のファイルが新しく作成されます。(※ファイル名の先頭にある英数字はランダムなIDなので、環境によって異なります)

これが、データベースを変更するための「変更手順書(スクリプト)」となります。

この「flask db migrate」コマンドは、あくまで「変更手順書(スクリプト)」を作成しただけです。

注意 「flask db migrate」コマンドは、あくまで「変更手順書」を作成しただけなので、この時点ではまだ、実際のデータベースにテーブルは作成されていません。

変更の適用(テーブル作成)

作成した「変更手順書(マイグレーションファイル)」を使って、実際のデータベースにテーブルを作成します。

flask db upgradeコマンドを実行して、変更を適用(データベースのテーブルを作成)します。

root@e984c9539da4:/app# flask db upgrade
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 0db7d32200c7, Initial migration

一番下の行に Running upgrade -> ... と表示されれば成功です!

これで、定義したusersテーブルや time_logsテーブルがデータベース内に作成されました。

flask db upgradeコマンドで行われたこと

「flask db upgrade」コマンドは、以下の作業を行ってくれています。

  • flask db migrate で作成されたマイグレーションファイルを読み込む。
  • その内容を SQL(データベースを操作する言語) に自動変換する。
  • データベースに対してそのSQLを実行し、テーブル作成やカラム追加を行う。

コンテナからのログアウト

コンテナ内での作業が完了したら、exit コマンドを実行してコンテナから抜けます。

root@e984c9539da4:/app# exit
exit
exit code: 0

プロンプトの表示が元のパソコン(ホスト側)のものに戻れば、ログアウト完了です。

作成したデータベースの確認

作成されたテーブルが正しく反映されているか、実際にデータベースに直接アクセスして確認してみましょう。

以下のコマンド構造で、データベース(PostgreSQL)に接続できます。

podman-compose exec コンテナ名 psql -U ユーザ名 -d DB名
ユーザ名=# \dt             (テーブルを表示)
ユーザ名=# \d テーブル名   (テーブル定義(構造)の表示)
ユーザ名=# \q              (終了)

データベースへの接続

実際にコマンドを実行して確認していきます。

今回は dbという名前のコンテナ(PostgreSQLコンテナ)に対して、データベース用のユーザー「timereport_user」で、「timereport_db」データベースに接続します。

$ podman-compose exec db psql -U timereport_user -d timereport_db

「timereport_db=#」というプロンプトに変われば接続成功です。

テーブル一覧の表示

「\dt」(Display Tables)コマンドを実行し作成されたテーブルをを表示します。

  • users, time_logs: 今回作成したテーブルです。
  • alembic_version: マイグレーションツール(Flask-Migrate/Alembic)がバージョン管理のために自動生成したテーブルです。
timereport_db=# \dt
                 List of relations
 Schema |      Name       | Type  |      Owner      
--------+-----------------+-------+-----------------
 public | alembic_version | table | timereport_user
 public | time_logs       | table | timereport_user
 public | users           | table | timereport_user
(3 rows)

テーブル定義(構造)の確認

「\d テーブル名」コマンドを実行すると、テーブル定義(構造)を表示させることができます。

※このコマンドは、「構造(どんなカラムがあるか)」を表示するもので、中身のデータを表示するものではありません。

実際に「\d users」コマンドを実行して、usersテーブルの定義を表示してみます。

timereport_db=# \d users
                                          Table "public.users"
    Column     |            Type             | Collation | Nullable |              Default              
---------------+-----------------------------+-----------+----------+-----------------------------------
 id            | integer                     |           | not null | nextval('users_id_seq'::regclass)
 username      | character varying(64)       |           |          | 
 email         | character varying(120)      |           |          | 
 password_hash | character varying(256)      |           |          | 
 created_at    | timestamp without time zone |           |          | 
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "ix_users_email" UNIQUE, btree (email)
    "ix_users_username" UNIQUE, btree (username)
Referenced by:
    TABLE "time_logs" CONSTRAINT "time_logs_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

カラムの型(Type)や、ユニーク制約(Indexes)、外部キー制約(Referenced by)などが、Pythonのモデル定義通りに設定されていることがわかります。

終了コマンド

確認が終わったら、\q(Quit)コマンドを実行してデータベース接続を終了します。

timereport_db=# \q
exit code: 0

コメント

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