2023年4月16日日曜日

従属するテーブルの連動削除

昨日触れた header - detail 構造はデータベースにおいても、当然使われる構造である。 帳票の基本情報としての header と、それに対する明細情報としての detail など、事務処理においてはお約束のような構造である。

今回は、header のレコードを削除した場合に、従属する detail のレコードを連動して削除するクエリを考えてみる。 データベース上でテーブルの整合性を取るための処理である。

まず、主体となるテーブル header は主キー key を持っているものとする。 従属するテーブル detail は主キー key, no を持っているものとする。 header と detail の key は紐づいたれコードが登録されているものとする。

header レコード削除後に、detail 側で紐づいていないレコードを削除する DELETE 文の書き方はいくつか存在するため、 何パターンか書いてみることにしよう。

1. サブクエリを使って主キーが存在しない内容を削除する
DELETE FROM detail
 WHERE key NOT IN (
    SELECT key FROM header
 );

2. 外部結合で紐づかない内容を削除する
DELETE FROM detail
 USING detail d
  LEFT OUTER JOIN header h ON d.key = h.key
 WHERE detail.key = d.key AND h.key IS NULL;

3. NOT EXISTS を使う
DELETE FROM detail
 WHERE NOT EXISTS (
    SELECT 1
      FROM header h
     WHERE detail.key = h.key
 )

実際にパフォーマンスなどのテストする場合は、トランザクションをかけてロールバックしながら何度も分析する形になると思うが、 クエリ上で行うならば、以下のように EXPLAIN ANALYZE を使えば、その結果でパフォーマンスを確認できる。
BEGIN;

EXPLAIN ANALYZE
DELETE FROM detail
 WHERE key NOT IN (
    SELECT key FROM header
 );

ROLLBACK;
数千件程度では結果としては 1 の方法がシンプルで概して速かった。2 と 3 は外で結合させている手順の分だけ時間がかかっており、 メモリに展開できる程度の小さなデータであれば 1, 3, 2 の順番で速かった。

しかし、手元にあった50万件ほどのレコードで試してみると、案の定というか、3, 2 の順で速く、1 に至っては数分待ってもレスポンスが返らない。 という結果になった。サブクエリの内容を全展開した上で、否定的な判定をする必要があることから、対象となるデータ量によっては膨大なメモリが 必要となるためだろう。そのため、想定されるレコード数によっては全く使い物にならない方法と言える。

クエリの書き方で、この処理は重そう、軽そう。というのが感覚的に解る十分な経験や実績があり、技量に自信があれば、直感で組み上げても それほど問題が起こることはないだろうが、少しでも怪しいのであれば、実際の環境を想定した条件で逐一確認すべきである。 そのためにも、データベースを扱う開発者は、テストに使える様々な規模のデータを準備しておくべきでもある。

2023年4月15日土曜日

header - detail 構造

よくあるデータ構造として、header - detail 構造が挙げられる。

概念


主体の基本情報 header と、主体に従属する詳細情報 detail の組み合わせで、 主体側:1 に対して 従属側:N の関係になる構造である。
実際には、これらを複合的に組み合わせた構造で捉える必要のあることも多いだろうが、 主体側をグループや人物として捉えても成立するし、それらに汎用的なパラメータとして詳細情報を紐づけるなど、 この単位で現実なり業務の要素をオブジェクトとして捉えるスキルがあれば、それだけで幅広く現実をデータ構造に落とし込めるようになる。 それほどまでにこの構造は世に溢れている。

具象


開発言語で捉える場合、主体側が集合要素を捉えられる何らかのコンテナ構造(Array, List, Queue, Stack, Set, Map など)を介して従属側を所有あるいは参照する形で扱うのが一般的と言えるだろう。 Java であれば、このコンテナ構造のことをコレクションと呼称しているようだ。

