SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

DB分析ツールが無かったので(手早く)実装した

公開する予定は(今のところ)ありません。*1

ちなみに、フリーでいいソフトあるよ!とかあれば教えてください。


私は退職を控えているわけですが、なぜか新しい案件に回され*2、そこで受領したExcelのデータ定義があまりにも意味不明でした。一応データベースっぽく書いてあります。

しかし、どれほどダメな資料であっても、一応案件に入った以上内容を分析する必要があります。

とはいえ、Excelをただ読んで分析しても、結局新しい資料が来たら無に帰す可能性が高い、という状況でした。

そこで、

もういいや、ツール作っちゃえ…

と思い立ち、ツールを作った話をします。

前提

仕事を思い出しながら書くので、主要な機能と実装方法を簡単にお知らせします。

入力となるファイルに関して、ある程度の構造が定義されています。

それらのデータを何らかの形で計算・集約し、出力するのがゴールです。

ただ、入力ファイルが多く、列数がトータル1,500を超えるので、

  • Excelのみで管理するのは難しい
  • そもそも、仕様レベルで記述が曖昧なため、どんどん検索したい
  • Excelの検索性は最悪

という感じでした。特に検索性がひどく、定義が複数Excelファイルにまたがっている状況で、そのファイルをスイッチさせる時間が非常に苦痛でした。

環境

さっと作るアプリなら、Visual StudioC#Windows Formあたりがとても楽なのですが、仕事で使えるVisual Studioのライセンスがありませんし、私はWinFormが好きではない*3です。

そこで、

せっかくならDjangoCRUDだけのアプリを作ったら、フレームワークをフル活用できて楽じゃね?

と思い立ちましたので、ローカルだけで動かすWebアプリを作ることにしました。

環境 バージョン
Python 3.7.2
Django 2.1.7
Bootstrap 4.3.1
SQLite3 Django組込みのもの

Bootstrapで必要となるjsは読み込ませています。下記URL記載の通り、CDNから読み込むようにしています。

getbootstrap.com

縛りとして、自分でJavaScriptを書いて、動的にするというのは避けます。前述の通りさくっと作る必要があるからです。

もちろんDBもさくっと用意します。MySQLをインストールしている場合ではない。

主要な部分

Djangotemplates部分はほとんど触れません。

テーブル

入力ファイル一つを、一つのテーブルとみなして登録する仕組みにしています。

カラム

テーブルの下には、カラムが存在します。

なお、テーブルとカラムに関しては、データ数が多すぎたため、csv取込を実装し、そちらでアプリデータを初期化しています。

ソース例

  • content.py
"""コンテンツ系"""
from django.db import models

class Table(models.Model):
    """テーブル一覧"""
    name = models.CharField(max_length=100)

class Column(models.Model):
    """カラム定義(物理名格納)"""
    table = models.ForeignKey(Table, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)

class LogicalColumn(models.Model):
    """カラム論理名"""
    column = models.OneToOneField(Column, on_delete=models.CASCADE, primary_key=True)
    name = models.CharField(max_length=100)

modelは単純で、これに付随するビュー等があります。列には論理名が設定されている場合があるので、それも領域として用意されています。

カラム種類

カラムをグルーピングして扱うための概念です。

分かりやすいところでは「日付」項目、「ID」項目、「結果Aで使用する」項目のように、任意の単位でグループを作成できます。

後から追いやすいです。

コメント

カラム種類に対して、コメントを付与できます。

「結果Aで使用する」項目という種類を用意したら、そこに計算式をコメントとして書いておくと、仕様を見なくてもだいたいの内容は分かります。

ソース例

  • kind.py
"""分類系"""
from django.db import models
from inputs.models import Column

class KindDefinition(models.Model):
    """カラム種類定義"""
    name = models.CharField(max_length=50)

class ColumnKind(models.Model):
    """カラム種類マッピング"""
    definition = models.ForeignKey(KindDefinition, on_delete=models.CASCADE)
    column = models.ForeignKey(Column, on_delete=models.CASCADE)

class KindDescription(models.Model):
    """種類説明"""
    definition = models.OneToOneField(KindDefinition, on_delete=models.CASCADE, primary_key=True)
    comment = models.TextField()

種類説明となっている部分が、コメント登録場所を指します。

列検索

contet.pyの中、ColumnLogicalColumnに対して検索を行います。

  • forms.py
from django import forms

class ColumnForm(forms.Form):
    """列フォーム"""
    column_name = forms.CharField(label="列名")

検索対象を入力するだけのフォームを用意します。

  • views.py
class ColumnSearchView(FormView):
    """列検索ビュー"""
    model = Column
    template_name = "search.html"
    form_class = ColumnForm

    def form_valid(self, form):
        if form.is_valid():
            return redirect(
                'inputs:search_res_col',
                name=form.cleaned_data['column_name']
            )
        return render(self.request, self.template_name)

