djangoでblob列を含むレコードを作成・更新・参照・削除することができるようになった。

自分特有の blob操作 もできるようになった。

ただ、これだけでは自分にとって使える処理とは言えない。

普段は一度入力した内容をコピーして再作成することのほうが多い。

1から入力するんじゃなく、既存レコードをコピーして更新しながら作る。

そのほうが楽やし。

今回はレコードをコピーする処理を作ったのでそのメモ。

結論

実際の作りはこうなった。

月間作業予定の一覧を年月単位で存在させてて、優先順位を番号として付けてある。

listviewを使って一覧表示させたレコードに「行コピー」ってボタンを作って、そのレコードをコピーし、ボタンを押すとそれ以降のレコードの優先順位を+1する。

例えばこういうレコードがあって3列目にあるコピーボタンを押すと、

django-blob-copy

こうなる。

django-blob-copy

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って名前で始まる列の定義は他のテーブルも完全に同じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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になる。

自動生成させた内容にdefaultdef __str__書き加えた。 なぜか1つ目のidって列は定義にない。

パフォーマンスは知らんけど、get_yyyymmがあるとviewsの中でいちいち日付から年月を取り出す必要がなくなるから、書くのがちょっと楽になる。

workshubetsuは今は使ってないから定義そのものをコメント化。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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段階目はリンク

横に長くて読みづらいけどしゃぁない。

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の記述は改行したらアカン。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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段階目は確認ポップアップつきのボタン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<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要素を埋め込むと確認ポップアップが使える。

django-blob-copy

これでいきなりはコピー処理が動かへんから、思った動きになった。

ただし、URLにパラメータを埋め込むことができないので、inputをhidden属性で定義して受け渡すってのを忘れてた。

最初パラメータが渡らずに、なんでやねんってなった。

views

djangoでcrudするときには、こんなインポートをして使うと一覧表示とか更新削除がスイスイ動く。

1
2
3
4
5
6
7
from django.views.generic import (
                            ListView,       ## リスト表示のため
                            DetailView,     ## 単一レコードの詳細情報表示のため
                            CreateView,     ## レコードの作成のため
                            DeleteView,     ## レコードの削除のため
                            UpdateView,     ## レコードの更新のため
)

今回はレコードをコピーしたいから、UpdateViewの処理をベースにしていけばいいかなって最初予想してた。

でもそれじゃうまくコピーできなかったので、コピー処理ではクラスビューを使うのはあきらめて関数で書くように方針変更した。

一括更新ってのがあるらしいけど、自分がやりたいのは1レコードずつ更新。 フィルタの使い方とか、いくつかサイトを参考にさせてもらった。

作者さんありがとう。

テスト終わった頃のソースはこんな感じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@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に出力してる。

「コピー登録」で探せばこんな感じで検索できてる。

django-blob-copy

クラスベースビューだとログ出力は、

1
gv_logwrite('info',self.request, ・・・

関数ベースだとselfを外して書いておく。

1
gv_logwrite('info',request, ・・・

最後にredirectで検索ページに戻るようにすればコピーした結果が見える。 redirectとrenderとreverseとreverse_lazyの違いがイマイチ理解できてへんな。

最初reverseとかreverse_lazyとか書いてたから、めっちゃエラー出た。

urls.py

こう書き足しとく。

1
    path('GvisWorkCopy/', gvis_work_views.GvisWorkCopy, name="GvisWorkCopy"),

ここまで来たら、コピーが機能するかをテストして今回の処理は動いてくれた。

あと10個ほど同様の機能をdjangoで作れば、夏までにphp版とはおさらばできるかも。

せっせと作るかな。