ラベル Solution の投稿を表示しています。 すべての投稿を表示
ラベル Solution の投稿を表示しています。 すべての投稿を表示

2021年2月2日火曜日

ブロックチェーン表の行への署名をブラウザにて実施する

ブロックチェーン表への挿入操作をブラウザから実施し、その操作によって挿入した行の署名をブラウザのWeb Cryptography APIを使って行う方法を試してみました。その作業記録です。

以下、作業手順です。

ブロックチェーン表BC_BROWSER_TRANSACTIONSを、以下のDDLで作成しました。

create blockchain table BC_BROWSER_TRANSACTIONS
(
    browser_transaction_id number generated by default on null as identity,
    transaction_name varchar2(80),
    transaction_volume number,
    register_date date
)
no drop until 31 days idle no delete locked HASHING USING "SHA2_512" version "v1";

鍵の生成には以下のコマンドを使っています。

% openssl genrsa -out private.pem 2048

Generating RSA private key, 2048 bit long modulus

..........................+++

..............................................+++

e is 65537 (0x10001)

%

証明書の元になるCSRを生成します。

% openssl req -new -days 365 -key private.pem -out cert.csr

You are about to be asked to enter information that will be incorporated

into your certificate request.

What you are about to enter is what is called a Distinguished Name or a DN.

There are quite a few fields but you can leave some blank

For some fields there will be a default value,

If you enter '.', the field will be left blank.

-----

Country Name (2 letter code) []:JP

State or Province Name (full name) []:Tokyo

Locality Name (eg, city) []:Minato-ku

Organization Name (eg, company) []:*******

Organizational Unit Name (eg, section) []:******

Common Name (eg, fully qualified host name) []:****.****.*****

Email Address []:****.*****@******.com


Please enter the following 'extra' attributes

to be sent with your certificate request

A challenge password []:

%

データベースが署名の検証に使用する証明書を作ります。

% openssl x509 -in cert.csr -out cert.crt -req -signkey private.pem -days 365

Signature ok

subject=/C=JP/ST=Tokyo/L=Minato-ku/O=Oracle/OU=Cloud/CN=apex.oracle.com/emailAddress=yuji.nakakoshi@oracle.com

Getting Private key

% 

Web Cryptography APIで扱えるように、秘密鍵をPKCS8形式に変換します。

% openssl pkcs8 -topk8 -nocrypt -in private.pem -outform DER -out private.der

% 

バイナリを16進数表現に変換します。

% xxd -ps private.der > private.hex 

%

変換された16進数表現を改行なしの1行にします。

% while read l

while> do

while> echo -n $l

while> done < private.hex > private.l

%

以下のPL/SQLスクリプトをOracle APEXのSQLワークショップに含まれるSQLコマンドより実行し、作成した証明書をデータベースに登録します。httpsにてアクセスできる場所に証明書を配置します。

declare
l_cert blob;
l_cert_guid raw(16);
begin
l_cert := apex_web_service.make_rest_request_b(
p_url => 'https://どこかにファイルを配置します/cert.crt',
p_http_method => 'GET'
);
dbms_user_certs.add_certificate(l_cert, l_cert_guid);
dbms_output.put_line('Certificate GUID = ' || l_cert_guid);
end;

ハッシュを取り出すファンクションBC_GET_HASHを作成します。

create or replace function bc_get_hash
(p_browser_transaction_id in number) 
return varchar2
as  
  l_inst_id binary_integer; 
  l_chain_id binary_integer; 
  l_sequence_no binary_integer; 
  l_row_data blob; 
  l_raw raw(2000); 
begin 
  select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$ 
  into l_inst_id, l_chain_id, l_sequence_no 
  from bc_browser_transactions where browser_transaction_id = p_browser_transaction_id;
  --
  dbms_blockchain_table.get_bytes_for_row_signature( 
      'APEXDEV', 
      'BC_BROWSER_TRANSACTIONS', 
      l_inst_id, 
      l_chain_id, 
      l_sequence_no, 
      1, 
      l_row_data 
  ); 
  l_raw := dbms_lob.substr(l_row_data, dbms_lob.getlength(l_row_data));
  return rawtohex(l_raw);
end bc_get_hash;

次に、外部で生成された署名を引数として、それをブロックチェーン表に加えるプロシージャBC_SIGN_ROWを作成します。

create or replace procedure bc_sign_row 
(p_browser_transaction_id in number,
 p_signature in varchar2
) 
as  
  l_inst_id binary_integer; 
  l_chain_id binary_integer; 
  l_sequence_no binary_integer; 
  sign_raw raw(2000); 
  cert_guid raw(16) := hextoraw('証明書のGUID'); 
begin 
  select orabctab_inst_id$, orabctab_chain_id$, orabctab_seq_num$ 
  into l_inst_id, l_chain_id, l_sequence_no 
  from bc_browser_transactions where browser_transaction_id = p_browser_transaction_id; 
  sign_raw := hextoraw(p_signature);
  dbms_blockchain_table.sign_row( 
      'APEXDEV', 
      'BC_BROWSER_TRANSACTIONS', 
      l_inst_id, 
      l_chain_id, 
      l_sequence_no, 
      NULL, 
      sign_raw, 
      cert_guid, 
      DBMS_BLOCKCHAIN_TABLE.SIGN_ALGO_RSA_SHA2_512 
   ); 
end bc_sign_row;

ブラウザで署名をつけるために使用するコードは以下です。

// 16進数表現からバイナリに変換する
// 参照先: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript
const fromHexString = hexString =>
  new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// バイナリから16進数表現に変換する