扱うデータ構造と必要な操作に合わせたコンテナを組み合わせることで、実装者はデータを捉えやすくなる上にパフォーマンスも上げられるため、できれば幅広く扱えたほうが良いが、 一般的な業務では List, Set, Map 辺りの汎用的な構造が最低限使えれば、それらの組み合わせ次第で(効率を気にしなければ)他は代用できるため、そこまで困ることはないが、 できる限り設計に対して明確なコンテナで捉えることが望ましい。汎用的な構造では、どういう目的でどのように管理しようとしているのか?など用途が曖昧になる場合もあるため、 説明や管理方法が簡潔になる最適なコンテナが存在するのであれば優先的に選択すべきである。

個々の値を意識しなければ扱えない構造だった場合、開発中はそこまでの問題はないだろうが、一旦その開発から離れて数ヶ月や数年経ってから保守や拡張などの作業をする際に、 意味を思い出すための時間が必要となるため、オブジェクト指向的な理想としては、実データの個々の値を意識して直接的に扱うのではなく、抽象化した概念で扱える状況の方が 中長期に渡っての保守性が安定する。構造化的に捉えるのであれば、対象データが構造を使って隔離できており、各項目に対する説明なりドキュメント整備ができていれば そこまで時間が取られることはないだろう。

2023年4月14日金曜日

PostgreSQL で日付型絞り込み検索

PostgreSQL において、テーブル t に含まれる登録日時を記録した日付型のカラム entryTime が存在しているとする。

そのテーブルに対して先々月、先月、当月に該当するレコードをそれぞれ取り出す。なんてのは業務処理のレポート出力でありがちなパターンと言える。

それぞれのクエリを書いてみよう。

先々月
SELECT *
  FROM t
 WHERE date_trunc('month', current_date) - interval '2 month' <= t.entryTime
   AND t.entryTime < date_trunc('month', current_date) - interval '1 month';

先月
SELECT *
  FROM t
 WHERE date_trunc('month', current_date) - interval '1 month' <= t.entryTime
   AND t.entryTime < date_trunc('month', current_date);

当月
SELECT *
  FROM t
 WHERE date_trunc('month', current_date) <= t.entryTime
   AND t.entryTime < date_trunc('month', current_date) + interval '1 month';

interval の指定は '-1 month' 等の書式も有効であるため、上記のクエリをシステムに組み込む際には パラメータ置き換えを使って全て同じ式でまとめて表現することも可能である。

この手の実装でよくあるミスとしては、月初から月末の 00:00:00 までの日付範囲で判定して月末の時刻を持つデータが拾えていないパターンである。 上記のクエリで示したように対象月の月初から翌月の月初未満の範囲で判定するのが漏れの発生しない手堅く確実な方法だと言える。

2023年4月13日木曜日

DOM 抽象化

XML、SVG、あと XHTML なんかは、それぞれ異なるデータ形式やマークアップ言語だが、 これらすべてのデータ構造は、DOM(Document Object Model)という共通の概念で扱うことができる。 DOM は、データの構造や内容に関係なく、一貫したインターフェイスを提供するプログラムと文書のための 標準モデルであり、これを使うことで開発者は異なるデータ形式に対して同じ方法でアクセスし、 操作することが可能になる。

次の XML に対して、JavaScript と Java で同じ DOM操作を行うコードを考えてみる。
<?xml version="1.0" encoding="UTF-8"?>
<fruits>
  <fruit id="1">
    <name>Apple</name>
    <color>Red</color>
  </fruit>
  <fruit id="2">
    <name>Banana</name>
    <color>Yellow</color>
  </fruit>
</fruits>
以下は JavaScript で xml を制御するコードである
// DOMParserを使用して、XML文字列をDOMオブジェクトに変換
const xmlString = `<上記のXMLデータ>`;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");

// 最初のフルーツ要素を取得
const firstFruit = xmlDoc.getElementsByTagName("fruit")[0];

// フルーツの名前を取得
const firstName = firstFruit.getElementsByTagName("name")[0].textContent;
console.log("First fruit name: " + firstName);

// 新しいフルーツ要素を作成し、fruits要素に追加
const newFruit = xmlDoc.createElement("fruit");
newFruit.setAttribute("id", "3");
const newName = xmlDoc.createElement("name");
newName.textContent = "Orange";
const newColor = xmlDoc.createElement("color");
newColor.textContent = "Orange";
newFruit.appendChild(newName);
newFruit.appendChild(newColor);
xmlDoc.getElementsByTagName("fruits")[0].appendChild(newFruit);

