3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-07

TL;DR

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

経緯

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

チュートリアルをやった時に思ったのですが…
python manage.py migrate←これが本当に不気味でした。

$ 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の中に入ってみると…。

$ 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でテーブルを作っており、データも入っていました。
モデルのドキュメントを見ると独自の記法があり、コレを覚えるのか…とめんどくさがって使うのを諦めました。ここが本当にダメだった。
なのでmigrateせずに、つまりmodelを作らずに、初めてのWeb開発に突入しました。

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

image.png

昔のやり方: 問題点

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

実際のプログラム

ルーティング

これは普通です。URLディスパッチャです。

url.py
urlpatterns = [
    path('', views.index, name='index')
]

やばいview

この辺りからきな臭くなります。rawDbDataってヤバそうな変数もいい(悪い)。

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だと貧弱でダメなのかなと思ってたけど、コードがダメでした。

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

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

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になりました。

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)

テンプレート上でもモデル.カラム名で値を指定できることも知り、可読性が向上しました。

index.html
<tr>
    <th></th>
    <td>{{ info.hoge }}({{ info.tako }})</td>
</tr>

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

  • 怠惰のトレードオフ

    • わずかな学習コスト
    • 学習しないで自分のスキルでフレームワークを再現する膨大なコスト
  • 既存のDBをモデルにするべきだった

    • python manage.py inspectdb > model.pyで解決

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

  • 参考
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という素晴らしいフレームワークに関わってくれたありとあらゆる先人に感謝いたします🙏

宣伝

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?