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

## はじめに

Part2では、DailyBeatのデータベース設計とユーザー認証の実装について解説しました。Part3では、DailyBeatの核心機能である楽曲投稿機能とタイムライン機能の実装について詳しく見ていきます。

## 1. 楽曲投稿機能の実装

楽曲投稿機能は、ユーザーが毎日1曲を共有するためのDailyBeatの中心的な機能です。

### 1.1 投稿フォームの作成

まず、楽曲投稿用のフォームを作成します。

```python:forms/post.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, URLField
from wtforms.validators import DataRequired, URL

class DailySongForm(FlaskForm):
    title = StringField('曲名', validators=[DataRequired()])
    artist = StringField('アーティスト名', validators=[DataRequired()])
    genre = StringField('ジャンル', validators=[DataRequired()])
    spotify_url = URLField('Spotify URL', validators=[URL()])
    submit = SubmitField('投稿')
```

**コード解説：**
- `FlaskForm`: Flask-WTFのフォームクラスを継承しています。
- `DataRequired()`: 必須フィールドを指定します。
- `URL()`: URLの形式をバリデーションします。

### 1.2 投稿ビューの実装

次に、楽曲投稿を処理するビュー関数を実装します。

```python:views/post.py
from flask import render_template, flash, redirect, url_for
from flask_login import login_required, current_user
from app import app, db
from app.models import DailySong
from app.forms import DailySongForm

@app.route('/post', methods=['GET', 'POST'])
@login_required
def post_song():
    form = DailySongForm()
    if form.validate_on_submit():
        song = DailySong(
            title=form.title.data,
            artist=form.artist.data,
            genre=form.genre.data,
            spotify_url=form.spotify_url.data,
            author=current_user
        )
        db.session.add(song)
        db.session.commit()
        flash('楽曲を投稿しました！')
        return redirect(url_for('index'))
    return render_template('post_song.html', title='楽曲投稿', form=form)
```

**コード解説：**
- `@login_required`: ログインユーザーのみがアクセスできるようにします。
- `form.validate_on_submit()`: フォームのバリデーションを行います。
- `DailySong`: 新しい楽曲オブジェクトを作成します。
- `db.session.add()`と`db.session.commit()`: データベースに新しい楽曲を追加します。

### 1.3 投稿テンプレートの作成

楽曲投稿フォームのテンプレートを作成します。

```html:templates/post_song.html
{% extends "base.html" %}

{% block content %}
    <h1>今日の1曲を投稿</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.title.label }}<br>
            {{ form.title(size=32) }}
            {% for error in form.title.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.artist.label }}<br>
            {{ form.artist(size=32) }}
            {% for error in form.artist.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.genre.label }}<br>
            {{ form.genre(size=32) }}
            {% for error in form.genre.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.spotify_url.label }}<br>
            {{ form.spotify_url(size=64) }}
            {% for error in form.spotify_url.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}
```

**テンプレート解説：**
- `form.hidden_tag()`: CSRFトークンを含む隠しフィールドを生成します。
- 各フォームフィールドにエラーメッセージ表示機能を追加しています。

## 2. タイムライン機能の実装

タイムライン機能は、ユーザーが投稿した楽曲を時系列順に表示する機能です。

### 2.1 タイムラインビューの実装

```python:views/timeline.py
from flask import render_template
from flask_login import login_required
from app import app
from app.models import DailySong

@app.route('/timeline')
@login_required
def timeline():
    songs = DailySong.query.order_by(DailySong.timestamp.desc()).limit(50).all()
    return render_template('timeline.html', title='タイムライン', songs=songs)
```

**コード解説：**
- `DailySong.query.order_by()`: 投稿を時間順に並べ替えます。
- `limit(50)`: パフォーマンスを考慮して、取得する投稿数を制限しています。

### 2.2 タイムラインテンプレートの作成

タイムライン表示用のテンプレートを作成します。