以下は Java で xml を制御するコードである
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Main {
  public static void main(String[] args) {
    try {
      // XML文字列をDOMオブジェクトに変換
      String xmlString = "<上記のXMLデータ>";
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = factory.newDocumentBuilder();
      Document xmlDoc = builder.parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8")));

      // 最初のフルーツ要素を取得
      NodeList fruitNodes = xmlDoc.getElementsByTagName("fruit");
      Node firstFruit = fruitNodes.item(0);

      // フルーツの名前を取得
      NodeList firstNameList = ((Element) firstFruit).getElementsByTagName("name");
      String firstName = firstNameList.item(0).getTextContent();
      System.out.println("First fruit name: " + firstName);

      // 新しいフルーツ要素を作成し、fruits要素に追加
      Element newFruit = xmlDoc.createElement("fruit");
      newFruit.setAttribute("id", "3");
      Element newName = xmlDoc.createElement("name");
      newName.setTextContent("Orange");
      Element newColor = xmlDoc.createElement("color");
      newColor.setTextContent("Orange");
      newFruit.appendChild(newName);
      newFruit.appendChild(newColor);
      xmlDoc.getElementsByTagName("fruits").item(0).appendChild(newFruit);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

これらのコードは、XMLデータから最初のフルーツ要素を取得し、その名前を表示し、 新しいフルーツ要素を作成して追加する機能を実装している。 両方のコードからJavaScriptとJavaの両方でXMLデータを扱い、DOM操作を行う方法を確認できるだろう。

JavaScriptとJavaでの主な違いは、オブジェクト生成とメソッド呼び出しの方法であり、 JavaScriptでは、DOMParser と createElement を使用して新しいオブジェクトを作成し、 Javaでは DocumentBuilderFactory と DocumentBuilder を使用する。 また、要素の取得や操作に関しても、それぞれ言語に応じたメソッドが提供されている。

この例からも、DOM操作に関しては異なる言語であっても同様のアプローチを取れることが理解できるだろう。

もちろん、今の時代に DOM を意識して操作することが適切なのか?という根本的な話はあるが、 これを理解した上で、それぞれのデータ形式や利用する開発言語、実行環境に合わせた方法を選択すべきだろう。

2023年4月12日水曜日

Chrome を使ったフォルダ走査

2023-04 現在、Chromium系のブラウザは、ドロップされたフォルダに対する走査が実現可能であり、 再帰的にフォルダを走査することで、配下も含めたフォルダやファイルが制御可能なため、 フォルダ配下のファイルをまとめてアップロードすることもできる。

つまり、サーバ側のAPIを整えフォルダまで扱える機能がサーバ側に備わっていれば、 フォルダ階層も含めてファイルをアップロードできるのである。

ただし、これが実行できる状況には制限があり、サーバとの接続が localhost か https 環境のみ、 いわゆるセキュリティ的に問題のない経路でしか動作しない。

特にイントラ内での開発やテストにおいて、192.168.*.* 等に http で接続して動かない。 というパターンで動かない。という状況に陥る人が多いため注意すること。

で、今回はドロップされたフォルダを再帰的に走査する JavaScript を書いてみようと思う。

以下に HTML 一式を示す。ローカルのウェブサーバに置けば確認できるはずだ。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ドロップされたフォルダの走査</title>
  <style>
    #drop-area {
      width: 100%;
      height: 200px;
      border: 2px dashed #ccc;
      text-align: center;
      line-height: 200px;
    }
  </style>
</head>
<body>
  <div id="drop-area">フォルダをここにドロップ</div>
  <ul id="file-list"></ul>
  <script>
    function handleFolder(folder) {
      const reader = folder.createReader();
      reader.readEntries(function (entries) {
        entries.forEach(function (entry) {
          if (entry.isDirectory) {
            handleFolder(entry);
          } else if (entry.isFile) {
            const listItem = document.createElement('li');
            listItem.textContent = entry.fullPath;
            document.getElementById('file-list').appendChild(listItem);
          }
        });
      });
    }

    document.getElementById('drop-area').addEventListener('dragover', function (event) {
      event.preventDefault();
    });

    document.getElementById('drop-area').addEventListener('drop', function (event) {
      event.preventDefault();
      const items = event.dataTransfer.items;
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.kind === 'file' && item.webkitGetAsEntry().isDirectory) {
          handleFolder(item.webkitGetAsEntry());
        }
      }
    });
  </script>
