djangoをdockerコンテナで利用(14) – djangoでblobカラムを含むレコードのコピー

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

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は今は使ってないから定義そのものをコメント化。

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要素を埋め込むと確認ポップアップが使える。

django-blob-copy

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

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

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

views

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

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

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

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

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

作者さんありがとう。

Djangoのモデルオブジェクトをコピー - Qiita
環境django 3.0.3まるまるコピーインスタンスのidを空にして保存したら、新規で登録できます。product = Product.objects.get(pk=id)product…
https://tsukasa-blog.com/programming/bulk-create-update/
[Django]QuerySetのfilterメソッドの使い方まとめ | CodeLab
Django のクエリセットの filter メソッドを使うとき、毎回 Google で使い方調べてたんですが、毎回調べるのも大変なので filter メソッドの使い方をまとめました。

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

@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

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

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版とはおさらばできるかも。

せっせと作るかな。

タイトルとURLをコピーしました