2021年4月13日火曜日

ひとつのフォームで複数行を扱う

 以下のような相談があったので、どのような実装ができるのか紹介します。

都道府県と市町村ごとに、その土地のお土産を一覧する画面を作ります。お土産は複数ありますが、表示は一行、編集も市町村ごとに行いたい、というのが要件です。画面上はお土産をひとまとめに編集しますが、表にはお土産ひとつひとつを1行として保存します。

クイックSQLのモデルは以下です。

# prefix: sve
# semantics: default
souvenirs
    prefecture vc40 /nn
    city       vc80 /nn
    item       vc80 /nn

SQLワークショップユーティリティからクイックSQLを開きます。上記のモデルを左ペインに入力し、SQLの生成SQLスクリプトを保存レビューおよび実行を行い、表SVE_SOUVENIRSを作成します。アプリケーションの作成は行いません。

表が作成されたら、空のアプリケーションを作成します。アプリケーション・ビルダーからアプリケーション作成ウィザードを実行します。名前土産管理とします。

アプリケーションが作成されたら、お土産の一覧表示と入力/編集/削除をする画面を作成します。ページの作成を実行します。


フォームを選択します。


フォーム付きレポートを選択します。


レポート・タイプ対話モード・レポートレポート・ページ名としてお土産一覧フォーム・ページ名お土産編集とします。フォーム・ページ・モードモーダル・ダイアログを選択します。以上を設定し、に進みます。


ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択し、に進みます。


データ・ソースとしてローカル・データベースソース・タイプSQL問合せを選択します。SQL SELECT文を入力に、以下を記述します。
select
    prefecture,
    city,
    listagg(item, ',') within group (order by item) items
from sve_souvenirs
group by prefecture, city
お土産については、都道府県、市町村ごとに、LISTAGG集計関数を使って、カンマ区切りで一項目にまとめます。


主キー列PREFECTURE(Varchar2)2次キー列CITY(Varchar2)を選択し、作成をクリックします。レポートとフォームのページが作成されます。

主キー列、2次キー列となっている列はデフォルトでタイプが非表示になるため、タイプをプレーン・テキストに変更します。列PREFECTURECITYの両方を選択し、識別タイププレーン・テキストに変更します。


続いて、ページ・デザイナでフォームを開きます。

ページ・アイテムP3_PREFECTUREタイプ非表示からテキスト・フィールドに変更し、ラベルPrefectureを設定します。ページ・アイテムのプロパティ必須の値ONにすべきですが、画面サイズの都合でスクリーンショットには含んでいません。


同様にページ・アイテムP3_CITYタイプテキスト・フィールドに変更し、ラベルCityを設定します。


集計関数を含むSQLを対象とした更新処理はできないため、フォームのプロセスを変更します。

設定ターゲット・タイプPL/SQL Codeに変更し、挿入/更新/削除するPL/SQLコードとして以下を記述します。
begin
    case
    when :APEX$ROW_STATUS = 'C' then
        insert into sve_souvenirs(prefecture, city, item)
        select :P3_PREFECTURE, :P3_CITY, trim(column_value)
        from apex_string.split(:P3_ITEMS, ',')
        where trim(column_value) is not null;
    when :APEX$ROW_STATUS = 'U' then
        -- 未登録のお土産があれば、挿入する。
        insert into sve_souvenirs(prefecture, city, item)
        select prefecture, city, item
        from
        (
            select :P3_PREFECTURE prefecture, :P3_CITY city, trim(column_value) item
            from apex_string.split(:P3_ITEMS, ',')
            where trim(column_value) is not null
            minus
            select prefecture, city, item
            from sve_souvenirs
            where prefecture = :P3_PREFECTURE and city = :P3_CITY
        );
        -- 更新対象に含まれない既存のお土産を削除する。
        delete from sve_souvenirs
        where 
        (prefecture, city, item)
        in
        (
            select prefecture, city, item
            from sve_souvenirs
            where prefecture = :P3_PREFECTURE and city = :P3_CITY
            minus
            select :P3_PREFECTURE prefecture, :P3_CITY city, trim(column_value) item
            from apex_string.split(:P3_ITEMS, ',')
            where trim(column_value) is not null
        );
    when :APEX$ROW_STATUS = 'D' then
        delete from sve_souvenirs 
        where prefecture = :P3_PREFECTURE and city = :P3_CITY;
    end case;
end;

Region Sourceの設定では行のロックができないので、行のロックをPL/SQL Codeに変更し、以下のコードを設定します。
declare
    r sve_souvenirs%rowtype;
begin
    select * into r from sve_souvenirs 
    where 1=1
      and prefecture = :P3_PREFECTURE
      and city = :P3_CITY
      for update nowait
    ;
end;

以上でアプリケーションの作成が完了しました。作成したアプリケーションを実行すると、表題のGIF動画の動作を確認できます。

表SVE_SOUVENIRSの内容を確認すると、列ITEMにお土産がひとつづつ登録されていることが確認できます。


以上です。

行のロック時間を調整したい場合は、PL/SQLのコードに含まれるselect for update文にwait指定を含めるようにします。

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/souvenir-management.sql

Oracle APEXのアプリケーション作成の参考になれば幸いです。