POSTされてくるので、リダイレクトします。

class ColumnSearchResultView(ListView):
    """列検索結果ビュー"""
    template_name = 'search_result.html'
    context_object_name = 'columns'
    paginate_by=50

    def get_queryset(self):
        name = self.kwargs.get('name', '')
        if not name:
            return Column.objects.none()
        
        phys = Column.objects.filter(
            name__contains=name
        ).values(
            'id'
        )

        logs = LogicalColumn.objects.filter(
            name__contains=name
        ).select_related(
            'column'
        ).values(
            'column__id'
        )

        return Column.objects.filter(
            id__in=phys.union(logs)
        ).select_related(
            'table', 'logicalcolumn'
        )

QuerySet.unionを使って、クエリ発行を遅延させています。(たぶん)クエリ発行は一度で済んでいます。(目立った速度遅延が無かったため未検証)

  • urls.py(抜粋)
urlpatterns = [
    # 省略
    path('search/column', ColumnSearchView.as_view(), name='search_col'),
    path('search/column/<str:name>', ColumnSearchResultView.as_view(), name='search_res_col'),
    # 省略
]

パラメータならsearch/column?name=xxxみたいな形式にしたかったのですが、調べてもすぐに実装できなかったので妥協しました。

リンクを張り巡らせる

テーブル、カラム等、すべての項目には一覧画面を作成しています。

それらすべてに対し、template側でa要素を作るようにし、どこからでも飛べるようにしています。

これにより、カラム種類を見ているときに

あれ、このテーブルの定義どうなってたっけ?

という疑問が生じたとき、即座にリンクを辿って飛べます。

結果

上記では省略していますが、CRUD操作は必要そうなものから実装しています。

Djangoでは、viewMVCController相当)のテンプレートとして、ListViewCreateViewUpdateVIewDeleteView等が用意されており、少ない実装量でいろいろ作れます。

結果、分析が捗りました。

導入したほうが良い概念

上記で書いたぐらいの処理は、とてもさくっと作れました。

しかし、設計はほぼ詰めずに作ったので、改善点はたくさんあります。

全文検索

ツール上、カラム名のみ検索対象としています。

それを、テーブル名、カラム種類名、コメントまで含めて検索したらいいのに、と思い始めました。

これはDB構造を変えず、Viewを作ればなんとかなります。

Content概念

主要概念の上位概念です。ツールを使っているうちに、どのコンテンツに対してもコメントを付与したいと思うようになりました。また、グループ化した概念を、さらにグループ化して管理したい、と思い始めました。

そうすると、KindDescriptionがカラム定義に紐づいている、という構造が邪魔になります。

そこで、コンテンツという概念で、要素を扱ったほうが良いのでは?と思い始めました。コンテンツ種類として、「テーブル」「列」「グループ」があるイメージです。

こうすると、

  • コメントをコンテンツに紐づけることで、どこにでもコメントできる
  • コメント検索が楽になる
  • コンテンツ同士のリンクが作れるようになる

等々、多くのメリットが得られます。

もっとも、紐づけ管理が面倒にはなるので、一長一短です。今の規模なら、各要素にコメントを用意すれば事足りますし、コメント一覧と各要素の対応表テーブルを用意する等、手段は様々です。

おわりに

半ば、殴り書きに近い感じでしたが、私はこういう感じで仕事しています。こうやってまとめてみると、仕様をWikiにまとめれば十分では?と思いましたが、そのとき思いつかなかったので仕方ありません。

Djangoの使い方をかなり覚え、ローカルツール作るのにも使える!と確信したのでやってみましたが、案外何とかなりました。

分析できればいいのであれば、ツールなど必要ないのです。しかし、資料をいくら眺めても整理できなかったので、ツール作りで回り道したけど作ってよかったです。

なお、作成は約2日、業務時間の半分ずつを使ったので、実質1日で作成しています。

分析用途にちゃちゃっとツールを作る、というの、結構楽しいものです*4。前職はVisual Studioのライセンス自由に使えたので、いろいろ補助ツールを作っていましたし、自由にやってよい環境が人を育てる面もあると思いました。

せっかくだし、完全に作り替えて自分用にしてしまおうか、などと考えています。

ではでは、今回もご覧いただきありがとうございました。

*1:Pythonのモジュールとして、pipで公開ならできるかも…?

*2:この件に関して思うところはあるが、本題から逸れるため触れない。思うところを書いたほうがPV数伸びるとは思うが

*3:こういう場合でもWPFやUWPを選ぶ

*4:正直、かなりしんどい案件なので、こういうところで楽しまないとやってられない