</body>
</html>

Chromium系ブラウザでは、webkitGetAsEntry() メソッドを使って、ファイルアイテムからディレクトリエントリを取得できるため、 フォルダである場合はフォルダ走査を行う handleFolder関数を呼び出している。

handleFolder関数は、渡されたフォルダを走査し、ディレクトリだった場合は再帰的に走査し、ファイルだった場合はファイルパスを ul要素に追加している。

実際のプロジェクトで使うには、複数ドロップされた場合やドロップ後のアップロード処理、エラー処理など様々な処理が必要となるため、 非常に複雑なものとなってしまうが、基本的なフォルダ走査を行う要素は上記のように再帰的な処理で捉えてしまえば単純なものとなる。


2023年4月11日火曜日

JavaScript XMLHttpRequest を使ったファイルアップロードの進捗状況表示

今回は XMLHttpRequestを使ってファイルをサーバにアップロードする際に、その進捗状況を把握したい場合のコードを書いてみる。

コードは当然 JavaScript である。以下に HTMLと合わせたコードを示す。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ファイルアップロードの進捗状況</title>
  <style>
    #progress-bar {
      width: 100%;
      background-color: #f3f3f3;
      position: relative;
    }
    #progress-bar span {
      position: absolute;
      height: 100%;
      background-color: #4CAF50;
    }
  </style>
</head>
<body>
  <input type="file" id="file-input">
  <div id="progress-bar"><span></span></div>
  <script>
    document.getElementById('file-input').addEventListener('change', function (event) {
      const file = event.target.files[0];
      const xhr = new XMLHttpRequest();
      const formData = new FormData();

      xhr.open('POST', '/upload', true);

      xhr.upload.addEventListener('progress', function (event) {
        if (event.lengthComputable) {
          const percentage = (event.loaded / event.total) * 100;
          document.getElementById('progress-bar').querySelector('span').style.width = percentage + '%';
        }
      });

      xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          if (xhr.status === 200) {
            alert('ファイルのアップロードに成功しました。');
          } else {
            alert('ファイルのアップロードに失敗しました。');
          }
        }
      };

      formData.append('file', file);
      xhr.send(formData);
    });
  </script>
</body>
</html>

input type="file" 要素と、進捗状況を表示する div 要素を配置し、CSS で調整しておく。

input 要素が change されたタイミングで XMLHttpRequest を使ってサーバにPOSTする。

リクエストの進捗状況を表示するには、xhr.upload オブジェクトの progress イベントを使う。

event.loadedとevent.totalプロパティを使って、アップロードの進捗率(進捗率)を計算し、その値を使って進捗バーの幅を更新している。

最後に、xhr.onreadystatechangeでリクエストの状態を監視し、リクエストが完了したら(readyState === XMLHttpRequest.DONE)、ステータスコードが200の場合はアップロード成功、それ以外の場合はアップロード失敗として単純にアラートで通知している。

実際のプロジェクトでは、エラー処理やファイル制限等が必要となるため、このようなサンプルと同じようにはいかないだろうが、進捗管理の要点は十分に理解できると思う。


2023年4月10日月曜日

年月に対する締め日範囲の算出

会計処理や月次レポートを行うシステムにおいて、対象年月(YYYYMM形式)と締め日(1~28,99:月末)から、対象年月に対する日付範囲を算出したい状況というのが稀に存在する。

今回はその局面で使えそうなコードを Java で書いてみようと思う。

