Djangoでフォロー機能を実装する

# 前段
SNS系アプリにほぼ必須のフォロー機能をDjangoで実装しました。
ライブラリ不使用の自前実装になります。
各自用途や要件に合わせてカスタマイズしてください。

## 開発環境
- Django 
- Python 3.8.5
- MySQL
- Visual Studio Code

※SQLite3でもほぼコード変更なしで書けます。

#フォロー機能の具体的な要件
- フォローすることができる/解除もできる
- 自分自身をフォローすることはできない
- フォロー数、フォロワー数を確認できる

フォロワーの一覧表示などはここでは省きます。
必要最小限の機能要件を実装します。

#テーブル設計
テーブルを1つだけ作成して済ましてしまう方法もありますが、個人的にSQL文が冗長になったり、テーブル管理がしにくい印象を持ったので今回は２つ用意します。

**follow_relationテーブル**

| データ | 格納する情報 | データ型 |
|:-:|:-:|:-:|
|user_id   |フォローする側のユーザーID   | UUID  |
|followed_id   |フォローされる側のユーザーID   |UUID   |

**follow_countテーブル**

| データ| 格納する情報| データ型 |
|:-:|:-:|:-:|
| user_id  |ユーザーID   | UUID  |
| follow  | フォローしている数  | INT  |
| followed  |フォローされている数   |INT   |


`follow_relation`テーブルは、ユーザー間のフォロー状態の管理、`follow_count`テーブルはユーザーのフォロー数、フォロワー数の管理をしています。

#views.pyの編集
テーブルができたらさっそくフォロー機能を実装していきましょう。
フォロー機能のおおまかな流れは以下のようになります。

```text
ページのリクエスト
↓
followd_status関数にてフォロー状態を確認＆表示
↓
フォローボタンを押す
↓
follows関数がフォロー状態に応じて処理
↓
元のページにリダイレクト
```

では以下コードになります。

```python:followed_status関数
@login_required
def followed_status(request, accesskey):
    # 省略
    # 各自必要なパラメータを取得してください

    # データベースへの接続
    connection = MySQLdb.connect(
    host='localhost',
    user='***',
    passwd='***',
    db='***',
    charset="utf8"
    )  

    # カーソルの取得
    cursor = connection.cursor() 

    # クエリのセット
    follow_relation = "SELECT COUNT(*) FROM follow_relation where user_id = UUID_TO_BIN(%s) and followed_id = UUID_TO_BIN(%s);"

    # クエリの実行
    # user_id、followed_idは各自で取得
    cursor.execute(follow_relation, (user_id, followed_id))
    row_follow = cursor.fetchone()
    follows = row_follow[0]

    # ログインユーザーとページユーザーが同一人物か判定
    # 同一人物ならフォローできないようにします
    if followed_id == user_id:
        followed_status_code = 1
    else:
        followed_status_code = 2

    # もしfollowsが0より大きかったらTrue
    # followsが0だったらFalseを返す
    followed = False
    if follows > 0 :
        followed = True
 
    return followed_status_code, followed
```

必要なパラメータは各自別テーブルから引き出してください。
今回必要なのはフォローする側とされる側のIDのみです。
`user_id`がフォローする側、`followed_id`がフォローされる側です。

- ユーザー同士のIDを照合します

ステータスコードを付与して同一人物か判別します。

`followed_status_code = 1`なら自分自身（フォローできない）
`followed_status_code = 2`なら他人同士（フォローできる）

になります。

- フォロー状態を確認します

TrueとFalseでフォロー状態を確認します。

`False`ならまだフォローしていない
`True`なら既にフォローしている

になります。
では、次にfollows関数を見ていきます。

