https://www.thatjeffsmith.com/archive/2025/02/sessionless-transactions-in-database-23ai-your-rest-apis/
セッションレス・トランザクション!? なんだろうと思ったので上記の記事を読んで、自分でも動作を確認してみました。
Oracle Databaseのトランザクションは基本的にデータベースのセッションに紐づいていますが、セッションレス・トランザクションはセッションと切り離すことができます。
セッションレス・トランザクションを使うと、Oracle REST Data Servicesの連続したREST API呼び出しをひとつのデータベース・トランザクションに含めることができます。
テストに使用したAPEXアプリケーションは以下のように動作します。
ボタンStart, Update, Commit, Rollbackはそれぞれ、1回のREST API呼び出しに対応しています。
ボタンStartをクリックしセッションレス・トランザクションを開始します。呼び出したブラウザにXID - トランザクションIDを返します。ボタンUpdateではそのXIDとTextに入力した文字列を引数として、REST API経由で表TEST_SESSIONLESS_TXに文字列を書き込みます。
連続した文字列の書き込みは、ボタンCommitのクリックで確定します。これもREST API呼び出しです。ボタンRollbackをクリックすると、それまで書き込まれた文字列はロールバックされます。
REST APIやSQLclのセッションからはDBMS_TRANSACTION.SUSPEND_TRANSACTIONを呼び出すことができますが、Oracle APEXのプロセスからDBMS_TRANSACTION.SUSPEND_TRANSACTIONを呼び出すと、処理がハングします。Oracle APEXに埋め込まれたSQLはDBMS_SQL(正確にはDBMS_SYS_SQL)パッケージを呼び出して実行されるので、一般的なセッションとは扱いが異なることがハングする理由だと思われます。そのため、セッションレス・トランザクションは概ねOracle REST Data ServicesのREST APIとともに使用することになると思われます。
以下より、上記のサンプル・アプリケーションの実装について簡単に紹介します。
ボタンUpdateをクリックしたときにデータを挿入する表をTEST_SESSIONLESS_TXとして作成しています。画面下に配置されたクラシック・レポートのソースです。
This file contains hidden or 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 tables | |
create table test_sessionless_tx ( | |
id number generated by default on null as identity | |
constraint test_sessionless_tx_id_pk primary key, | |
text varchar2(80 char) not null, | |
created date not null, | |
created_by varchar2(255 char) not null, | |
updated date not null, | |
updated_by varchar2(255 char) not null | |
); | |
-- triggers | |
create or replace trigger test_sessionless_tx_biu | |
before insert or update | |
on test_sessionless_tx | |
for each row | |
begin | |
if inserting then | |
:new.created := sysdate; | |
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user); | |
end if; | |
:new.updated := sysdate; | |
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user); | |
end test_sessionless_tx_biu; | |
/ |
セッションレス・トランザクションを開始、再開するには、DBMS_TRANSACTION.START_TRANSACTIONを呼び出します。
ボタンStart、Update、Commit、Rollbackの処理に対応するファンクションやプロシージャを組み込んだパッケージTEST_SESSIONLESS_TX_PKGを作成します。
ファンクションSTART_TXがボタンStart、プロシージャUPDATE_TXがボタンUpdate、プロシージャCOMMIT_TXがボタンCommit、プロシージャROLLBACK_TXがボタンRollbackの処理になります。
パッケージTEST_SESSIONLESS_TX_PKGの定義部です。
This file contains hidden or 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 test_sessionless_tx_pkg as | |
/** | |
* セッションレス・トランザクションを開始する。 | |
* システムが生成したトランザクションIDを返す。 | |
*/ | |
function start_tx( | |
p_timeout in number default 60 | |
) | |
return varchar2; | |
/** | |
* 表TEST_SESSIONLESS_TXにp_textに与えられた文字列を1行挿入する。 | |
* p_xidで与えられたセッションレス・トランザクションで実行する。 | |
*/ | |
procedure update_tx( | |
p_xid in varchar2 | |
,p_text in varchar2 | |
); | |
/** | |
* p_xidで与えられたセッションレス・トランザクションをコミットする。 | |
*/ | |
procedure commit_tx( | |
p_xid in varchar2 | |
); | |
/** | |
* p_xidで与えられたセッションレス・トランザクションをロールバックする。 | |
*/ | |
procedure rollback_tx( | |
p_xid in varchar2 | |
); | |
end test_sessionless_tx_pkg; | |
/ |
パッケージTEST_SESSIONLESS_TX_PKGの本体部です。ほとんどはDBMS_TRANSACTION.START_TRANSACTIONを呼び出しセッションレス・トランザクションの開始または再開をした後に、1行データベースの操作を行っているだけです。
This file contains hidden or 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 body test_sessionless_tx_pkg as | |
/** | |
* 新たにセッションレス・トランザクションを開始し、サスペンドする。 | |
* システムが生成したトランザクションIDを呼び出し元に返す。 | |
* | |
* dbms_transaction.start_transactionの戻り値の方はVARCHAR2だが、 | |
* 引数xidが受け取るのはRAW型なので、dbms_transaction.start_transactionの | |
* 戻り値をxidに与える際にはhextorawでRAW型にする必要がある。 | |
*/ | |
function start_tx( | |
p_timeout in number default 60 | |
) | |
return varchar2 | |
as | |
l_xid_raw raw(16); | |
l_xid_hex varchar2(32); | |
begin | |
l_xid_hex := dbms_transaction.start_transaction( | |
-- xid is null to generate id by system. | |
transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS | |
,timeout => p_timeout | |
,flag => dbms_transaction.TRANSACTION_NEW | |
); | |
dbms_transaction.suspend_transaction; | |
return l_xid_hex; | |
end start_tx; | |
/** | |
* 指定されたセッションレス・トランザクションを再開したのち、 | |
* p_textをtest_sessionless_txに挿入する。 | |
* 挿入後、セッションレス・トランザクションをサスペンドする。 | |
*/ | |
procedure update_tx( | |
p_xid in varchar2 | |
,p_text in varchar2 | |
) | |
as | |
l_xid_raw raw(16); | |
l_xid_hex varchar2(32); | |
begin | |
l_xid_raw := hextoraw(p_xid); | |
l_xid_hex := dbms_transaction.start_transaction( | |
xid => l_xid_raw | |
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS | |
,flag => dbms_transaction.TRANSACTION_RESUME | |
); | |
insert into test_sessionless_tx(text) values(p_text); | |
dbms_transaction.suspend_transaction; | |
end update_tx; | |
/** | |
* 指定されたセッションレス・トランザクションを再開してコミットする。 | |
*/ | |
procedure commit_tx( | |
p_xid in varchar2 | |
) | |
as | |
l_xid_raw raw(16); | |
l_xid_hex varchar2(32); | |
begin | |
l_xid_raw := hextoraw(p_xid); | |
l_xid_hex := dbms_transaction.start_transaction( | |
xid => l_xid_raw | |
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS | |
,flag => dbms_transaction.TRANSACTION_RESUME | |
); | |
commit; | |
end commit_tx; | |
/** | |
* 指定されたセッションレス・トランザクションを再開してロールバックする。 | |
*/ | |
procedure rollback_tx( | |
p_xid in varchar2 | |
) | |
as | |
l_xid_raw raw(16); | |
l_xid_hex varchar2(32); | |
begin | |
l_xid_raw := hextoraw(p_xid); | |
l_xid_hex := dbms_transaction.start_transaction( | |
xid => l_xid_raw | |
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS | |
,flag => dbms_transaction.TRANSACTION_RESUME | |
); | |
rollback; | |
end rollback_tx; | |
end test_sessionless_tx_pkg; | |
/ |
これらの処理を呼び出すRESTサービスを作成します。RESTサービス・モジュールとしてsessionlessを作成し、そのテンプレートとしてstart、update、commit、rollbackを作成します。テンプレートstartにはGETハンドラを作成し、それ以外はPOSTハンドラを作成します。ハンドラの処理はすべてPL/SQLで記述します。
処理内容自体は、パッケージTEST_SESSIONLESS_TX_PKGの対応するファンクションまたはプロシージャの呼び出しです。
This file contains hidden or 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
-- Generated by ORDS REST Data Services 24.3.2.r3121009 | |
-- Schema: WKSP_APEXDEV Date: 金 2月 14 05:46:05 2025 | |
-- | |
BEGIN | |
ORDS.DEFINE_MODULE( | |
p_module_name => 'sessionless', | |
p_base_path => '/sessionless/', | |
p_items_per_page => 25, | |
p_status => 'PUBLISHED', | |
p_comments => NULL); | |
ORDS.DEFINE_TEMPLATE( | |
p_module_name => 'sessionless', | |
p_pattern => 'start', | |
p_priority => 0, | |
p_etag_type => 'HASH', | |
p_etag_query => NULL, | |
p_comments => NULL); | |
ORDS.DEFINE_HANDLER( | |
p_module_name => 'sessionless', | |
p_pattern => 'start', | |
p_method => 'GET', | |
p_source_type => 'plsql/block', | |
p_mimes_allowed => NULL, | |
p_comments => NULL, | |
p_source => | |
'declare | |
l_xid varchar2(32); | |
l_response clob; | |
l_response_json json_object_t; | |
begin | |
l_xid := test_sessionless_tx_pkg.start_tx(p_timeout => 180); | |
-- レスポンスの送信。 | |
l_response_json := json_object_t(); | |
l_response_json.put(''id'', l_xid); | |
l_response_json.put(''success'', true); | |
l_response := l_response_json.to_clob(); | |
htp.p(l_response); | |
end;'); | |
ORDS.DEFINE_TEMPLATE( | |
p_module_name => 'sessionless', | |
p_pattern => 'update', | |
p_priority => 0, | |
p_etag_type => 'HASH', | |
p_etag_query => NULL, | |
p_comments => NULL); | |
ORDS.DEFINE_HANDLER( | |
p_module_name => 'sessionless', | |
p_pattern => 'update', | |
p_method => 'POST', | |
p_source_type => 'plsql/block', | |
p_mimes_allowed => 'application/json', | |
p_comments => NULL, | |
p_source => | |
'declare | |
l_request json_object_t; | |
l_xid varchar2(32); | |
l_text varchar2(4000); | |
l_response clob; | |
l_response_json json_object_t; | |
begin | |
l_request := json_object_t(:body_json); | |
l_xid := l_request.get_string(''id''); | |
l_text := l_request.get_string(''text''); | |
test_sessionless_tx_pkg.update_tx(l_xid, l_text); | |
-- レスポンスの送信。 | |
l_response_json := json_object_t(); | |
l_response_json.put(''success'', true); | |
l_response := l_response_json.to_clob(); | |
htp.p(l_response); | |
end;'); | |
ORDS.DEFINE_TEMPLATE( | |
p_module_name => 'sessionless', | |
p_pattern => 'commit', | |
p_priority => 0, | |
p_etag_type => 'HASH', | |
p_etag_query => NULL, | |
p_comments => NULL); | |
ORDS.DEFINE_HANDLER( | |
p_module_name => 'sessionless', | |
p_pattern => 'commit', | |
p_method => 'POST', | |
p_source_type => 'plsql/block', | |
p_mimes_allowed => 'application/json', | |
p_comments => NULL, | |
p_source => | |
'declare | |
l_request json_object_t; | |
l_xid varchar2(32); | |
l_response clob; | |
l_response_json json_object_t; | |
begin | |
l_request := json_object_t(:body_json); | |
l_xid := l_request.get_string(''id''); | |
test_sessionless_tx_pkg.commit_tx(l_xid); | |
-- レスポンスの送信。 | |
l_response_json := json_object_t(); | |
l_response_json.put(''success'', true); | |
l_response := l_response_json.to_clob(); | |
htp.p(l_response); | |
end;'); | |
ORDS.DEFINE_TEMPLATE( | |
p_module_name => 'sessionless', | |
p_pattern => 'rollback', | |
p_priority => 0, | |
p_etag_type => 'HASH', | |
p_etag_query => NULL, | |
p_comments => NULL); | |
ORDS.DEFINE_HANDLER( | |
p_module_name => 'sessionless', | |
p_pattern => 'rollback', | |
p_method => 'POST', | |
p_source_type => 'plsql/block', | |
p_mimes_allowed => 'application/json', | |
p_comments => NULL, | |
p_source => | |
'declare | |
l_request json_object_t; | |
l_xid varchar2(32); | |
l_response clob; | |
l_response_json json_object_t; | |
begin | |
l_request := json_object_t(:body_json); | |
l_xid := l_request.get_string(''id''); | |
test_sessionless_tx_pkg.rollback_tx(l_xid); | |
-- レスポンスの送信。 | |
l_response_json := json_object_t(); | |
l_response_json.put(''success'', true); | |
l_response := l_response_json.to_clob(); | |
htp.p(l_response); | |
end;'); | |
COMMIT; | |
END; |
RESTモジュールsessionlessとして作成したREST APIのブラウザからの呼び出しは、APEXアクションとして実装します。
共有コンポーネントの静的アプリケーション・ファイルとしてtest.jsを作成します。
This file contains hidden or 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
/* | |
+ ボタンクリックにより、サーバーサイドでセッションレスなトランザクションを開始する | |
*/ | |
const REST_PATH = '/ords/apexdev'; // 環境に合わせて変更が必要。 | |
const a_START = { | |
name: "START", | |
action: (event, element, args) => { | |
fetch(REST_PATH + '/sessionless/start') | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`HTTP error! Status: ${response.status}`); | |
} | |
return response.json(); // Parse the JSON response | |
}) | |
.then(data => { | |
console.log(data); | |
apex.item("P1_XID").setValue(data.id); | |
}) | |
.catch(error => { | |
console.error('Error fetching data:', error); | |
}); | |
} | |
}; | |
const a_UPDATE = { | |
name: "UPDATE", | |
action: (event, element, args) => { | |
fetch(REST_PATH + '/sessionless/update', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
id: apex.item("P1_XID").getValue(), | |
text: apex.item("P1_TEXT").getValue() | |
}) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`HTTP error! Status: ${response.status}`); | |
} | |
return response.json(); // Parse the JSON response | |
}) | |
.then(data => { | |
console.log(data); | |
}) | |
.catch(error => { | |
console.error('Error fetching data:', error); | |
}); | |
} | |
}; | |
const a_COMMIT = { | |
name: "COMMIT", | |
action: (event, element, args) => { | |
fetch(REST_PATH + '/sessionless/commit', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
id: apex.item("P1_XID").getValue() | |
}) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`HTTP error! Status: ${response.status}`); | |
} | |
return response.json(); // Parse the JSON response | |
}) | |
.then(data => { | |
console.log(data); | |
}) | |
.catch(error => { | |
console.error('Error fetching data:', error); | |
}); | |
} | |
}; | |
const a_ROLLBACK = { | |
name: "ROLLBACK", | |
action: (event, element, args) => { | |
fetch(REST_PATH + '/sessionless/rollback', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
id: apex.item("P1_XID").getValue() | |
}) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`HTTP error! Status: ${response.status}`); | |
} | |
return response.json(); // Parse the JSON response | |
}) | |
.then(data => { | |
console.log(data); | |
}) | |
.catch(error => { | |
console.error('Error fetching data:', error); | |
}); | |
} | |
}; | |
const controlsCtx = apex.actions.createContext("controls", document.getElementById("CONTROLS")); | |
controlsCtx.add([a_START, a_UPDATE, a_COMMIT, a_ROLLBACK]); |
作成したファイルtest.jsは、ページ・プロパティのJavaScriptのファイルURLに記述します。
[defer]#APP_FILES#test#MIN#.js
アプリケーションの画面を作成します。
ボタンに紐づけるAPEXアクションのコンテキストを作成するリージョンとしてControlsを作成しています。静的IDにCONTROLSを設定しています。REST API呼び出しに関わるボタンはすべてのこのリージョンに含めます。
セッションレス・トランザクションのトランザクションIDを保持するページ・アイテムとして、P1_XIDを作成しています。セッション・ステートのストレージはリクエストごと(メモリーのみ)を選択します。
ボタンSTART、UPDATE、COMMIT、ROLLBACKの動作のアクションはすべて動的アクションで定義とします。詳細のカスタム属性にdata-action=を設定し、呼び出すAPEXアクションを指定します。
ボタンUpdateをクリックしたときに表TEST_SESSIONLESS_TXに書き込む文字列を入力するページ・アイテムをP1_TEXTとして作成しています。セッション・ステートのストレージはリクエストごと(メモリーのみ)です。
ボタンREFRESHを作成し、動的アクションでクラシック・レポートのリージョンをリフレッシュしています。
ボタンUpdateをクリックして表TEST_SESSIONLESS_TXを更新しても、レポートに更新した行は現れません。ボタンCommitをクリックしてレポートをリフレッシュすると、それまでにUpdateした行が表示されます。
ボタンRESETはハウスキーピングに使用します。クリックするとプロセスRESETを呼び出し、表TEST_SESSIONLESS_TXの内容を全削除します。
プロセスRESETのソースのPL/SQLコードとして以下を記述します。
delete from test_sessionless_tx;
ボタンRESETをクリックしたときに実行されるように、サーバー側の条件のボタン押下時にRESETを指定します。
確認用のアプリケーションについての説明は以上です。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/test-sessionless-tx.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完