public static void outputCutoffDateRange(int yyyymm, int cutoffDay) {
    int year = yyyymm / 100;
    int month = yyyymm % 100;

    YearMonth targetYearMonth = YearMonth.of(year, month);

    LocalDate startDate;
    LocalDate endDate;

    if (cutoffDay == 99) {
        endDate = targetYearMonth.atEndOfMonth();
        startDate = targetYearMonth.minusMonths(1).atEndOfMonth().plusDays(1);
    } else {
        startDate = targetYearMonth.atDay(cutoffDay).minusMonths(1).plusDays(1);
        endDate = targetYearMonth.atDay(cutoffDay);
    }

    LocalDate currentDate = startDate;
    while (!currentDate.isAfter(endDate)) {
        System.out.println(currentDate.getYear() * 10000 + currentDate.getMonthValue() * 100 + currentDate.getDayOfMonth());
        currentDate = currentDate.plusDays(1);
    }
}
単純に年月と締め日を使って開始日と終了日を算出し、開始から終了までの範囲を走査するだけである。
試しに実行してみる。それっぽい日付は出てるか?
public static void main(String[] args) {
    outputCutoffDateRange(202304, 15);
    System.out.println("---");
    outputCutoffDateRange(202303, 15);
    System.out.println("---");
    outputCutoffDateRange(202302, 99);
    System.out.println("---");
    outputCutoffDateRange(202302, 15);
}
これを基準に出力をDate型に変えたり、クラス化して Iterable で実装するなど使い方はいろいろあるかもしれない。

2023年4月9日日曜日

年月範囲の算出

会計処理や様々な期限のレポートを扱うシステムにおいて、開始年月から終了年月までの範囲を年月で扱いたい状況というのが稀に存在する。
  
今回はそれに対するコードを Java で書いてみようと思う。
public static void outputYearMonthRange(int startYearMonth, int endYearMonth) {
    int currentYearMonth = startYearMonth;
    while (currentYearMonth &lt;= endYearMonth) {
        System.out.println(currentYearMonth);
        if (currentYearMonth % 100 == 12) {
            currentYearMonth += 89; // 12 -&gt; 01
        } else {
            currentYearMonth += 1;
        }
    }
}
開始年月と終了年月をYYYYMM形式の数値として渡してもらい、開始から終了の範囲を走査するだけである。

試しに実行してみる。いい感じに出ている。
public static void main(String[] args) {
    outputYearMonthRange(202304, 202403);
}
出力を月初の Date型に変えたり、クラス化して Iterable で実装するなど組み込み方は色々あると思う。

2023年4月8日土曜日

高齢運転者による交通事故

ここ数年毎日のように高齢運転者による交通事故のニュースを目にして、その度に嫌な気分になる。実際、高齢者が起こした国内の事故統計情報を見る限り1日1件以上起こっている計算なのだから、目にするのは当然と言えるのだろう。

誰しもミスはあるし、事故が起こっても元の状態に戻せるならそこまで大きな問題でもないが、現実は残酷で、将来があったはずの若者が被害に合い、その後の生活に一生障害をかかえたり、場合によっては死亡する事故を見る限り、あまりにも非対称性が強い状況が多く、加害者である老人がそれに見合う代償を払えるかどうか怪しいことの方が多く感じる。


そのような状況に対して、私は一つエンジニアとして現実的な提言をしたい。

今後国内向けに新車として販売する一般の市販車に対して、運転免許証を車に刺さないと車のエンジンなりモーターが始動しないようにすべきだ。警察、消防、自衛隊などの特殊車両や、既に販売されている車、海外から輸入した車などは除外して良いと思う。自動車産業全体に時限措置を設け、5年程度の時間をかけて新規に販売される大多数の大衆車が運転免許証を刺さなければ動かない状態が作れることをひとまずのゴールと考える。

なぜ、運転免許証を使うのか?という観点に対して、一番の理由として現状の免許返納が意味のある行為になっていないからである。

歯に衣着せぬ物言いをさせてもらうが、既にボケ老人だった場合、免許を返納したとしても動く状態の車が残っており、家に車の鍵があれば、自身が免許を持っていない事も忘れて車を暴走させてしまうのは過去日本中で幾度となく事故が繰り返されている以上、覆りようのない事実である。