// 参照先: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
function buf2hex(buffer) { // buffer is an ArrayBuffer
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

// 16進数表現のhashHexよりバイナリのhashDataを生成する
var hashHex = '95E144F09593033ABFC70AA78922ECDC6108A5E005BE30C10228E357414992216E8DCEF7A3A5163614FD553239DCA7EC57B8E28A8644EEC8AE19EFE40A1F5F6D';
var hashData = fromHexString(hashHex);
// 16進数表現のprvHexよりバイナリのprvDataを生成する
var prvHex = '16進数表記の秘密鍵のデータ';
var prvData = fromHexString(prvHex);

// 署名をつける
// 参照先: http://blog.livedoor.jp/k_urushima/archives/1759093.html
window.crypto.subtle.importKey(
    "pkcs8", prvData, 
    { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-512"} },
    true, ["sign"]
).then(
    function(prvKey) {
      return window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", prvKey, hashData);
    }
).then(
    function(sigVal) {
      console.log(buf2hex(sigVal));
    }
);

Oracle APEXのアプリケーションに組み込む前に、動作の確認を行います。

最初にブロックチェーン表BC_BROWSER_TRANSACTIONS表に一行挿入し、主キーであるBROWSER_TRANSACTION_IDを取得します。

declare
l_id number;
begin
insert into bc_browser_transactions(transaction_name, transaction_volume, register_date) values('Test Tx', 4000, sysdate)
returning browser_transaction_id into l_id;
dbms_output.put_line('BROWSER_TRANSCTION_ID = ' || l_id);
end;

以下のように表示されます。

BROWSER_TRANSCTION_ID = 2

次に、ファンクションBC_GET_HASHを呼び出して、行のハッシュ値を取得します。

begin
dbms_output.put_line(bc_get_hash(2));
end;

以下のようにハッシュ値が返されます。

82FE46794AE7052DAB42886E57C7E2C557CF98234989B6067330A1EC1E8D963CE0A4665534181756EB3D618BD7FE1253F64968D3DFF13F0E7AF9C463F5480D4E

このハッシュ値をhashHex、秘密鍵であるファイルprivate.lprvHexに与えて、ブラウザの開発者ツールで、先ほどのJavaScriptを実行します。(httpsのページが開いている状態で開発ツールを動かす必要があります)。

コンソールに出力された署名をコピーします。


プロシージャBC_SIGN_ROWを、BROWSER_TRANSACTION_IDの数値とコピーした署名を引数にして呼び出します。

begin
bc_sign_row(2,
'981e9759d8 --- 電子署名の16進数表現 --- 5b2a38893989b2296cf'
);
end;

以下が表示されます。
文が処理されました。
付加された署名の検証を行います。

declare
l_num number;
begin
dbms_blockchain_table.verify_rows(
schema_name => 'APEXDEV',
table_name => 'BC_BROWSER_TRANSACTIONS',
number_of_rows_verified => l_num
);
dbms_output.put_line('Number of rows verified = ' || l_num);
end;

エラーが発生せず、以下のような出力が得られれば、設定の完了です。
Number of rows verified = 2
ビルディング・ブロックの準備ができたので、これからOracle APEXのアプリケーションを作成します。

まず、空のアプリケーションを作成します。名前Browser Transactionとしました。この作業については、スクリーンショットの添付は省きます。

続いて、ブロックチェーン表に1行書き込むフォームのページを作成します。アプリケーション・ビルダーより、ページの作成を実行します。


ページ・タイプコンポーネントフォームをクリックします。


フォーム付きレポートをクリックします。


ページ属性の設定を行います。レポート・タイプ対話モード・レポートフォーム・ページ・モード標準にします。その他の設定項目は任意です。に進みます。


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


データ・ソース表/ビューの名前として、表BC_BROWSER_TRANSACTIONSを選択します。すべての列をレポートの表示対象のままにし、に進みます。


フォームの主キー列として、BROWSER_TRANSACTION_ID(Number)を選択します。作成を実行します。


これでレポートとフォームのページが作成されました。作成したアプリケーションを実行して、動作を確認してみます。最初にレポート・ページを開きます。

挿入済みの行を確認した後、作成をクリックします。


フォームが開くので、それぞれフォームに値を指定し、作成を実行します。


行が挿入されたことを確認します。


これで、通常の表への行の挿入については問題なく実装されていることが確認できました。これから、1行挿入時にブラウザ側で署名をつけるようにします。

フォーム・リージョンを含むページ(ページ番号は3であることを前提とします)をページ・デザイナで開き、ページ・アイテムP3_HASHを追加します。タイプ非表示にします。このページ・アイテムは動的アクションによって値を設定するため、保護された値OFFにします。


同様に、ページ・アイテムP3_SIGNATUREを追加します。タイプ非表示です。また、保護された値OFFにします。


主キーの項目であるP3_BROWSER_TRANSACTION_IDについても、動的アクションにて値を設定するため、保護された値OFFにします。


ボタンCREATE動作アクションを、ページの送信から動的アクションで定義に変更します。


動的アクションの名前作成のクリックとします。発火のタイミングについては、デフォルトでCREATEボタンクリックされたときになります。


最初にサーバー側のコードを実行し、表BC_BROWSER_TRANSACTIONSに行を挿入します。挿入した結果として取得されるBROWSER_TRANSACTION_IDおよび行のハッシュ値をフォームに返します。結果として、ページ・アイテムP3_BROWSER_TRANSACTION_IDP3_HASHに値が設定されます。

PL/SQLコードは以下になります。
declare
    l_id number;
begin
    begin
        insert into bc_browser_transactions
        (
            transaction_name, 
            transaction_volume,
            register_date
        )
        values
        (
            :P3_TRANSACTION_NAME,
            :P3_TRANSACTION_VOLUME,
            :P3_REGISTER_DATE
        )
        returning browser_transaction_id into l_id;
        commit;
    end;
    :P3_HASH := bc_get_hash(l_id);
    :P3_BROWSER_TRANSACTION_ID := l_id;
end;
送信するアイテムは、P3_TRANSACTION_NAME, P3_TRANSACTION_VOLUME, P3_REGISTER_DATEです。

戻すアイテムP3_BROWSER_TRANSACTION_ID, P3_HASHです。

動作に影響はないはずですが、変更イベントの禁止ONとしておきます。


すでに動作を検証したJavaScriptのコードがあります。一部をページのプロパティのファンクションおよびグローバル変数の宣言に記述します。
// 16進数表現からバイナリに変換する
// 参照先: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript
const fromHexString = hexString =>
  new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// バイナリから16進数表現に変換する
// 参照先: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
function buf2hex(buffer) { // buffer is an ArrayBuffer
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
// 16進数表現のprvHexよりバイナリのprvDataを生成する
var prvHex = '16進数表記の秘密鍵のデータ';
var prvData = fromHexString(prvHex);

Oracle APEXのアプリケーションはサーバー側にコードが保存されるので、このコードではサーバー側に秘密鍵の情報が保存されてしまいます。しかし、JavaScript自体の実行は手元のブラウザで行われるので、このprvHexのデータをローカルから取得するように書き換えることで、ユーザーそれぞれの秘密鍵を使用した署名ができるでしょう。秘密鍵の持ち方については、検討が必要です。

サーバー側でのPL/SQLコードの実行に続いて、JavaScriptコードの実行を行います。実行するJavaScriptのコードは以下になります。
// 署名をつける
// 参照先: http://blog.livedoor.jp/k_urushima/archives/1759093.html
window.crypto.subtle.importKey(
    "pkcs8", prvData, 
    { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-512"} },
    true, ["sign"]
).then(
    function(prvKey) {
      return window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", prvKey, fromHexString($v('P3_HASH')));
    }
).then(
    function(sigVal) {
        apex.page.submit( {
            request: "CREATE",
            set: {
                "P3_BROWSER_TRANSACTION_ID": $v("P3_BROWSER_TRANSACTION_ID"),
                "P3_SIGNATURE": buf2hex(sigVal)
            },
            showWait: false
        })
    }
);


Web Cryptography APIを使用し、秘密鍵をインポートして、ブラウザ側でブロックチェーン 表から取得したハッシュ値(P3_HASH)に署名を行います。生成した署名(P3_SIGNATURE)と主キーであるP3_BROWSER_TRANSACTION_IDを引数として、apex.page.submitを呼び出し、サーバーに送信します。

ページのプロセスによって、生成した署名をブロックチェーン表に登録します。ボタンCREATEを押したときに起動されるプロセスですが、すでに表BC_BROWSER_TRANSACTIONSへの行の挿入は完了しており、単に署名だけをプロセスで追加します。

フォームを作成した時に作成されるタイプフォーム - 行の自動処理(DML)となっているプロセスがあります。名前はページ名に依存しますが、プロセス・フォームで始まり、ページ名が続きます。

こちらのプロセスの設定ターゲット・タイプPL/SQL Codeに変更します。そして、挿入/更新/削除するPL/SQLコードとして、以下を指定します。
begin
  bc_sign_row(:P3_BROWSER_TRANSACTION_ID, :P3_SIGNATURE);
end;
失われた更新の防止、および、行のロックは両方とも不要なので、OFFおよびNoにします。サーバー側の条件として、ボタン押下時CREATEを選びます。


フォームについてはこれで完成です。最後にレポート上で署名の有無を確認するため、列ORABCTAB_SIGNATURE_CERT$を検索される列に追加します。レポートのページをページ・デザイナで開きます。

ソースのタイプをSQL問合せに変更し、以下のSQLを設定します。
select BROWSER_TRANSACTION_ID,
       TRANSACTION_NAME,
       TRANSACTION_VOLUME,
       REGISTER_DATE,
       ORABCTAB_SIGNATURE_CERT$
from BC_BROWSER_TRANSACTIONS

以上で今回確認したかった、Web Cryptography APIを使用したブロックチェーン表への電子署名の生成については完了です。

このまま実用できる、というものではありませんが、ブラウザ側で電子署名をつけることができる、というのは面白いと思います。Oracle APEXによるアプリケーション作成の一助になれば幸いです。

謝辞

JavaScriptのコードについては、ほとんどWebから見つけたものを使っています。参考にさせていただいたサイトについては、コードの内のコメントとしてURLを記載しています。本当にありがとうございました。

2021年1月9日土曜日

jsQRを組み込んでOracle APEXでQRコードを読み取る

 気になっていたので、扱いやすそうに見えたjsQRというJavaScriptライブラリを、Oracle APEXのアプリケーションに組み込んでみました。カメラがQRコードを認識したときに、ページ・アイテム(ここではQRコードからのデータ)に、読み取ったデータを設定しています。


こちらにjsQRが提供しているデモのページがあります。このデモのページのソースを、ほとんどコピペしてOracle APEXのアプリに組み込みました。

デモのコードの中に一行だけ、以下のコードをQRコードが認識されたときに実行される部分に追加しています。P1_QRCODEはページ・アイテムの名前です。

$s('P1_QRCODE', code.data);

この後にJavaScriptのコードを追加することで、QRコードを認識した時点で次の処理に遷移する、といったことも可能でしょう。

作成したアプリケーションはこちらからアクセスできます。

上記のアプリケーションのエクスポートをダウンロードできるよう、こちらにおきました。

テストはAppleのMacbook ProのSafariで確認しています。

Chrome, Edgeでは動かず、iPad, iPhoneでは動画としての取り込みをしてくれない(1秒くらいでビデオが止まる)ので、おそらくビデオ映像を取得するために呼び出しているnavigator.mediaDevices.getUserMediaにもっと詳しいオプションが必要だったり、デバイスを検知してオプションを切り替える、といった処理が必要なのではないか?と予想しています。

今回は動かすだけが目的だったので、これで完了です。実際のコードは、アプリケーションのエクスポートをインストールすると参照できます。

2021年1月5日火曜日

Oracle APEXでWebコンポーネントを扱う

 Oracle IndiaのSrihari RavvaさんがWeb Components in Oracle APEXとして、WebコンポーネントをOracle APEXで扱う方法を紹介しています。面白いトピックなので、自分でも試してみました。

環境に依存せずに作業を試行できるよう、いくつか手順を変えています。特にJavaScriptから呼び出すRESTful APIを独自のものから、Oracle APEXのRESTful APIのサンプルとして提供されているoracle.example.hrを使うようにしています。

確認作業は以下の順番で行います。

  1. Oracle APEX Builder Extension by FOSを導入する。
  2. RESTfulサービスのサンプルをインストール(またはリセット)する。
  3. 空のアプリケーションを作成する。
  4. 静的アプリケーション・ファイルに、WebコンポーネントとなるJavaScriptのコードを記述する。
  5. 静的リージョンを作成し、動作を確認する。
  6. 動的アクションを作成し、動作を確認する。
1、2、3が準備作業、4、5、6が元記事に記載のあるWebコンポーネントにまつわる作業です。

Oracle APEX Builder Extension by FOSを導入する



FOEX GmbHが開発した、Oracle APEXの静的アプリケーション・ファイル、静的ワークスペース・ファイルやプラグインのファイルを、Monaco Editorで直接編集可能にするブラウザの拡張機能です。

こちらよりChromeまたはFirefoxのウェブストアへ移動し、拡張機能を導入します。

この拡張機能を導入しなくても、手元で編集したファイルを静的アプリケーション・ファイルとしてアップロードすることもできます。

RESTfulサービスのサンプルをインストール(またはリセット)する


SQLワークショップよりRESTfulサービスを開きます。


RESTfulサービスは、Oracle REST Data Servicesによって実装されています。Oracle APEXでは、サービスを作成するためのユーザー・インターフェースのみ提供しています。Oracle REST Data Servicesによって、Oracle APEXのワークスペースに紐づいているスキーマが扱えるよう、ORDSにスキーマを登録を実行します。

今回はサンプル・サービスをそのまま利用するので、サンプル・サービスのインストールONにし、スキーマ属性の保存を実行します。


以上でサンプル・サービスが利用可能になっています。すでにRESTfulサービスが有効になっている場合は、サンプル・サービスのリセットを実施します。


警告が表示されます。oracle.example.hrモジュールおよび表EMPとDEPTが削除され、再作成されることに注意してください。困る場合は、検証の際に使用する従業員の情報を調整することにして、リセットせずに作業を進めましょう。リセットすることに問題がなければ、サンプル・サービスのリセットを実行します。


ORDSハンドラ定義を開いて、完全なURLを確認します。今回使用するREST APIは、モジュールoracle.example.hrURIテンプレートemployees/:idメソッドGETです。


Webコンポーネントの中から、この完全なURLに従業員番号(EMPNO)を引数として与えて呼び出します。スクリーンショットに表示されている完全なURLは、それぞれの環境で異なります。

空のアプリケーションを作成する


今回の検証を実装するアプリケーションを作成します。アプリケーション・ビルダーより作成を実行します。

新規アプリケーションをクリックします。

アプリケーションの名前は任意ですが、今回はWebコンポーネント検証としています。その他は何もせず、アプリケーションの作成を実行します。

以上でアプリケーションの準備も完了しました。

JavaScriptのコードを記述する


静的アプリケーション・ファイルに、WebコンポーネントとなるJavaScriptのコードを記述します。共有コンポーネント静的アプリケーション・ファイルを開きます。


Oracle APEX Builder Extension by FOSがインストールされているとEdit Filesの領域が表示されます。Create Fileをクリックします。


File Nameとしてemployee-info.jsを指定し、Createを実行します。


以下のコードを貼り付け、Saveを実行します。
// JavaScriptコード
// temlate要素を生成する。
const template = document.createElement('template');
// 従業員情報を表示するテンプレートとなるHTMLとCSSを記述する。
// Shadow DOMにになるので、ここで定義されたCSSはカスタム要素のemployee-data内でのみ有効。
// ページレベルで定義されているCSSはカスタム要素には適用されない。
template.innerHTML = `
  <style>
  div.employee-data {
    background: #F5F4F2;
    width: 400px;
    margin-bottom: 10px;
    border-bottom: purple 8px solid;
    padding: 4px;
  }
  div.employee-data h2{
    color: purple;
  }
  </style>
  <div class="employee-data">
    <h2></h2>
    <div class="empno">EMPNO: <span></span> </div>
    <div class="job">JOB: <span></span> </div>
  </div>
`;
// カスタム要素のためのクラスを定義する。
// 一般的にはカスタム要素の名前が custom-element であれば、 CustomElement をクラス名とする。
// 今回は employee-info がカスタム要素なので、クラス名は EmployeeInfo となる。
class EmployeeInfo extends HTMLElement {
  constructor() {
    // かならず super をコンストラクタの先頭で呼び出す。HTMLElementとして正しく初期化される。
    super();
    // Shadow DOMをアタッチする。これによりカスタム要素が独立する。
    this.attachShadow({ mode: 'open' });
    // テンプレートをクローンし、ShadowRootに追加する。
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    // RESTful APIを呼び出し従業員データを取得する。
    // 従業員番号(empno)の属性を引数として、RESTful APIを呼び出す。
    // RESTful APIの応答をテンプレートの穴埋めに使用する。
    fetch('https://apex.oracle.com/pls/apex/your_workspace/hr/employees/' + this.getAttribute('empno')).then(response => response.json()).then(data => {
      this.shadowRoot.querySelector('h2').innerText = data.ename;
      this.shadowRoot.querySelector('div.empno > span').innerText = data.empno;
      this.shadowRoot.querySelector('div.job > span').innerText = data.job;
    });
  }
}
// カスタム要素 employee-info と クラス EmployeeInfo を関連づける。
window.customElements.define('employee-info', EmployeeInfo);

employee-info.jsを保存すると、employee-info.js.mapおよびemployee-info.min.jsも作成されます。

処理内容については、コードのコメントを参照してください。

静的リージョンで動作確認をする

ページ・デザイナホーム・ページ(Page 1)を開きます。HTMLヘッダーとして以下を設定し、ホーム・ページのロード時に、先ほど静的アプリケーション・ファイルとして作成したJavaScriptのファイルを読み込みます。

<script type="module" src="#APP_IMAGES#employee-info.min.js"></script>

静的アプリケーション・ファイルを標準のJavaScriptのファイルURLではなく、HTMLヘッダーのscriptタグで読み込んでいます。これは、スクリプトをmoduleとして認識させ、グローバルから参照できないようにするためです。

次にリージョンを作成し、名前Web Componentsタイプ静的コンテンツとします。ソーステキストには以下を設定します。

<employee-info empno="7839"></employee-info>

リージョンの設定ができたら、ページの保存と実行を行い、動作を確認します。

以下のように、従業員KINGが表示されたら、手順通り実装できています。


動的アクションを使って動作確認をする


Webコンポーネントの表示に動的アクションを使います。再度、新規にリージョンを作成します。名前Web Components - Dynamic Actionタイプ静的コンテンツです。ソーステキストには以下を設定します。

<div id="employee_data_container"></div>

表示する従業員を指定するため、ページ・アイテムの作成を実行します。名前P1_EMPLOYEESタイプチェック。ボックス・グループラベルEmployeesとします。LOVタイプ静的値とします。


LOV静的値として、以下のペアを設定します。

表示値

戻り値

BLAKE

7698

CLARK

7782

JONES

7566

SCOTT

7788


サンプルのRESTfulサービスが初期状態でない場合は、表示値としている従業員が存在しない場合もあります。存在する従業員を探してLOVの値を登録してください。

最後に動的アクションを登録します。左ペインから動的アクション・ビューを開き、動的アクションを作成します。

名前Display Employee Dataとし、タイミングとして、イベント変更選択タイプjQueryセレクタjQueryセレクタとしてinput[name=P1_EMPLOYEES]を指定します。


実行されるアクションとして、JavaScriptコードの実行を指定し、設定コードに以下を設定します。

// チェック・ボックスより従業員番号を取得する
let empNo = $(this.triggeringElement).val();
// 従業員番号に対応するemployee-infoのタグを探して取得する。
let employeeNode = $("employee-info[empno=" + empNo + "]");
if ($(this.triggeringElement).is(':checked')) {
    // 要素が既に存在するか確認する。存在する場合は表示する。
    if (employeeNode.length == 1)
        employeeNode.show();
    else
        // そうでない場合は、要素を追加する。
        $('#employee_data_container').append('<employee-info empno="' + empNo + '"></employee-info>');
}
else {
    // チェックが外れたときは非表示にする。
    employeeNode.hide();
}

動的アクションの設定が完了したら、ページの保存と実行を行います。この記事の先頭にあるGIF動画の動作を確認できるはずです。

以上でOracle APEXでのWebコンポーネントの実装についての記事は終了です。

実際にはより複雑な実装になるかと思いますが、元記事を読んで、基本的な実装手順について参考になる内容だと思いました。また、Oracle APEXのプラグインとして実装することで、サーバー側の条件や認可スキームなども活用できるようになり、より便利なものになるでしょう。

2020年4月2日木曜日

Oracle Machine Learningの機能をOracle APEXで使用する

Autonomous Database for APEX Developersのプログラムに含まれているLab 300: Adding Machine Learning to your APEX applicationを試してみました。それで、どのような作業になったかを紹介します。Always Freeとして提供されているAutonomous Databaseを使用して演習を進めます。

準備


以下の3つの作業が完了していれば、準備は完了です。
  1. Oracle Cloudのアカウント取得 - 無料Oracle Cloudプロモーションへのサインアップ
  2. (Always Freeの)Autonomous Databaseのインスタンス作成 - こちらの記事の前半部分
  3. Oracle APEXのワークスペース作成 - 同じ記事の後半部分
作業自体はOracle Spatialの機能をOracle APEXで使用すると同じなので、すでにこの演習を行っていれば準備はできています。

Oracle Machine Learningの機能をOracle APEXのアプリケーションで使用する


この演習では、あらかじめ提供されているOracle APEXのアプリケーションをインポートし、そのアプリケーションにOracle Machine Learningが提供する機能を使った処理を追加します。

作成するアプリケーションの要件は以下になります。
  1. あるゲーム・コンソールのメーカーが販売している"Y Box Games"という製品があります
  2. このメーカーは(この製品に限らない)顧客リストと、その顧客のプロファイルを持っています
  3. 顧客ごとの"Y Box Games"を含む各種製品の販売実績を持っています
  4. まだ"Y Box Games"を購入していない顧客の内で、顧客ごとに、この製品の購買意欲がどの程度になるかを導き出します - Oracle Machine Learning!
  5. 販売員に、顧客が"Y Box Games"に、どの程度の購買意欲を持ちそうか伝えます - Oracle APEX!

機械学習に使用するデータは、Oracleが提供しているSHスキーマのサンプルに含まれるSUPPLEMENTARY_DEMOGRAPHICS表のデータになります。この表は以下の属性を持ちます。
  • CUST_ID: 顧客番号
  • EDUCATION: 最終学歴
  • OCCUPATION: 職業
  • HOUSEHOLD_SIZE: 世帯人数
  • YRS_RESIDENCE: 居住年数
  • AFFINITY_CARD: 優待カードあり
  • BULK_PACK_DISCKETS: ディスク・パック - バルク品購入実績あり
  • FLAT_PANEL_MONITOR: フラット・パネル・モニタ購入実績あり
  • HOME_THEATER_PACKAGE: ホーム・シアター機材セット購入実績あり
  • BOOKKEEPING_APPLICATION: 家計簿アプリケーション購入実績あり
  • PRINTER_SUPPLIES: プリンタ用品購入実績あり
  • Y_BOX_GAMES: Y Box Games購入実績あり
  • OS_DOC_SET_KANJI: OS日本語ドキュメンテーションセット購入実績あり
  • COMMENTS: コメント
この中のY_BOX_GAMESがターゲット属性です。ターゲット属性とその他の属性の関連を基に生成したモデルを、まだY Box Gamesを購入していない顧客に適用し、どの程度興味を持ちそうかを導出します。

アプリケーションをインポートする


機能追加を行う元となるアプリケーションをインポートします。このアプリケーションは、メーカーのコール・センターにて、電話を受け取った販売やサポートのスタッフが顧客の情報を確認するために使用することを想定しています。

GitHubのこちらから、f100.sqlをダウンロードします。GitHubの画面上でRawをクリックします。

ブラウザにファイルの内容だけが表示されます。

メニューの例はMac上のChromeですが、だいたいどのブラウザでもファイル・メニューにページを別名で保存...または別名でページを保存...といった機能はあるので、それを使ってファイルに保存します。

Oracle APEXのワークスペースにログインし、アプリケーション・ビルダーを開いて、いまダウンロードしたファイルをアプリケーションとしてインポートします。

インポートのファイルのインポートとして、ダウンロードしたf100.sql(またはf100.sql.txt)を指定します。ファイル・タイプはデフォルトで指定される、データベース・アプリケーション、ページまたはコンポーネントのエクスポートのままにし、へ進みます。

これ以降のインポート・ウィザードからの質問はすべてデフォルトを選択して進みます。最終的に以下の緑のチェックの表示を含む画面になればアプリケーションのインポートは正常終了しています。ファイルの編集を除いて、Oracle Spatialの機能をOracle APEXで使用するAPEXアプリケーションをインポートするとほぼ同じですので、より細かいステップが必要であればそちらも参考にしてください。

作成されたアプリケーションを確認する


インポートしたアプリケーションを実行します。ワークスペースのログイン時に与えたユーザー名、パスワードでログインすると以下の対話レポートが表示されます。

顧客の名前、性別、生年、婚姻歴、居住地、電話番号、メール・アドレスが一覧されています。この対話レポートの表示に、以下のRecommendationの項目を追加することが、この演習の作業になります。

Oracle Machine Learningユーザーを作成する


機械学習のモデルの作成には、Autonomous Databaseに付属しているZeppelinを使用します。ZeppelinはWebベースのノートブック環境で、データの分析や操作を行うことができます。少々、ノートブックって?と思って調べると、Kunuth先生の提唱する文芸的プログラミングを行うために使用するもの、とのこと。コードをドキュメントに埋め込み、文章として読めて、かつ、コンピュータで実行可能とする、といった環境です。Autonomous Databaseに組み込まれているZeppelinベースのOracle Machine Learningノートブックに記載するコードは、SQLかPL/SQLになります。

Oracle Machine Learningユーザー(以後、MLユーザーとします)を作成するには、まず、Autonomous Databaseのサービス・コンソールを開きます。

サービス・コンソールの中の管理を選択し、Oracle MLユーザーの管理を開きます。

デフォルトでシステム管理者としてADMINユーザーが登録されていますが、これは使わず、新規にMLユーザーmluser1を作成します。作成をクリックします。

ユーザー名電子メール・アドレスを指定し、ユーザーにパスワードと電子メール・アカウントの詳細を生成してください。云々、と記載されているチェックボックスのチェックを外します。チェックを入れるとパスワードが自動再生され、設定した電子メール・アドレスに通知されます。ユーザーは初回ログイン時にパスワードを変更する必要があります。今回はチェックを外して、パスワードは自分で設定します。

(別のタブで開いている)サービス・コンソールに戻り、開発からOracle Machine Learningノートブックを開きます。

ノートブックへのサイン・イン画面が開くので、先ほど作成したユーザー名と、そのユーザーのパスワードを入力してサインインをクリックします。

ノートブックを作成する


サインインするとOracle Machine Learningノートブックのホーム画面が開きます。これからの作業を行うために使用するノートブックを作成するため、ノートブックをクリックします。

作成済みのノートブック一覧が表示されます。新規のノートブックを作成するために、作成をクリックします。

名前Predict Y Box Games接続Globalとし、OKをクリックします。

起動中の画面になります。

起動が完了すると、ノートブックの作成完了です。このノートブックには、SQL、PL/SQLの記述と実行、コメントや説明の記述を行うことができます。

機械学習モデルを構築する


Y Box Gamesをすでに購入済みの顧客の情報から、モデルを機械学習モデルを作成します。

日本語マニュアルはこちらになります。

作成するモデルの入力となるデータを、以下のSQL文を実行することで確認します。
SELECT * FROM SH.SUPPLEMENTARY_DEMOGRAPHICS
上記SQLをパラグラフの入力とし、実行します。

それぞれの列についてはすでに説明済みです。検索結果のデータを眺めて、果たして、どういったプロファイルを持つ人がY Box Gamesを購入するのか、またはしたのか(Y_BOX_GAMESの値が1)、見つけることができるか考えてみましょう。

簡単には見つけられない、または、ほとんど見つけられる気がしないのではないでしょうか。

機械学習を適用することで、ターゲットなる属性Y_BOX_GAMESとその他の属性にある関係を見つけることができます。

次の処理を記述するため、新しくパラグラフを追加します。すでにパラグラフが追加ずみであれば、それをそのまま使用します。

入力データを2つのセットに分割します。60%をトレーニング用、40%をテスト用とします。最初に60%のデータを含む表N1_TRAIN_DATAを作成します。
CREATE TABLE N1_TRAIN_DATA AS SELECT * FROM SH.SUPPLEMENTARY_DEMOGRAPHICS SAMPLE (60) SEED (1);

次に、残りのデータを含む表N1_TEST_DATAを作成します。
CREATE TABLE N1_TEST_DATA AS SELECT * FROM SH.SUPPLEMENTARY_DEMOGRAPHICS MINUS SELECT * FROM N1_TRAIN_DATA;

それぞれの表の行数を確認します。

select 'TRAIN', count(*) from n1_train_data
union
select 'TEST', count(*) from n1_test_data

機械学習(今回は決定木による分類)のモデルは、ターゲット属性(Y_BOX_GAMES)と予測子である他の属性との関係を保持します。これらの関係はモデルの作成(トレーニング)プロセスにて見つけられます。モデルを作成するには、いくつかのパラメータの指定が必要です。最初に表を作成して、これらのパラメータの指定を保存します。このパラメータを保持する表の名前は任意で指定できます。今回の演習では、使用するアルゴリズム、つまり決定木(ディシジョン・ツリー)のみを指定します。

パラメータを保存する表N1_BUILD_SETTINGSを作成します。
CREATE TABLE N1_BUILD_SETTINGS(SETTING_NAME VARCHAR2(30), SETTING_VALUE VARCHAR2(4000));

次に、アルゴリズム(ALGO_NAME)として、決定木(ALGO_DECISION_TREE)を指定します。
INSERT INTO N1_BUILD_SETTINGS(SETTING_NAME, SETTING_VALUE) VALUES('ALGO_NAME','ALGO_DECISION_TREE');

準備ができたので、モデルの作成とトレーニングを行います。以下のPL/SQLコードを実行します。最初の'%script'は、PL/SQLコードとしての実行をノートブックに指示しています(デフォルトは'%sql' - SQL文の実行です。これ以外にも'%md' - マークダウンの記述、といったものもあります)。
%script
CALL DBMS_DATA_MINING.CREATE_MODEL('N1_CLASS_MODEL', 'CLASSIFICATION', 
   'N1_TRAIN_DATA', 'CUST_ID', 'Y_BOX_GAMES', 'N1_BUILD_SETTINGS');

DBMS_DATA_MININGパッケージ、および、CREATE_MODELプロシージャーの説明はマニュアルに詳しいです。今回の指定は以下の意味になります。
  1. N1_CLASS_MODEL: 作成されるモデルにつける名前です。データベースには特別な型のオブジェクトとして保存されます。
  2. CLASSFICATION: マイニング機能を指定します。今回は分類(CLASSIFICATION)を指定しています。その他に相関(ASSOCIATION)、属性評価(ATTRIBUTE_IMPORTANCE)、クラスタリング(CLUSTERING)、特徴抽出(FEATURE_EXTRACTION)、回帰(REGRESSION)、時系列(TIME_SERIES)があります。
  3. N1_TRAIN_DATA: トレーニングに使用するデータを保持している表の名前です。
  4. CUST_ID: トレーニング・データを一意で識別できる列名です。
  5. Y_BOX_GAMES: ターゲット属性の名前です。
  6. N1_BUILD_SETTINGS: モデルの作成設定が含まれる表の名前です。
これでモデルが作成できました。

結果とモデルの精度を検証する


今回作成したモデルがどの程度の正確度で、Y Box Gamesの所有者を予測できるのかを求めてみます。この用途のために、先ほど作成したテスト用の(トレーニングに使用しなかった)データを保持している表N1_TEST_DATAを使用します。この表に含まれているデータは現実の顧客情報ですので、Y_BOX_GAMES列には予測ではない所有情報が含まれています。表N1_TEST_DATAに含まれるデータを用いてターゲット属性であるY Box Games列の結果を予測し、それと現実の情報を比較します。

予測した結果を保持する列Y_BOX_GAMES_PREDを表N1_TEST_DATAに追加します。
ALTER TABLE N1_TEST_DATA ADD Y_BOX_GAMES_PRED NUMBER(1);

次に予測を行い、結果をY_BOX_GAMES_PREDに保存します。
UPDATE N1_TEST_DATA SET Y_BOX_GAMES_PRED = PREDICTION(N1_CLASS_MODEL USING *);

SQLスコアリング関数PREDICTIONの説明はこちらになります。今回は、モデルとしてN1_CLASS_MODELを使用し、すべての予測子である属性を使用する、という指定になっています。

結果を確認します。
SELECT CUST_ID, Y_BOX_GAMES, Y_BOX_GAMES_PRED FROM N1_TEST_DATA;

予測がどれほど実際の値と一致しているのか、正解率を求めてみます。
select to_char(
    (
        sum(case when y_box_games = y_box_games_pred then 1 else 0 end) / count(*)
    ) * 100, '999.99') correct_pred_percentage
from n1_test_data;

おおよそ90%です。

次に混合行列(confusion matrix)を求めます。ターゲットである2つの列を含めてGROUP BYすることにより、SQLにて比較的簡単に求められます。
SELECT Y_BOX_GAMES, Y_BOX_GAMES_PRED, COUNT(*)
FROM N1_TEST_DATA GROUP BY Y_BOX_GAMES, Y_BOX_GAMES_PRED ORDER BY 1,2;

上の行から、真陰性(True Negative)、偽陽性(False Positive)、偽陰性(False Negative)、真陽性(True Positive)です。偽陰性の件数が高そうです。実際には購入しているのに購入しない、と予測していますので、実際の運用場面では、売り込みをしない機会損失になるでしょう。偽陽性はそれほどでもないので、無駄になる売り込みはあまり発生しないようです。

予測を実行する


モデルの作成と検証ができました。これから予測を実行する方法には、おおよそ以下の2つの方法があります。
  • ある時点ですべての顧客について、バッチで予測処理を行います。
  • 予測結果が必要なときに、顧客ごとに予測処理を行います。例えば、顧客情報のフォームを開いたときなどです。
今回の演習ではバッチ処理として、予測処理を行います。

まだY Box Gamesを所有していない顧客を対象として、購入意思を持ちそうかどうかを予測します。
CREATE TABLE CUST_PREDICTION AS
  SELECT CUST_ID, 
  PREDICTION(N1_CLASS_MODEL USING *) PREDICTION,
  PREDICTION_PROBABILITY(N1_CLASS_MODEL USING *) PRED_PROBABILITY
  FROM SH.SUPPLEMENTARY_DEMOGRAPHICS WHERE Y_BOX_GAMES = 0;

今後、バッチ処理にて予測を更新するには、上記処理をジョブとして登録し繰り返し実行させる必要がありますが、その実装は今回の演習には含んでいません。

APEXアプリケーションに組み込む


インポート済みのAPEXアプリケーションに、Oracle Machine Learningによる予測結果を追加します。予測結果は対話レポートの追加の列として表示されます。

Oracle Machine Learningノートブックが所有しているスキーマに含まれている表に、Oracle APEXからアクセスできるように権限を与えます。スキーマ名はOracle APEX側で使用しているスキーマ名ですが、ワークスペースを作成するときに同時に作成したスキーマの名前になります。大抵の場合はワークスペース名と同じ名前を使用していると思います。
grant select on cust_prediction to スキーマ名;
以下は、Oracle APEXのスキーマ名がMYWORKSPACEとして実行した結果になります。

インポート済みのCustomer Service AppをOracle APEXのアプリケーション・ビルダーから開き、編集作業を開始します。

編集対象の対話レポートを含む、ページ2のCustomersを開きます。

対話レポートで使用されているビューがCUSTOMER_Vであることを確認します。

ビューの定義を以下に置き換えます。顧客情報に機械学習を使用して予測した結果であるPREDICTION列とPRED_PROBABILITY列を追加します。SELECT文にある"CUST_ID"をc."CUST_ID"に変更している点に注意してください。
CREATE OR REPLACE FORCE VIEW "CUSTOMER_V" 
("CUST_ID", "CUST_FIRST_NAME", "CUST_LAST_NAME", "CUST_GENDER", 
 "CUST_YEAR_OF_BIRTH", "CUST_MARITAL_STATUS", "CUST_STREET_ADDRESS", 
 "CUST_POSTAL_CODE", "CUST_CITY", "CUST_CITY_ID", "CUST_STATE_PROVINCE", 
 "CUST_STATE_PROVINCE_ID", "COUNTRY_ID", "CUST_MAIN_PHONE_NUMBER", 
 "CUST_INCOME_LEVEL", "CUST_CREDIT_LIMIT", "CUST_EMAIL", "CUST_TOTAL", 
 "CUST_TOTAL_ID", "CUST_SRC_ID", "CUST_EFF_FROM", "CUST_EFF_TO", "CUST_VALID",
 prediction, pred_probability
) AS 
  select c."CUST_ID","CUST_FIRST_NAME","CUST_LAST_NAME","CUST_GENDER",
  "CUST_YEAR_OF_BIRTH","CUST_MARITAL_STATUS","CUST_STREET_ADDRESS",
  "CUST_POSTAL_CODE","CUST_CITY","CUST_CITY_ID","CUST_STATE_PROVINCE",
  "CUST_STATE_PROVINCE_ID","COUNTRY_ID","CUST_MAIN_PHONE_NUMBER",
  "CUST_INCOME_LEVEL","CUST_CREDIT_LIMIT","CUST_EMAIL","CUST_TOTAL",
  "CUST_TOTAL_ID","CUST_SRC_ID","CUST_EFF_FROM","CUST_EFF_TO","CUST_VALID",
  prediction, pred_probability
  from sh.customers c left outer join mluser1.cust_prediction p
       on c.cust_id = p.cust_id
/

APEXアプリケーションに戻り、ページ2の対話レポートのSQLを編集します。SELECT文に以下の列RECOMMENDATIONを追加します。"CUST_EMAIL"の後ろにカンマを追加するのを忘れないようにしましょう。
case
when prediction = 1 and pred_probability > 0.5 then
cust_first_name || ' is ' || to_char(pred_probability * 100, '999.99') || '% likely to be interested in Y Box Games'
else
''
end recommendation

機械学習による予測を追加したAPEXアプリケーションを確認する


更新したアプリケーションを実行し、顧客リストの対話レポートを表示します。Recommendation列に何も表示されていない行が多いので、Y Box Gamesの販売対象となっている顧客はあまり存在していません。これらの顧客は、この製品に興味はもたないでしょう。

名(Cust First Name)が"Connor"、姓(Cust Last Name)が"Clark"の顧客を検索します。1970年生まれのConnor Clarkさんの行にはRecommendation列の表示があり、Y Box Gamesを高い確率で気に入るだろうと案内されています。

Oracle Machine Learningの機能をOracle APEXから使用するアプリケーションを作成するワークショップの内容は以上でした。行ったことはAPEXアプリケーションに一列、Recommendationを追加しただけですが、これが有るのと無いのとでは大きく違うことを感じるのではないでしょうか。

Oracleデータベースが提供する機械学習の機能については、以下の資料も提供されています。 Oracle Machine Learningについて、より実践的な内容になっています。
最後にマニュアルへのリンクをまとめます。