作成したアプリケーションは以下のように動作します。
以下より、アプリケーションの作成手順を紹介します。
最初に以下のパッケージSUDOKUを作成します。数独の検証ロジックを実装しています。また、数独の9x9のマトリックスの数値はパッケージ変数G_MATRIXに : (コロン)で区切った文字列として保存します。G_ERRORSには重複している値を保存します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package sudoku | |
as | |
G_MATRIX apex_t_number; /* 9x9のセルを保持する。 */ | |
G_ERRORS apex_t_number; /* エラーになったセルだけを保持する。 */ | |
/** | |
* G_MATRIXを初期化する。 | |
*/ | |
procedure initialize( | |
p_matrix in varchar2 | |
); | |
/** | |
* 検証として呼び出す。 | |
*/ | |
function initialize( | |
p_matrix in varchar2 | |
) | |
return boolean; | |
/** | |
* 9x9のマトリックスすべてを検証する。 | |
*/ | |
procedure validate_all; | |
/** | |
* 検証として呼び出す。 | |
*/ | |
function validate_all | |
return boolean; | |
/** | |
* セルの結果を確認する。 | |
*/ | |
function is_valid( | |
p_column_position in integer | |
,p_row_position in integer | |
) | |
return boolean; | |
/** | |
* 9x9の配列をアイテムに保存する。 | |
*/ | |
procedure save( | |
p_item in out varchar2 | |
); | |
end sudoku; | |
/ | |
create or replace package body sudoku | |
as | |
CO_SEPARATOR constant varchar2(1) := ':'; | |
G_IS_INITIALIZED boolean; /* G_MATRIXが有効 */ | |
G_IS_VALIDATED boolean; /* G_ERRORSが有効 */ | |
G_IS_SAVED boolean; /* すでに保存済み */ | |
/** | |
* 数独の9個の数字に重複がないことを確認する。 | |
* p_valは検証対象となる9個の数値。p_locはその数値のsudoku.G_MATRIX内の位置を保持する。 | |
*/ | |
procedure validate_i( | |
p_loc in apex_t_number | |
,p_val in apex_t_number | |
) | |
as | |
l_valid boolean := true; | |
begin | |
for c in ( | |
select column_value from table(p_val) where column_value is not null | |
group by column_value having count(column_value) > 1 | |
) | |
loop | |
l_valid := false; | |
for i in 1..9 | |
loop | |
if p_val(i) = to_number(c.column_value) then | |
G_ERRORS(p_loc(i)) := p_val(i); | |
end if; | |
end loop; | |
end loop; | |
end validate_i; | |
/** | |
* 行の検証。nは行番号。 | |
*/ | |
procedure validate_h( | |
n in integer | |
) | |
as | |
l_val apex_t_number; | |
l_loc apex_t_number; | |
l_idx integer; | |
begin | |
for i in 1..9 | |
loop | |
l_idx := ((n - 1)*9) + i; | |
apex_string.push(l_loc, l_idx); | |
apex_string.push(l_val, G_MATRIX(l_idx)); | |
end loop; | |
validate_i(l_loc, l_val); | |
end validate_h; | |
/** | |
* 列の検証。nは列番号。 | |
*/ | |
procedure validate_v( | |
n in integer | |
) | |
as | |
l_val apex_t_number; | |
l_loc apex_t_number; | |
l_idx integer; | |
begin | |
for i in 0..8 | |
loop | |
l_idx := n + (i * 9); | |
apex_string.push(l_loc, l_idx); | |
apex_string.push(l_val, G_MATRIX(l_idx)); | |
end loop; | |
validate_i(l_loc, l_val); | |
end validate_v; | |
/** | |
* 3x3のマトリックスの検証。 | |
*/ | |
procedure validate_s( | |
n in integer | |
) | |
as | |
l_val apex_t_number; | |
l_loc apex_t_number; | |
l_idx integer; | |
begin | |
if n < 4 then | |
l_idx := 1 + ((n - 1) * 3); | |
elsif n < 7 then | |
l_idx := 28 + ((n - 4) * 3); | |
else | |
l_idx := 55 + ((n - 7) * 3); | |
end if; | |
for i in 1..3 | |
loop | |
for j in 0..2 | |
loop | |
apex_string.push(l_loc, (l_idx + j)); | |
apex_string.push(l_val, G_MATRIX(l_idx + j)); | |
end loop; | |
l_idx := l_idx + 9; | |
end loop; | |
validate_i(l_loc, l_val); | |
end validate_s; | |
/** | |
* G_MATRIXを初期化する。 | |
*/ | |
procedure initialize( | |
p_matrix in varchar2 | |
) | |
as | |
begin | |
if not G_IS_INITIALIZED then | |
G_MATRIX := apex_string.split_numbers(p_matrix, CO_SEPARATOR); | |
G_IS_INITIALIZED := true; | |
end if; | |
end initialize; | |
/** | |
* 検証として呼び出す。 | |
*/ | |
function initialize( | |
p_matrix in varchar2 | |
) | |
return boolean | |
as | |
begin | |
initialize(p_matrix); | |
return true; | |
end initialize; | |
/** | |
* 9x9のマトリックス全体の検証。 | |
*/ | |
procedure validate_all | |
as | |
begin | |
if not G_IS_VALIDATED then | |
for i in 1..9 | |
loop | |
validate_h(i); | |
validate_v(i); | |
validate_s(i); | |
end loop; | |
G_IS_VALIDATED := true; | |
end if; | |
end validate_all; | |
/** | |
* 検証として呼び出す。 | |
*/ | |
function validate_all | |
return boolean | |
as | |
begin | |
validate_all(); | |
return true; | |
end validate_all; | |
/** | |
* セルの結果を確認する。 | |
*/ | |
function is_valid( | |
p_column_position in integer | |
,p_row_position in integer | |
) | |
return boolean | |
as | |
l_loc integer; | |
begin | |
if not G_IS_VALIDATED then | |
validate_all(); | |
end if; | |
l_loc := ((p_row_position - 1) * 9) + p_column_position; | |
return (G_ERRORS(l_loc) is null); | |
end is_valid; | |
/** | |
* 9x9の配列をアイテムに保存する。 | |
*/ | |
procedure save( | |
p_item in out varchar2 | |
) | |
as | |
begin | |
if not G_IS_SAVED then | |
p_item := apex_string.join(G_MATRIX, CO_SEPARATOR); | |
G_IS_SAVED := true; | |
end if; | |
end save; | |
/** | |
* パッケージ変数 G_MATRIX, G_ERRORSの初期化。 | |
*/ | |
begin | |
for i in 1..81 | |
loop | |
apex_string.push(G_MATRIX, null); | |
apex_string.push(G_ERRORS, null); | |
end loop; | |
G_IS_INITIALIZED := false; | |
G_IS_VALIDATED := false; | |
G_IS_SAVED := false; | |
end sudoku; | |
/ |
アプリケーション作成ウィザードを起動します。
アプリケーションの名前は数独とします。デフォルトで追加されているホーム・ページを削除し、代わりに対話グリッドのページを追加します。
対話グリッドの名前は数独、ソースとしてSQL問合せを選択し以下のSELECT文を記述します。編集を許可を選択します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
select id, c1, c2, c3, c4, c5, c6, c7, c8, c9, 'X' fu from | |
( | |
select * from | |
(select ceil(rownum / 9) as id, mod(rownum,9) as column_name, column_value from table(apex_string.split_numbers(:P1_MATRIX,':'))) | |
pivot | |
(max(column_value) for column_name in (1 as C1,2 as C2,3 as C3,4 as C4,5 as C5,6 as C6,7 as C7,8 as C8,0 as C9)) | |
order by id | |
) |
ページ・デザイナで対話グリッドのページを開きます。
アプリケーション作成ウィザードが対話グリッドの列IDを主キーとして認識していません。列IDを選択し、識別のタイプを非表示、ソースの主キーをオンに変更します。
列FUを選択し識別のタイプを非表示にし、設定の保護された値をオフにします。この列は変更のない対話グリッドの行をサーバーに送信するために、ページの送信前に動的アクションから値を変更するために使用します。利用者はこの列を見る必要がないため非表示にし、動的アクションからの更新を許可するために保護された値をオフにしています。
編集の実行可能な操作から行の追加と行の削除を外します。可能な操作は行の更新に限ります。
ツールバーより検索列の選択、検索フィールド、それと保存ボタンを外します。対話グリッドの保存からは、呼び出される検証やプロセスが編集可能リージョンとして(保存ボタンのある対話グリッドである)数独が紐づけられているものに限られます。また、呼び出される検証やプロセスは、変更された行毎に複数回呼び出されます。今回は対話グリッドには紐づかず、送信時に一度だけ呼び出される検証やプロセスの実行が必要なため、別にボタンを作成してページの送信を行います。
対話グリッドをJavaScriptから扱うため、詳細の静的IDとしてSUDOKUを設定します。
9x9の配列に入力した数値を保存するページ・アイテムを作成します。
識別の名前はP1_MATRIX、タイプはテキスト・フィールドです。ラベルは配列とします。
ページ・アイテムP1_MATRIXを初期化するプロセスを作成します。
レンダリング前のヘッダーの前にプロセスを作成します。識別の名前はP1_MATRIXの初期化、タイプはコードの実行とします。ソースのPL/SQLコードとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if :P1_MATRIX is null then | |
:P1_MATRIX := apex_string.join(sudoku.G_MATRIX, ':'); | |
else | |
sudoku.G_MATRIX := apex_string.split_numbers(:P1_MATRIX,':'); | |
end if; |
検証を実行するボタンを作成します。
作成したボタンはページ・アイテムP1_MATRIXの下に配置します。識別のボタン名はVALIDATE、ラベルは検証とします。外観のホットをオン、テンプレート・オプションのWidthはStretch、Spacing BottomにLargeを選択します。
動作のアクションはデフォルトのページの送信とします。
左ペインでプロセス・ビューを開き、検証を作成します。
識別の名前はG_MATRIXの初期化とします。検証の編集可能リージョンは未指定(- 選択 -のまま)にし、ページ送信時に1回だけ実行するようにします。タイプに式を選び、PL/SQL式として以下を記述します。
sudoku.initialize(:P1_MATRIX)
パッケージ変数G_MATRIXにページ・アイテムP1_MATRIXの値を設定しています。
ファンクションsudoku.initializeは検証として実行されますが、必ずtrueを返します。そのため検証に失敗することはありません。しかし、エラー・メッセージは必須であるため適当な値(今回は . ピリオド)を設定しておきます。
サーバー側の条件のボタン押下時にVALIDATEを設定します。
この設定はボタンの動作の検証の実行に対応しています。常に実行がオンのときは、ボタンの検証の実行がオフであっても、検証が実行されます。
続いて実行される検証を作成します。
識別の名前はG_MATRIXを更新とします。対話グリッドで変更された行を受信し、パッケージ変数G_MATRIXを更新します。結果として数独の配列の値が最新の状態になります。この検証も常に成功します。
検証の編集可能リージョンは数独とします。この検証は対話グリッドで変更された行の数だけ繰り返し実行されます。タイプとしてファンクション本体(ブールを返す)を選択し、PL/SQLファンクション本体として以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_idx integer; | |
begin | |
l_idx := (:ID - 1) * 9; | |
apex_debug.info('SUDOKU: Update row_id = %s', :ID); | |
sudoku.G_MATRIX(l_idx + 1) := :C1; | |
sudoku.G_MATRIX(l_idx + 2) := :C2; | |
sudoku.G_MATRIX(l_idx + 3) := :C3; | |
sudoku.G_MATRIX(l_idx + 4) := :C4; | |
sudoku.G_MATRIX(l_idx + 5) := :C5; | |
sudoku.G_MATRIX(l_idx + 6) := :C6; | |
sudoku.G_MATRIX(l_idx + 7) := :C7; | |
sudoku.G_MATRIX(l_idx + 8) := :C8; | |
sudoku.G_MATRIX(l_idx + 9) := :C9; | |
return true; | |
end; |
エラー・メッセージおよびサーバー側の条件は、先ほどの検証と同様に設定します。
続いて実行される検証を作成します。
識別の名前は数独の検証とします。検証の編集可能リージョンは未指定(- 選択 -のまま)にし、ページ送信時に1回だけ実行するようにします。タイプに式を選び、PL/SQL式として以下を記述します。
sudoku.validate_all()
数独の配列を検証します。検証結果はパッケージ変数G_ERRORSに保存し、このファンクション自体はつねにtrueを返します。
エラー・メッセージおよびサーバー側の条件は、先ほどの検証と同様に設定します。
検証結果を画面に反映させる検証を作成します。
識別の名前はC1の検証とします。検証の編集可能リージョンとして数独を設定します。この検証は対話グリッドが送信する行ごとに実行されます。タイプに式を選択し、PL/SQL式として以下を記述します。
sudoku.is_valid(1, :ID)
第1引数は列の位置、第2引数が行の位置です。その位置にある値が数独のルールに違反しているときにfalseが返されます。
この検証結果は画面に表示します。エラー・メッセージは重複しています。とします。表示位置はフィールド上でインライン表示、関連付けられた列にC1を設定します。エラー・メッセージは、その対話グリッド上のフィールドに表示されます。
サーバー側の条件のボタン押下時にVALIDATEを設定します。
検証のサーバー側の条件に実行スコープの設定があります。作成済および変更済の行またはすべての送信済の行の2通りを選ぶことができますが、リージョンが対話グリッドの場合、対話グリッド自体が作成済および変更済の行しか送信しないため、この設定に意味はありません。このような設定になっている理由については、下記のFR-2608に説明があります。
エンハンスメント・リクエストがAPEX IdeasのFR-2608 Interactive Grid: Execution Scope = "All Submitted Rows" should submit all rows, not just new/modified rows として上がっており、ステータスが将来実装予定(ROADMAP)になっています。
作成した検証を重複させ、検証C2の検証を作成します。名前、PL/SQL式、関連付けられた列に1が割り当たっている部分を2に変更します。
同様の作業を列C9までを対象として繰り返します。
最終的に列の検証が9つ、全部で12の検証が作成されます。
プロセス数独 - 対話グリッド・データの保存を選択し、対話グリッドのデータをページ・アイテムP1_MATRIXに保存するように変更します。
識別のタイプをコードの実行に変更し、編集可能リージョンを未選択(- 選択 -に戻す)にします。ソースのPL/SQLコードとして以下を記述します。
sudoku.save(:P1_MATRIX);
成功メッセージは検証できました。とします。サーバー側の条件のボタン押下時にVALIDATEを設定します。
ここで一旦、アプリケーションの動作を確認してみます。
同じ列で数値が重複している場合は、両方のフィールドともにエラーが通知されます。
数独の配列G_MATRIXは初期のデータをページ・アイテムP1_MATRIXから取り出し、対話グリッドから送信された値で更新しているため、9x9の配列のデータをすべて保持しています。しかし、検証C1の検証からC9の検証までは、対話グリッドが送信する行、つまり変更された行だけを対象に検証が呼び出されます。結果として、エラー自体は検出されていても(パッケージ変数G_ERRORSには保存されている)、変更がされていない行ではエラーが通知されません。
検証を実行するためには、変更の有無にかかわらずすべての行をサーバーに送信する必要があります。
Oracle Corporationに所属しており、対話グリッドを開発したJohn Snydersさんが、彼のブログで対話グリッドの検証について記事を書かれています。
Interactive Grid Validation
この記事の中で紹介されているAPEXアプリケーションIG Validateの、EMP Validate 6(ページ番号8)にページ送信前に変更のされていない行を見つけて非表示の列を更新する(ことにより送信対象にする)JavaScriptの実装が含まれています。
この実装を導入します。
左ペインで動的アクション・ビューを開き、動的アクションを作成します。
識別の名前はIGの全行を更新するとします。タイミングのイベントにカスタムを選択し、カスタム・イベントにapexpagesubmitを設定します。選択タイプにJavaScript式を選択し、JavaScript式としてapex.gPageContext$を設定します。
TRUEアクションとしてJavaScriptコードの実行を選択し、設定のコードとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let model = apex.region("SUDOKU").call( "getViews" ).grid.model; | |
model.forEach( (rec, index, id) => { | |
let meta = model.getRecordMetadata( id ); | |
if ( !meta.agg ) { // ignore aggregate records | |
if ( !meta.deleted && !meta.updated && !meta.inserted ) { | |
model.setValue( rec, "FU", "" ); | |
} | |
} | |
} ); |
以上でアプリケーションは完成です。今度はすべての行が送信されているため、数値が重複しているフィールドがすべてエラーになります。
対話グリッドの検証については、Matthew Mulvaneyさんによる以下のブログ記事も参考になります。
Interactive Grid duplicate values – Learn How to prevent a common problem using a Zero-JavaScript approach
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sudoku-validate.zip
対話グリッドのみで実装した版のエクスポートは以下です。
https://github.com/ujnak/apexapps/blob/master/exports/sudoku-validate2.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完