であるならば、物理的に運転免許証が無ければ動かせない状況を作ってしまう方が良く、それで事故被害者が減らせるなら安いものである。勝手に家族の免許証を老人に奪われた。などの屁理屈を言う輩は出るだろうが、管理できていない状況などは知ったことではない、奪われるようであれば、認知や行動に問題があるのだから、病院なり警察なりに連れて行き、被害が出る前に隔離されるべきである。本人にとっても家族にとっても責任が取れないことを起こす前にそうすべきだし、社会秩序や人々の安全を優先すべきではないだろうか?

また、一般人が使う車に免許証を刺さなければ動かせない制限が課されたとしても、そもそもに運転者は免許携帯が義務なのだから何の問題も無いはずである。多少不便になることは確かであるが、それで多くの人命が救われるのであれば全体が負担すべきコストとしては十分に釣り合うと感じる。毎回乗る際の手間の部分に関しては、確かに手間だとは思う。しかし、根本的な部分も含めて、より良いアイデアなりがあれば随時改善していけば良い話である。

技術的な実現性に関しては、現状エンジンの始動が電子制御になっていない車など皆無であり、その状況で制御を加えるのが難しいわけがない。そんなに簡単にできないというのなら技術者の頭を疑う。そして、日本の運転免許証には既に Mifare Type-B のチップが埋め込まれており、個人であっても市販の安価なカードリーダを使って中身にアクセスできる。実際に運転免許証に対してコマンドを叩き、PIN コードを渡せば、免許証にJPEG2000形式の顔写真やデータが入っていることを個人レベルで確認できるのだから。

そして、自動車でのカードの識別はPINコードを入れることもせず、個人認証をする必要なども一切なく、運転免許証のチップが埋め込まれたカードであることだけを認識できれば良いと考える。難しいことはさせず、誰でも使える手順にすることを考えると、これだけで目的は果たせるからである。

PINコードを入れない限り運転免許証の中のデータは取り出せないため、プライバシーやセキュリティの問題も回避できる。後ろめたい事も無く普通に生きている市民からすると、これで問題ないはずだ。権利侵害だ何だとやたら騒ぐ連中は犯罪者のような連中としか考えられないため、そのような輩の戯言は無視すべきだろう。

チップの認識だけであれば、機能的にも複雑さが抑えられるため、そのレベルのモジュールを大量に導入するのであれば、開発も生産も難しいはずがなく、今後の国産の新車に付けるのであれば価格も当然抑えられるべきであるが、生産側も慈善事業ではないため、ある程度の金額であれば仕方ないと思う。ただ、その価格分も国民の安全のために国策として税金投入しても未来の人命が救われるのであれば安いぐらいだ。国策として国交省辺りが主導的に行うのは利権が作られる可能性が高いとはいっても、実現性を考えれば理想かもしれない。

既存の車に対しては、後付けできるような運転免許証スロット的なモジュールを流通させ、モジュールや作業工賃などの一部をキャッシュバックするキャンペーンを国主導で実施するのも良いと思う。

もちろん、これだけでそれまでの問題が全て解決できるわけではないだろうし、上記の策も完璧なはずもないだろう。しかし、少なくともやらないより、やったほうが確実に人命が救えると確信しているし、我々はできることを模索し続け、実践し、間違っていたなら協議し、より良い方向へ軌道修正すれば良いだけの話ではないのだろうか?

交通事故自体が減ることと、被害があってもできるだけ軽症で済むことを願う。



2023年4月7日金曜日

JSONIC で総称型を使ったデコード

 以下、マニュアルに記載のある内容であり、知っている人にとっては読む価値は無いメモ書き。

何度も忘れるので、ここに書いておく。

Java で JSON を扱う場合に未だ JSONIC を使っている。jackson への移行が推奨されているが、特にパフォーマンス面での不満を感じる規模の要件を対処することもなく機能的にも困っていないため、未だに利用している。

さて、以下のようなシンプルな配列の JSON がサーバに投げ込まれた場合において

[1, 2, 3]

Java で対応する場合、List<Integer> の形式でデコードしたい状況が存在する。