```python:follows関数
@login_required
def followes(request, accesskey):
    # 省略
    # 各自必要なパラメータを取得してください

    # データベースへの接続
    connection = MySQLdb.connect(
    host='localhost',
    user='***',
    passwd='***',
    db='***',
    charset="utf8"
    )  

    # カーソルの取得
    cursor = connection.cursor()

    # 共通クエリをセットしてください
    # ここで必要な情報を取得しておきます

    # 自分自身はフォローできないようにする
    if user_id == followed_id:
        followed = False
        # セッションにフォロー状態を保存します
        request.session['page_followed_status'] = followed
    else:
        if followed == False:
            # フォローするときの処理になります
            # クエリのセット
            # 新規フォロー関係の挿入
            follow_reation = "INSERT INTO follow_relation (user_id, followed_id) values(UUID_TO_BIN(%s), UUID_TO_BIN(%s));"

            # フォローする側ユーザーのフォロー数を１つ増やす
            follow_update = "UPDATE follow_count SET follow = follow + 1 where user_id=UUID_TO_BIN(%s);"  

            # フォローされる側ユーザーのフォロワー数を１つ増やす
            followed_update = "UPDATE follow_count SET followed = followed + 1 where user_id=UUID_TO_BIN(%s);"

            # クエリの実行
            cursor.execute(follow_relation, (user_id, followed_id))

            cursor.execute(follow_update, (user_id, ))

            cursor.execute(followed_update, (followed_id, ))

            # 接続を終了する
            cursor.close()
            connection.commit()
            connection.close() 

            # フォローした状態にする
            followed = True
            request.session['page_followed_status'] = followed

        else:
            # フォローを外すときの処理になります
            # クエリのセット
            # フォロー関係の削除
            follow_relation = "DELETE FROM follow_relation where user_id=UUID_TO_BIN(%s) and followed_id = UUID_TO_BIN(%s);"

            # フォローを外す側ユーザーのフォロー数を１つ減らす
            follow_update = "UPDATE follow_count SET follow = follow - 1 where user_id=UUID_TO_BIN(%s);"

            # フォローを外される側ページユーザーのフォロワー数を１つ減らす
            followed_update = "UPDATE follow_count SET followed = followed - 1 where user_id=UUID_TO_BIN(%s);"

            # クエリの実行
            cursor.execute(sql_followrelation_delete, (user_id, followed_id))

            cursor.execute(sql_follow_update, (user_id, ))

            cursor.execute(sql_followed_update, (followed_id, ))

            # 接続を終了する
            cursor.close()
            connection.commit()
            connection.close() 

            # フォローしていない状態にする
            followed = False
            request.session['page_followed_status'] = followed

    return redirect('元のページ', accesskey)
```

こちらはif文で分岐させてフォローするorフォロー解除の実際の処理をおこなっています。そして最後にフォローの状態を切り替えています。元のページにリダイレクトして終了です。（今回はAjaxなどを使用しないためページ遷移が生じています）

最後に表示用のviewを編集します。

```python:result関数
@login_required
def result(request, accesskey):

    # 省略

    # フォローの状態を把握する
    status = followed_status(request, accesskey)
    followed_status_code = status[0]
    followed = status[1]
    # フォローの状態をセッションに保存（一時的）
    request.session['page_followed_status'] = followed

    params = {
        # 省略

        'page_followed_status': followed,
        'page_followed_status_code': followed_status_code,
    }
    return render(request, 'result.html', params)
```

該当箇所のみ記載しています。
`followed_status`関数を呼び出してフォロー状態を確認しています。
(`followed_status`関数では、ステータスコードとフォロー状態が返されます。)
そしてセッションに一時的に保存してます。


#urls.pyの編集

```python:urls.py
urlpatterns = [
    # 省略
    path('元のページ', views.pages, name='各自で設定'),
    path('元のページ/follow/', views.follows, name="follows"),
    # 省略
]
```


# テンプレートの編集

最後にテンプレートです。

```html:result.html
            <!-- フォローボタンを追記 -->
            {% if page_followed_status_code is 2 %}
                {% if page_followed_status is False %}
                <p><a href="{% url 'follows' page_accesskey %}">フォローする</a></p>
                {% else %}
                <p><a href="{% url 'follows' page_accesskey %}">フォローをやめる</a></p>
                {% endif %}
            {% endif %}
```

`ステータスコードが２`の場合（自分自身でなく他人同士の場合）のみ表示されるようになっています。

そしてフォロー状態が`False`のとき「フォローする」、`True`のとき「フォローをやめる」と表示させるためif分岐させています。

リンクが押されるとページ移動先でfollows関数が呼び出されフォロー処理をして、また元のページにリダイレクトされるような流れになってます。


#最後に
自分なりにコードを改変して使ってみてください。
「いいね機能」もほぼ同様に実装することができます!