Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part2

## はじめに

前回のPart1では、DailyBeatプロジェクトの概要、技術選定の理由、およびアーキテクチャ設計について解説しました。今回のPart2では、データベース設計とユーザー認証の実装について詳しく見ていきます。

## データベース設計

DailyBeatでは、SQLAlchemyを使用してORMベースのデータベース設計を行っています。主要なモデルは以下の通りです：

1. User（ユーザー情報）
2. DailySong（日々の楽曲投稿）
3. Follow（フォロー関係）

### 1. Userモデル

```python:models/user.py
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    daily_songs = db.relationship('DailySong', backref='author', lazy='dynamic')
    
    def __repr__(self):
        return f'<User {self.username}>'
    
    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)

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

**コード解説：**
- `UserMixin`: Flask-Loginが要求する各種メソッドを提供します。
- `db.relationship`: DailySongモデルとの1対多の関係を定義しています。
- `set_password`と`check_password`: パスワードのハッシュ化と検証を行います。
- `@login.user_loader`: Flask-Loginがユーザーをロードする際に使用する関数です。

### 2. DailySongモデル

```python:models/daily_song.py
from datetime import datetime
from app import db

class DailySong(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    artist = db.Column(db.String(100), nullable=False)
    genre = db.Column(db.String(50))
    spotify_url = db.Column(db.String(200))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    def __repr__(self):
        return f'<DailySong {self.title} by {self.artist}>'
```

**コード解説：**
- `nullable=False`: 必須フィールドを指定します。
- `db.ForeignKey('user.id')`: Userモデルとの関連を定義します。
- `timestamp`: 投稿日時を自動的に記録します。

### 3. Followモデル

```python:models/follow.py
from app import db

class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)
```

**コード解説：**
- このモデルは、ユーザー間のフォロー関係を表現します。
- `follower_id`と`followed_id`の両方が主キーとなり、ユニークな関係を保証します。

## データベースマイグレーション

FlaskアプリケーションでSQLAlchemyを使用する際、データベースのスキーマ変更を管理するためにFlask-Migrateを使用します。

```python:migrations/env.py
from alembic import context
from flask import current_app

config = context.config
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata

def run_migrations_online():
    with current_app.app_context():
        engine = current_app.extensions['migrate'].db.get_engine()
        with engine.connect() as connection:
            context.configure(
                connection=connection,
                target_metadata=target_metadata
            )
            with context.begin_transaction():
                context.run_migrations()

run_migrations_online()
```

**マイグレーションの実行方法：**
1. マイグレーションの初期化: `flask db init`
2. マイグレーションスクリプトの作成: `flask db migrate -m "Initial migration"`
3. データベースの更新: `flask db upgrade`

## ユーザー認証の実装

ユーザー認証はFlask-Loginを使用して実装しています。

### 1. ログイン機能

```python:views/auth.py
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user
from app import app, db
from app.models import User
from app.forms import LoginForm

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    return render_template('login.html', title='Sign In', form=form)
```

**コード解説：**
- `current_user.is_authenticated`: ユーザーが既にログインしているかチェックします。
- `form.validate_on_submit()`: フォームのバリデーションを行います。
- `User.query.filter_by()`: ユーザーをデータベースから検索します。
- `login_user()`: ユーザーをログイン状態にします。
- `next_page`: ログイン後のリダイレクト先を管理します。

### 2. ログアウト機能

```python:views/auth.py
@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))
```

**コード解説：**
- `logout_user()`: ユーザーをログアウト状態にします。

### 3. ユーザー登録機能

```python:views/auth.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)
```

**コード解説：**
- 新しいユーザーオブジェクトを作成し、パスワードをハッシュ化して保存します。
- `db.session.add()`と`db.session.commit()`: データベースに新しいユーザーを追加します。

## セキュリティ対策

1. **パスワードハッシュ化**: `werkzeug.security`の`generate_password_hash`を使用し、平文のパスワードは保存しません。
2. **CSRF対策**: Flask-WTFを使用し、フォームにCSRFトークンを自動的に追加しています。
3. **セッション管理**: Flask-Loginを使用し、セキュアなセッション管理を行っています。

## まとめ

Part2では、DailyBeatのデータベース設計とユーザー認証の実装について詳しく解説しました。SQLAlchemyを使用したORMベースの設計により、データベース操作を簡潔に記述でき、また将来的なデータベース移行も容易になっています。

Flask-LoginとFlask-WTFを組み合わせることで、セキュアで使いやすいユーザー認証システムを実現しています。これらのライブラリを使用することで、開発時間を短縮しつつ、セキュリティリスクを最小限に抑えることができます。

次回のPart3では、楽曲投稿機能とタイムライン機能の実装について詳しく解説する予定です。お楽しみに！

## 参考リンク
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part1](https://qiita.com/taipilo/items/62490dfa93e41d4be0b4)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part3](https://qiita.com/taipilo/items/bce5d59c0a4c5fbc8a8c)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part4](https://qiita.com/taipilo/items/b5a6e8d6dacfba51a465)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part5（最終回）](https://qiita.com/taipilo/items/ee5566c4f7601dc932dd)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 包括的ガイド]()