ただし、Java の文法的に以下のように総称型は指定できないため書けない。

String json = "[1,2,3]";
List<Integer> list = JSON.decode(json, List<Integer>.class); // 文法エラーとなる

このような状況に対して、TypeReference クラスを使ってオブジェクトで指定する形を取れば解決する。

List<Integer> list = JSON.decode(json, new TypeReference<List<Integer>>() {});

あるいは渡されるデータをフロントエンド側で投げ込むデータを JSON Object そのものにしてもらい。

{"list": [1, 2, 3]}

以下のように JSON リクエストデータに対応するクラスを準備し、クラス自体を使ってデコードすればよい。

@Data
public class ListRequestJson implements Serializable {
  private List<Integer> list;
}

...

ListRequestJson req = JSON.decode(json, ListRequestJson.class);

2023年4月6日木曜日

PostgreSQL で変数を使う

PostgreSQL のクエリ内で変数を使う方法

運用環境で連番を持つマスタレコードをクエリを使って追加したい場合など、 採番結果を後続のクエリで適用したい状況で、どのように解決するか? このような場合、いくつか方法がある。
 
1. WITH の利用
2. PL/pgSQL の利用

他にも物理的な一時テーブルを作っての対処などでも目的は果たせるが、 それは通常の DB の使い方と変わらないため、ここでは省略する。

1. WITH の利用

以下のクエリが参考になる。
WITH variables AS (
    SELECT 10 AS my_variable
)
SELECT my_variable
  FROM variables;
WITH で定義した内容を後続のクエリで参照して使うことができる。
 
この方法では、WITH で一時テーブルを作り、その直後のクエリで参照すれば使えるだけであり、 一度採番した内容を変数に格納し、連続したクエリで変数を使い回すような処理には適用できない。 そのため、使える局面はかなり限定される。わざわざ覚えて使うほどのものではないと感じる。
 

2. PL/pgSQL の利用

stored procedure をクエリ上で使う方法である。
 
この方法であれば一般的なプログラミングと同じように 完全な変数宣言が可能となるため、連続した処理でも使い勝手が良い方法となる。
 
PL/pgSQL は PL/SQL や Pascal系の言語を書いたことがある人であれば覚える量も少なく簡単に書ける。
 
以下のようなコードが採番処理とレコードの登録、マスタからリンク整備の雛形として使えるだろう。
DO $$ 
DECLARE
	master_id VARCHAR;
	copy_id VARCHAR;
	temp_no INTEGER;
	copy_no INTEGER;
	rec RECORD;
BEGIN
	SELECT '今回追加するマスタID' INTO master_id;
	SELECT 'リンク内容をコピーするID' INTO copy_id;

	-- 登録済判定
	SELECT m.no INTO temp_no FROM master_table m WHERE m.id = master_id;
	IF (temp_no <> 0) THEN
		RAISE NOTICE 'ID:[%] 登録済のため中断', master_id;
		RETURN;
	END IF;

	-- 採番と登録
	SELECT Coalesce(Max(m.no), 0) + 1 INTO tempNo FROM master_table m;
	INSERT INTO master_table (no, id, ... 必要なカラム)
	VALUES (temp_no, master_id, ... マスタ設定内容);
	RAISE NOTICE 'ID:[%] 登録完了', master_id;

	-- 指定ID と同じグループにリンクを貼る
	SELECT m.no INTO copy_no FROM master_table m WHERE m.id = copy_id;
	IF (copy_no = 0) THEN
		RAISE NOTICE 'ID:[%] コピー元が未登録のため中断', copy_id;
	END IF

	-- リンクテーブルに対するSELECT結果を使って新規にリンクを登録する
	FOR rec IN SELECT * FROM link_table l WHERE l.no = copy_no;
	LOOP
		INSERT INTO link_table (no, linkNo) VALUES (temp_no, rec.linkNo);
	END LOOP;
	RAISE NOTICE 'ID:[%] リンク整備 完了', master_id;
END $$;
PL/pgSQL だと癖はあるが、SQL上で可能な操作は全てできるため、ロジックを組む力さえあればなんとでもなる。