個人開発でDjangoのO/Rマッパーを使わなかったせいで余計な苦労をしたので懺悔する

# TL;DR
自作の方法で技術的負債を生むより、Djangoの正しい方法に従いましょう。  
具体的には、フレームワークではmigrateしてmodelを使いましょう。  

# 経緯
普段はインフラエンジニアをしていますが、プログラミングも好きで、コロナ禍の時間を活用してPythonを始めました。  
最初はTwitterのボットを作成していましたが、PythonでのWeb開発に興味を持ち、Djangoに触れました。

[チュートリアル](https://tutorial.djangogirls.org/ja/)をやった時に思ったのですが…
`python manage.py migrate`←これが本当に不気味でした。

```bash
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
```

何がOKなんや？ということでDBの中に入ってみると…。

```bash
$ sqlite3 db.sqlite3 
SQLite version 3.39.4 2022-09-07 20:51:41
Enter ".help" for usage hints.
sqlite> .tables
auth_group                  auth_user_user_permissions
auth_group_permissions      django_admin_log          
auth_permission             django_content_type       
auth_user                   django_migrations         
auth_user_groups            django_session            
sqlite> 
```

**なんで最初からよくわからないテーブルがあるの！？**  
と、凄まじい拒否反応が出てしまいました。自分はとてもめんどくさがりです。  
既に表示したい情報はSQLでテーブルを作っており、データも入っていました。  
モデルのドキュメントを見ると[独自の記法](https://docs.djangoproject.com/ja/5.1/ref/models/querysets)があり、コレを覚えるのか…とめんどくさがって使うのを諦めました。ここが**本当にダメ**だった。  
なので**migrateせずに、つまりmodelを作らずに**、初めてのWeb開発に突入しました。  

当時のリポジトリを見るとview_util.pyとかファイル名からしてよくない物があります。理解した今なら恥ずかしいのがわかります。詳しくみてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588836/6f4b6967-08ac-a531-9cfa-5389c0a86dff.png)



# 昔のやり方: 問題点
作ったアプリは画像をランダムに2件表示し、それに対して好きな方に対して投票するようなアプリでした。画像の説明などのメタ情報も一部表示していました。

## 実際のプログラム
### ルーティング
これは普通です。[URLディスパッチャ](https://docs.djangoproject.com/ja/5.1/topics/http/urls/)です。
```python:url.py
urlpatterns = [
    path('', views.index, name='index')
]
```

### やばいview
この辺りからきな臭くなります。`rawDbData`ってヤバそうな変数もいい（悪い）。
```python:view.py
def index(request):
    now = datetime.now()
    rawDbData = getImage()
    imgLs1, metadata1, delFlag1 = getViewContent(rawDbData)
    imgLs2, metadata2, delFlag2 = getViewContent(rawDbData)
    imageId1 = metadata1[0][0]
    imageId2 = metadata2[0][0]
    voteVal = [imageId1, imageId2]
    try:
        if metadata1[0][3] == "":
            metadata1[0][3] = "無し"
        if metadata1[0][4] == "":
            metadata1[0][4] = "無し"
        if metadata2[0][3] == "":
            metadata2[0][3] = "無し"
        if metadata2[0][4] == "":
            metadata2[0][4] = "無し"
    except:
        pass
    context = {
        'img1': imgLs1,
        "metadata1": metadata1,
        'delflag1': delFlag1,
        'img2': imgLs2,
        "metadata2": metadata2,
        'delflag2': delFlag2,
        "imageId1": imageId1,
        "imageId2": imageId2,
        "voteVal": voteVal,
        "now": now
    }
    return render(request, 'img/index.html', context)
```

### 自作O/Rマッパー？
今思うとutil.pyが自作したO/Rマッパーみたいなものでした。  
`select * from infoimage`というviewがやばくて、infoテーブルとimageテーブルを全部joinしたものでした。全件取得です。これを`getViewContent`という関数でfor文を使ってゴリ回していました。  
画像は大体1日100くらい増えます。作って数週間後には3人同時にサイト見るとサーバーダウンしました。インスタンスタイプが`t3.micro`だと貧弱でダメなのかなと思ってたけど、コードがダメでした。

```python:util.py
def getImage():
    query = f'select * from infoimage'
    with get_connection("postgresql") as conn:
        with conn.cursor() as cur:
            cur.execute(query)
            queryLs = [i for i in cur]
    return queryLs

def getViewContent(rawDbData):
    preRndData = list(set([i[0] for i in rawDbData]))
    rndId = random.choice(preRndData)
    imgLs = [i[5] for i in rawDbData if i[0] == rndId]
    choiceLs = [x for x in rawDbData if x[0] == rndId]
    delFlag = metaToDelFlag(choiceLs)
    metadata = [rndDataToFrontMetaData(choiceLs)]
    for i in metadata:
        i[3] = i[3][:30]
        i[4] = i[4][:30]
    return imgLs, metadata, delFlag
```

### マジックナンバーが乱舞する可読性がないテンプレート

```html:index.html
<div class="row">
    <div class="col-12">
        <table>
            {% for a in metadata1 %}
            {% for b in metadata2 %}
            <tr>
                <th>日</th>
                <td>{{ a.1 }}({{ delflag1 }})</td>
                <td>{{ b.1 }}({{ delflag2 }})</td>
            </tr>
            <tr>
                <th>元</th>
                <td><a href="https://hoge.com/{{ a.2 }}/{{ a.0 }} " target="_blank">{{ a.3 }}</a></td>
                <td><a href="https://hoge.com/{{ b.2 }}/{{ b.0 }} " target="_blank">{{ b.3 }}</a></td>
            </tr>
            <tr>
                <th>B</th>
                <td><a href="https://hoge.com/{{ a.2 }}/" target="_blank">{{ a.4 }}</a></td>
                <td><a href="https://hoge.com/{{ b.2 }}/" target="_blank">{{ b.4 }}</a></td>
            </tr>
            <tr>
                <th>票</th>
                <td>{{ a.9 }}</td>
                <td>{{ b.9 }}</td>
            </tr>
            {% endfor %}
            {% endfor %}
        </table>
    </div>
</div>
```
# 現在のやり方: 正しい方法
Modelを使うことを覚えたので、viewは当初の行数と比べて1/20になりました。

```python:view.py
def index(request):
    infos = Info.objects.order_by('?')[:2]
    images = Image.objects.filter(id__in=[info.id for info in infos])
    context = {'infos': infos, 'images': images}
    return render(request, 'random_image/random.html', context)
```

テンプレート上でもモデル.カラム名で値を指定できることも知り、可読性が向上しました。
```html:index.html
<tr>
    <th>日</th>
    <td>{{ info.hoge }}({{ info.tako }})</td>
</tr>
```

## 当時の自分はどうするべきだったのか

- 怠惰のトレードオフ
  - わずかな学習コスト
  - 学習しないで自分のスキルでフレームワークを再現する膨大なコスト

- 既存のDBをモデルにするべきだった
  - `python manage.py inspectdb > model.py`で解決

これはモデルを自動生成するコマンドで、model.pyに当たるものを自動作成してくれます。

https://docs.djangoproject.com/ja/5.1/howto/legacy-databases/

- 参考
```python:migrateで自動作成されたテーブル.py
$ python manage.py inspectdb

from django.db import models


class AuthGroup(models.Model):
    name = models.CharField(unique=True, max_length=150)

    class Meta:
        managed = False
        db_table = 'auth_group'


class AuthGroupPermissions(models.Model):
    id = models.BigAutoField(primary_key=True)
    group = models.ForeignKey(AuthGroup, models.DO_NOTHING)
    permission = models.ForeignKey('AuthPermission', models.DO_NOTHING)

    class Meta:
        managed = False
        db_table = 'auth_group_permissions'
        unique_together = (('group', 'permission'),)

```

## セルフ技術的負債錬成
技術的負債は個人開発でも作れることがわかりました。学習をサボってフレームワークの機能を再発明するのはやめましょう。動くものができても後で見て虚しくなるだけです。  
それと、技術的負債を避けるために、柔軟に学び続けることの重要性を理解しました。  
また、Djangoという素晴らしいフレームワークに関わってくれたありとあらゆる先人に感謝いたします🙏


## 宣伝
ということで、所属会社の方で技術的負債解消の勉強会やります。ぜひどうぞ。

https://livesense.connpass.com/event/333967/