djangoでblob列を含むレコードを作成・更新・参照・削除することができるようになった。
自分特有のblob操作もできるようになった。
ただ、これだけでは自分にとって使える処理とは言えない。
普段は一度入力した内容をコピーして再作成することのほうが多い。
1から入力するんじゃなく、既存レコードをコピーして更新しながら作る。
そのほうが楽やし。
今回はレコードをコピーする処理を作ったのでそのメモ。
結論
実際の作りはこうなった。
月間作業予定の一覧を年月単位で存在させてて、優先順位を番号として付けてある。
listviewを使って一覧表示させたレコードに「行コピー」ってボタンを作って、そのレコードをコピーし、ボタンを押すとそれ以降のレコードの優先順位を+1する。
例えばこういうレコードがあって3列目にあるコピーボタンを押すと、
こうなる。
4列目に優先順位「27」ってあるレコードがコピーされて「28」ができてる。
ここで「27」からさらにコピーすると、「27」と「28」は「28」と「29」になって、元の27を新規レコードとして保管する。
右端の3列にガッキーのjpeg、五線譜のpdfの1ページ目をjpeg変換したもの、ガッキーの別のjpegがちゃんと入ってコピーされている。
しっかしガッキーかわいい。
格納したファイルと性能
jpegは100KB程度やけど、pdfはムーンライトソナタの譜面で16ページ、7.5MB程度ある。
それをbase64でエンコードして保管するから、容量は1.5倍程度として1レコードはたぶん10MB強。
それでもdjango的には1秒もかからず処理してくれている。
20件ある先頭レコードでコピーしても同じやった。
数百件とか数千件だと時間かかるのかもしれないけど、ここではこれで十分。
dockerコンテナでもこれぐらい動いてくれるんやなぁ。
レコードの構成
jpegとpdfは実表示用データ(longblob)だけでなく、一覧表示するときのための縮小版(mediumblob)を持ってる。1レコードに3つの添付ができるように、同じようなフィールドが3つずつある。
mariaDB上の定義
前回のblob操作は在庫のテーブル使ってたけど、今回は活動テーブル。BLOB
って名前で始まる列の定義は他のテーブルも完全に同じ。
CREATE TABLE `GVIS_work` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`workPeriod` datetime DEFAULT NULL COMMENT '業務期間-開始日',
`workShubetsu` char(4) DEFAULT NULL COMMENT '作業種別',
`workPriority` int(10) DEFAULT NULL COMMENT '優先順位',
`Category1` varchar(100) DEFAULT NULL COMMENT 'カテゴリ1',
`Category2` varchar(100) DEFAULT NULL COMMENT 'カテゴリ2',
`Tehai` varchar(10000) DEFAULT NULL COMMENT '手配方法・名称・機種',
`Kazu` decimal(12,4) DEFAULT 0.0000 COMMENT '数量',
`Tnk` decimal(12,4) DEFAULT 0.0000 COMMENT '単価',
`Kng` decimal(12,4) DEFAULT 0.0000 COMMENT '金額',
`WorkTime` decimal(12,4) DEFAULT NULL COMMENT '作業時間',
`Biko` varchar(100) DEFAULT NULL COMMENT '備考',
`BLOB_data1` longblob DEFAULT NULL COMMENT 'BLOBデータ1',
`BLOB_data2` longblob DEFAULT NULL COMMENT 'BLOBデータ2',
`BLOB_data3` longblob DEFAULT NULL COMMENT 'BLOBデータ3',
`BLOB_extent1` varchar(100) DEFAULT NULL COMMENT '添付mime1',
`BLOB_extent2` varchar(100) DEFAULT NULL COMMENT '添付mime2',
`BLOB_extent3` varchar(100) DEFAULT NULL COMMENT '添付mime3',
`BLOB_medium1` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用1',
`BLOB_medium2` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用2',
`BLOB_medium3` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用3',
`ins_date` datetime DEFAULT NULL COMMENT 'データ作成日',
`ins_user` varchar(100) DEFAULT NULL COMMENT 'データ作成ユーザ',
`upd_date` datetime DEFAULT NULL COMMENT 'データ更新日',
`upd_user` varchar(100) DEFAULT NULL COMMENT 'データ更新ユーザ',
PRIMARY KEY (`id`),
KEY `IDX_work` (`workPeriod`,`workShubetsu`,`workPriority`,`Category1`,`Category2`,`Tehai`(255))
) ENGINE=InnoDB AUTO_INCREMENT=1502 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
django内のmodels.pyの定義
blob列はdjangoではTextField
になる。
自動生成させた内容にdefault
やdef __str__
書き加えた。
なぜか1つ目のid
って列は定義にない。
パフォーマンスは知らんけど、get_yyyymm
があるとviewsの中でいちいち日付から年月を取り出す必要がなくなるから、書くのがちょっと楽になる。
workshubetsu
は今は使ってないから定義そのものをコメント化。
from django.db import models
from django.utils import timezone
from pymysql import NULL
from datetime import datetime,timedelta ## テーブルの日付フィールドを扱うため
class GvisWork(models.Model):
## 参考URL https://note.nkmk.me/python-datetime-now-today/
dt_year = datetime.now().year
dt_mon = datetime.now().month
dt_day = datetime.now().day
dt_defdate = str(dt_year) + '-' + str(dt_mon) + '-' + str(dt_day) + ' ' + '00:00:00'
workperiod = models.DateTimeField(verbose_name='業務年月',db_column='workPeriod', blank=True, null=True,
default=datetime.strptime(dt_defdate,'%Y-%m-%d %H:%M:%S')) # Field name made lowercase.
def __str__(self):
return self.workperiod.strftime('%c')
## workshubetsu = models.CharField(db_column='workShubetsu', max_length=4, blank=True, null=True) # Field name made lowercase.
workpriority = models.IntegerField(verbose_name='優先順位',db_column='workPriority', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.workpriority
category1 = models.CharField(verbose_name='カテゴリ1',db_column='Category1', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.category1
category2 = models.CharField(verbose_name='カテゴリ2',db_column='Category2', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.category2
tehai = models.TextField(verbose_name='手配方法・名称・機種・内容',db_column='Tehai', max_length=10000, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.tehai
kazu = models.DecimalField(verbose_name='数量',db_column='Kazu', max_digits=12, decimal_places=4, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.kazu
tnk = models.DecimalField(verbose_name='単価',db_column='Tnk', max_digits=12, decimal_places=4, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.tnk
kng = models.DecimalField(verbose_name='金額',db_column='Kng', max_digits=12, decimal_places=4, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.kng
worktime = models.DecimalField(verbose_name='作業時間',db_column='WorkTime', max_digits=12, decimal_places=4, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.worktime
biko = models.CharField(verbose_name='備考',db_column='Biko', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.biko
blob_data1 = models.TextField(db_column='BLOB_data1', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_data1
blob_data2 = models.TextField(db_column='BLOB_data2', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_data2
blob_data3 = models.TextField(db_column='BLOB_data3', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_data3
blob_extent1 = models.CharField(db_column='BLOB_extent1', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_extent1
blob_extent2 = models.CharField(db_column='BLOB_extent2', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_extent2
blob_extent3 = models.CharField(db_column='BLOB_extent3', max_length=100, blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_extent3
blob_medium1 = models.TextField(db_column='BLOB_medium1', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_medium1
blob_medium2 = models.TextField(db_column='BLOB_medium2', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_medium2
blob_medium3 = models.TextField(db_column='BLOB_medium3', blank=True, null=True) # Field name made lowercase.
def __str__(self):
return self.blob_medium3
ins_date = models.DateTimeField(blank=True, null=True,
default=datetime.strptime(dt_defdate,'%Y-%m-%d %H:%M:%S')
)
def __str__(self):
return self.ins_date.strftime('%c')
ins_user = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return self.ins_user
upd_date = models.DateTimeField(blank=True, null=True,
default=timezone.datetime.min
)
def __str__(self):
return self.upd_date.strftime('%c')
upd_user = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return self.upd_user
def get_yyyymm(self):
return self.workperiod.strftime('%Y/%m')
class Meta:
managed = False
db_table = 'GVIS_work'
実際に編集した箇所
段階的にtemplatesを作って見せ方を考えながら、裏方のviewsを書く。
必要なら共通関数とか追記しながら書く。
普段はそんなふうに作ってる。
最後にurls.pyに表示URLを定義してテストしてる。
templates
1段階目はリンク
横に長くて読みづらいけどしゃぁない。
<a href="{% url 'gvisWebApp:GvisWorkCopy' %}?yyyymm={{GvisWork.get_yyyymm|slice:"0:4"}}{{GvisWork.get_yyyymm|slice:"5:7"}}&pr={{ GvisWork.workpriority }} ">行コピー</a>
呼び出し先のURLの末尾に?
って区切って年月と優先順位をパラメータで渡してる。
sliceって書く箇所、djangoの文字列は0番目から数え、抜き出したい文字位置の直後を指定する。
年だと2022って抜きたいから、以下の桁では0から4。
月だと03って抜きたいから、5から7。
|0|1|2|3|4|5|6|
|-|-|-|-|-|-|-|
|2|0|2|2|/|0|3|
指折り1から数えられるほうが楽やけど、0番目から始まるっちゅーことで。
2段階目はボタン
理由はないけどphp版はボタンにしてるから見た目そろえたかった。
djangoはテンプレートの中で改行しちゃいけない箇所あるけど、
ボタンのための表記は改行しても大丈夫。
onclickの記述は改行したらアカン。
<input
class="darkmode-ignore" type="button" name="COPY_btn"
onclick="openURL('{% url 'gvisWebApp:GvisWorkCopy' %}?yyyymm={{GvisWork.get_yyyymm|slice:"0:4"}}{{GvisWork.get_yyyymm|slice:"5:7"}}&pr={{ GvisWork.workpriority }} ')"
value="行コピー">
:(中略)
<script type="text/javascript">
:(中略)
function openURL(p) {
window.open(p);
}
:(中略)
</script>
ボタンにはなったけど、いきなりコピー処理が動くのはちょっとなぁ・・・。
3段階目は確認ポップアップつきのボタン
<form id="CPform" name="CPform" method="GET" style="text-align:left"
action="{% url 'gvisWebApp:GvisWorkCopy' %}" onSubmit="return copyPopup(); " >
<input class="darkmode-ignore" type="submit" name="COPY_btn" value="行コピー">
<input type="hidden" name="yyyymm" value="{{GvisWork.get_yyyymm|slice:"0:4"}}{{GvisWork.get_yyyymm|slice:"5:7"}}">
<input type="hidden" name="pr" value="{{ GvisWork.workpriority }}">
</form>
:(中略)
<script type="text/javascript">
:(中略)
{# formのonclickに渡すとキャンセルボタンを押してもtrueが戻ってしまうので注意 #}
function copyPopup() {
if(window.confirm('この内容でコピーしますか?')) {
return true;
} else {
return false;
}
}
:(中略)
</script>
formを使ってinput要素を埋め込むと確認ポップアップが使える。
これでいきなりはコピー処理が動かへんから、思った動きになった。
ただし、URLにパラメータを埋め込むことができないので、inputをhidden属性で定義して受け渡すってのを忘れてた。
最初パラメータが渡らずに、なんでやねんってなった。
views
djangoでcrudするときには、こんなインポートをして使うと一覧表示とか更新削除がスイスイ動く。
from django.views.generic import (
ListView, ## リスト表示のため
DetailView, ## 単一レコードの詳細情報表示のため
CreateView, ## レコードの作成のため
DeleteView, ## レコードの削除のため
UpdateView, ## レコードの更新のため
)
今回はレコードをコピーしたいから、UpdateViewの処理をベースにしていけばいいかなって最初予想してた。
でもそれじゃうまくコピーできなかったので、コピー処理ではクラスビューを使うのはあきらめて関数で書くように方針変更した。
一括更新ってのがあるらしいけど、自分がやりたいのは1レコードずつ更新。
フィルタの使い方とか、いくつかサイトを参考にさせてもらった。
作者さんありがとう。
テスト終わった頃のソースはこんな感じ。
@login_required
def GvisWorkCopy(request):
getparam = request.GET.get('yyyymm')
workperiodYYYY = getparam[0:4]
workperiodMM = getparam[4:6]
currentRec = request.GET.get('pr')
# 当月レコード取得
object_list = GvisWork.objects.filter(
workperiod__range = [ gvis_startdayOfMonth(workperiodYYYY,workperiodMM),
gvis_finaldayOfMonth(workperiodYYYY,workperiodMM)
]
).order_by('workpriority').reverse()
# カレントレコードを先に取得しておく -> allじゃなくてgetにするとsaveできる(allでもループさせればsaveできる)
currentRecord = object_list.get(
workpriority = int(currentRec)
)
# カレントレコード以上のレコードの取得
object_list = object_list.filter(
workpriority__gte=currentRec
).order_by('workpriority').reverse()
# カレントレコード以上のレコードの優先順位をインクリメント
for Work in object_list:
Work.workpriority += 1
## base64でエンコードしてある文字列になぜか「b'」がついてしまうので外す
Work.blob_data1 = None if Work.blob_data1 is None else Work.blob_data1.decode("UTF-8")
Work.blob_medium1 = None if Work.blob_medium1 is None else Work.blob_medium1.decode("UTF-8")
Work.blob_data2 = None if Work.blob_data2 is None else Work.blob_data2.decode("UTF-8")
Work.blob_medium2 = None if Work.blob_medium2 is None else Work.blob_medium2.decode("UTF-8")
Work.blob_data3 = None if Work.blob_data3 is None else Work.blob_data3.decode("UTF-8")
Work.blob_medium3 = None if Work.blob_medium3 is None else Work.blob_medium3.decode("UTF-8")
Work.save() # 更新のクエリ発行
# カレントレコードを丸々コピー保存 -> idをNoneにすると新規保存してくれるらしい
currentRecord.id = None
## base64でエンコードしてある文字列になぜか「b'」がついてしまうので外す
currentRecord.blob_data1 = None if currentRecord.blob_data1 is None else currentRecord.blob_data1.decode("UTF-8")
currentRecord.blob_medium1 = None if currentRecord.blob_medium1 is None else currentRecord.blob_medium1.decode("UTF-8")
currentRecord.blob_data2 = None if currentRecord.blob_data2 is None else currentRecord.blob_data2.decode("UTF-8")
currentRecord.blob_medium2 = None if currentRecord.blob_medium2 is None else currentRecord.blob_medium2.decode("UTF-8")
currentRecord.blob_data3 = None if currentRecord.blob_data3 is None else currentRecord.blob_data3.decode("UTF-8")
currentRecord.blob_medium3 = None if currentRecord.blob_medium3 is None else currentRecord.blob_medium3.decode("UTF-8")
currentRecord.save()
## ログ出力
key = workperiodYYYY + workperiodMM + '-' + '%06d' % int(currentRec)
gv_logwrite('info',request,'dj-活動コピー登録','(' + key + ')')
messages.success(request,'登録内容をコピーしました。(' + workperiodYYYY + workperiodMM + '-' + currentRec + ')')
return redirect('gvisWebApp:gvis_400_KatsudoKensaku')
おおまかな流れ
コピー対象レコードをとりこんでおき、同じレコード以降の優先順位に1を足して保存していく。
あとは最初に取り込んだレコードを新規レコードとして保管する。
blob列改変されてまう
レコードが変数に入って処理されるとき、blob列の先頭にb'
ってついてくる。
これを外すためにdecode("UTF-8")
ってやってる。
同じようなことやってる箇所があるから、本当は共通関数みたいなのにしたいけど、djangoは「C言語でいう参照渡し」ができへん。
やり方あるのかもしれんけど、見つけられへんかった。
引数の内容を更新して返してくれる処理って作れへんのかなぁ。
新規レコード保存
新規レコードとして保存する場合は、テーブルの先頭列につけているid
って列をNoneにしておくだけ。
このNoneを最初「Null」って勘違いして書いてしもて、vscodeが勝手にimport行を足したから、わけわからんエラーが出た。
NoneとNullとNillがごっちゃになってるのな。
gitの履歴と差分見たらvscodeが自動追加してるのがわかった。
vscodeいらんことすんなよなぁ。
ログの見え方
ログ出力はmariadbに出力してる。
「コピー登録」で探せばこんな感じで検索できてる。
クラスベースビューだとログ出力は、
gv_logwrite('info',self.request, ・・・
関数ベースだとselfを外して書いておく。
gv_logwrite('info',request, ・・・
最後にredirectで検索ページに戻るようにすればコピーした結果が見える。
redirectとrenderとreverseとreverse_lazyの違いがイマイチ理解できてへんな。
最初reverseとかreverse_lazyとか書いてたから、めっちゃエラー出た。
urls.py
こう書き足しとく。
path('GvisWorkCopy/', gvis_work_views.GvisWorkCopy, name="GvisWorkCopy"),
ここまで来たら、コピーが機能するかをテストして今回の処理は動いてくれた。
あと10個ほど同様の機能をdjangoで作れば、夏までにphp版とはおさらばできるかも。
せっせと作るかな。