```html:templates/timeline.html
{% extends "base.html" %}

{% block content %}
    <h1>タイムライン</h1>
    {% for song in songs %}
    <div class="song-post">
        <p>{{ song.author.username }}さんの投稿</p>
        <h3>{{ song.title }} - {{ song.artist }}</h3>
        <p>ジャンル: {{ song.genre }}</p>
        <p>投稿日時: {{ song.timestamp.strftime('%Y-%m-%d %H:%M') }}</p>
        {% if song.spotify_url %}
        <p><a href="{{ song.spotify_url }}" target="_blank">Spotifyで聴く</a></p>
        {% endif %}
    </div>
    {% endfor %}
{% endblock %}
```

**テンプレート解説：**
- `for`ループを使って、各投稿を表示しています。
- `strftime()`: タイムスタンプを読みやすい形式に整形しています。

## 3. ページネーションの実装

タイムラインの投稿数が増えた場合、ページネーションが必要になります。Flask-SQLAlchemyのpaginationメソッドを使用して実装します。

```python:views/timeline.py
from flask import request

@app.route('/timeline')
@login_required
def timeline():
    page = request.args.get('page', 1, type=int)
    songs = DailySong.query.order_by(DailySong.timestamp.desc()).paginate(
        page=page, per_page=10, error_out=False)
    next_url = url_for('timeline', page=songs.next_num) if songs.has_next else None
    prev_url = url_for('timeline', page=songs.prev_num) if songs.has_prev else None
    return render_template('timeline.html', title='タイムライン', 
                           songs=songs.items, next_url=next_url, prev_url=prev_url)
```

**コード解説：**
- `request.args.get()`: URLパラメータからページ番号を取得します。
- `paginate()`: ページネーションを適用します。
- `next_url`と`prev_url`: 次のページと前のページのURLを生成します。

タイムラインテンプレートにもページネーションのリンクを追加します：

```html:templates/timeline.html
{% extends "base.html" %}

{% block content %}
    <!-- 既存のコード -->
    
    {% if prev_url %}
    <a href="{{ prev_url }}">前のページ</a>
    {% endif %}
    {% if next_url %}
    <a href="{{ next_url }}">次のページ</a>
    {% endif %}
{% endblock %}
```

## 4. パフォーマンス最適化

タイムライン機能では、多くのデータベースクエリが発生する可能性があります。パフォーマンスを最適化するために、以下の方法を考慮しています：

1. **インデックスの追加**: `timestamp`カラムにインデックスを追加し、ソートのパフォーマンスを向上させます。

   ```python:models/daily_song.py
   class DailySong(db.Model):
       # 既存のコード
       timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
   ```

2. **キャッシング**: 頻繁にアクセスされるタイムラインデータをキャッシュします。例えば、Flask-Cacheを使用して実装できます。

   ```python:views/timeline.py
   from app import cache

   @app.route('/timeline')
   @login_required
   @cache.cached(timeout=300)  # 5分間キャッシュ
   def timeline():
       # 既存のコード
   ```

3. **非同期処理**: タイムライン生成を非同期で行い、レスポンス時間を短縮します。これにはCeleryなどのタスクキューを使用できます。

## まとめ

Part3では、DailyBeatの核心機能である楽曲投稿機能とタイムライン機能の実装について詳しく解説しました。これらの機能を実装することで、ユーザーは日々の楽曲を共有し、他のユーザーの投稿を閲覧することができます。

また、ページネーションの実装やパフォーマンス最適化についても触れ、実際のプロダクション環境での運用を見据えた設計について説明しました。

次回のPart4では、フォロー機能と楽曲再生機能の実装について詳しく解説する予定です。お楽しみに！

## 参考リンク

- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part1](https://qiita.com/taipilo/items/62490dfa93e41d4be0b4)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part2](https://qiita.com/taipilo/items/b74b2ed8ba690956bcac)
- [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」- 包括的ガイド]()