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とかファイル名からしてよくない物があります。理解した今なら恥ずかしいのがわかります。詳しくみてみましょう。
昔のやり方: 問題点
作ったアプリは画像をランダムに2件表示し、それに対して好きな方に対して投票するようなアプリでした。画像の説明などのメタ情報も一部表示していました。
実際のプログラム
ルーティング
これは普通です。URLディスパッチャです。
urlpatterns = [
path('', views.index, name='index')
]
やばいview
この辺りからきな臭くなります。rawDbData
ってヤバそうな変数もいい(悪い)。
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
だと貧弱でダメなのかなと思ってたけど、コードがダメでした。
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
マジックナンバーが乱舞する可読性がないテンプレート
<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になりました。
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)
テンプレート上でもモデル.カラム名で値を指定できることも知り、可読性が向上しました。
<tr>
<th>日</th>
<td>{{ info.hoge }}({{ info.tako }})</td>
</tr>
当時の自分はどうするべきだったのか
-
怠惰のトレードオフ
- わずかな学習コスト
- 学習しないで自分のスキルでフレームワークを再現する膨大なコスト
-
既存のDBをモデルにするべきだった
-
python manage.py inspectdb > model.py
で解決
-
これはモデルを自動生成するコマンドで、model.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という素晴らしいフレームワークに関わってくれたありとあらゆる先人に感謝いたします🙏
宣伝
ということで、所属会社の方で技術的負債解消の勉強会やります。ぜひどうぞ。