無限Streamに終了条件を設定する
例えばDOMのようなツリー構造があったとして、あるノードを渡された時に、ルートからそのノードまでの名前を連結してパス文字列を作成する、というプログラムを考えます。
Java7でも動くように、副作用ループを使った書き方だとこんな感じ。
public static String getTreePath(Node node, String delimiter) { LinkedList<String> nodePath = new LinkedList<>(); for (Node n = node; n != null; n = n.getParent()) { //根でgetParentするとnullが帰るものとする nodePath.add(0, n.getName()); } return delimiter + String.join(delimiter, nodePath); }
んで、これをJava8のStream APIで書くとどう書けるのか考えてみて、最初に書いたのがこれ。
public static String getTreePath2(Node node, String delimiter) { List<String> names = Stream.iterate(node, Node::getParent) .filter(n -> n != null) .map(o -> o.getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
半分くらい書いたところで、あれ?って思ったんですが、案の定これは NullPointerException で落ちました。Stream.iterate に渡した関数(Node::getParemt)を連鎖的に評価させてその結果をリスト化するわけですが、getParent が null を返しても、そこで評価を止めることができないわけですね。
API Document とかいろいろ漁って調べてみましたが、単純に Stream.iterate や Stream.generate で生成した無限 Stream は、limitで決まった数で区切るか、zip で有限 Stream と結合するか、短絡評価をする終端操作で区切るしかなさそうです。「指定した条件を満たした最初の要素より前のものだけ抽出」のような中間操作ないし終端操作はない模様。
こういう場合は、 Stream の作成のしかたからカスタマイズする必要があって、API Documentによると、「最も単純だが低パフォーマンスな」作成方法は、Iterator の実装クラスを作って、そのインスタンスから Splitterator を作ることとありました。なるほど、Iterator なら生成関数と終了条件関数をセットで実装できますね。
で書いてみたのがこちら。
public static String getTreePath3(Node node, String delimiter) { Iterator<Node> iter = new Iterator<Node>() { private Node n = node; @Override public boolean hasNext() { return (n != null); } @Override public Node next() { Node pre = n; n = pre != null ? pre.getParent() : null; return pre; } }; List<String> names = StreamSupport.stream( Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), false) .map(o -> o.getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
長ぇ。
最初のループを使った書き方とくらべても断然めんどくさいですね。
Stream を作るところを、別のメソッドに切り出して、軽く汎用的にしてみます。引数としては、 Stream.iterate の引数に終了条件 term を加えた形ですね。
private static <T> Stream<T> iterateLimited(T seed, UnaryOperator<T> generator, Predicate<T> term) { Iterator<T> iter = new Iterator<T>() { T current = seed; @Override public boolean hasNext() { return !term.test(current); } @Override public T next() { T pre = current; current = generator.apply(pre); return pre; } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), false); }
これを使うと、メインの処理はこんなふうに書けます。(ついでに Optional 使って null を隠蔽してみました)
public static String getTreePath4(Node node, String delimiter) { List<String> names = iterateLimited( Optional.ofNullable(node), o -> o.map(n -> n.getParent()), o -> !o.isPresent()) .filter(o -> o.isPresent()) .map(o -> o.get().getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
だいぶ「らしく」なったような気がします。
… iterateLimited みたいな関数は、標準で用意してもらいたいものですが…
(2017/12/06追記)
Java9では簡単に書けるようになりました。
noisyspot.hatenablog.com
ADFアプリのリソースチューニング
ADFアプリのリソースチューニングというと、独自のチューニングポイントとしてアプリケーション・モジュール・プールがありますが、ここでは大抵のJavaEEアプリで問題となるHTTPセッションタイムアウトとデータベース接続プールについて考えます。
HTTPセッションタイムアウト
HTTPセッションタイムアウト時間は、5分程度の短めの時間を設定しておくのがよいようです。
従来の(非AJAXな)JavaEEアプリとして業務アプリなどを作る場合は、30分や60分といった長めの時間を設定することが多かったと思います。このようなアプリケーションでは、画面遷移のタイミングでしかリクエストが発生しないために、入力画面での長考によってタイムアウトが頻発することになるからです。
また、大抵のORMフレームワークでは、HTTPリクエストからレスポンスの間にトランザクションを完結させるように作ってあるので、セッション属性に積むデータ量にさえ気をつければ、HTTPセッション残留によるリソースの専有はそれほど気をつけなくても良かったということもあります。
ADFの場合は事情が変わります。ADFアプリケーションでは、複数のリクエストを跨がってトランザクションが維持されます。これは、内部的には、HTTPセッションとDB接続が、アプリケーションモジュールを介して結び付けられることによって実現されています。このため、セッションの長時間残留はDB接続の枯渇に繋がります。
一方で、ADF Facesの画面は部分書き換えや値検証などのためにリクエストが頻発するため、作業中にセッションタイムアウトに至る確率はかなり抑えられます。また、デフォルトの設定では、2分ほどサーバとの通信がない場合はダイアログが表示され、「OK」をクリックするとサーバ通信を行うので、ここでもタイムアウトの発生を抑止できます。
このような事情から、HTTPセッションタイムアウトは短めに設定するのがよいです。
データベース接続プール
データベース接続プールについては、最大数を多めに設定しましょう。同時利用ユーザが10名程度でも、接続プールの最大値は50以上は確保するべきです。
すでに述べた通り、ADFアプリでは複数リクエストに跨がってDB接続を保持しますので、リクエスト単位で開放される従来型のフレームワークに比べて、同時利用ユーザ数に対するDB接続使用数が格段に増加します。
また、複数のアプリケーションモジュールを構成するアプリケーションの場合、トランザクション分離のために、アプリケーションモジュールごとに個別のDB接続を保持することもあります。つまり、ひとつのセッションで複数の接続を専有することもありえます。
(ちなみに、1回限りのサービス提供のためのアプリケーションモジュールの場合、サービスメソッドの最初でgetDBTransaction、最後でDBTransaction#closeTransactionを呼ぶようにしましょう。DB接続の保持期間をメソッド実行中のみに制限できます)
今時のOracle Databeseであれば、MAX_SESSIONは数百程度に設定しても割と平気なので、ケチケチせずに行きましょう。
ADF Tableの末尾に新規行を追加するには
ADFのデータコントロールで、新規行を追加するOperationBindingは、CreateInsertにしろCreateWithParamにしろ、「カーソル位置への挿入」なので、データの末尾に追加するというのが素直に行えません。しょぼい。
OTNのディスカッションでも時々話題になるようで、いくつか方法も提案されてます。翻訳しようと思ったんですが、まあソースコード見ればなんとなくわかるので、リンクだけ。
Unwinding ADF: How to add a new row at the end of the ADF Table
Luc Bors Weblog: ADF 11g : How to control where a new row is inserted
画面やデータモデルの仕様にもよりますが、選択状態にかかわらず行追加は必ず最後に、ということであれば、ViewObject#insertRowメソッドをオーバライドしてしまう前者の方法がシンプルでいいかも知れません。
連番カラムを設けて、行挿入後にADF TableにSortEventを放り込むというのも試してみましたが、これだと新規行が表示されませんでした。ViewObjectへの行追加がTableに反映される前にSortEventが処理されてしまい、その後のPartial Refreshが行われないせいかなーと推測。
それにしても、できるだけJavaやSQLを書かずに済むように、というポリシーはいいと思うんだけど、結局この程度でコード書かなきゃいけないっていう中途半端さがイケてないなあ。
ADF Table とデータコントロールの選択状態同期
だんだん内容がショボくなって参ります。
データコントロールツリーからコレクションをデザイナ上にドラッグ・ドロップしてTableを構成するとき、行選択を有効にしておくと勝手に選択状態が同期するようになるのですが、一度行選択を無効にして構成してしまうと、後で RowSelection 属性の値だけ変えても、見かけ上は行選択できるようになりますが、データコントロールまで反映されません。
で、毎回「どの属性設定するんだっけ…」と悩むことになるので、ここにメモしときます。
選択状態の同期は、af:table 要素の以下の2つの属性にEL式を設定します。
- selectionListener : #{bindings.(DC名).collectionModel.makeCurrent}
- selectedRowKeys : #{bindings.(DC名).collectionModel.selectedRow}
selectionListener属性で、Tableの選択状態の変更を拾ってデータコントロールへつなげ、selectedRowKeysでデータコントロールの選択状態をTableに反映する、という感じ。
あと、アクションメソッド内で「選択された行の項目Nを取得したい」なんてときは、無理にRichTableオブジェクトごゴニョゴニョするよりは、同期するデータコントロールの取得したい項目を個別にバインドしておいて、EL式#{bindings.(項目名).inputValue}などで取ったほうが楽です。
参考:
http://otndnld.oracle.co.jp/document/products/jdev/11/doc_cd/web.1111/B52028-01/web_tables_forms.htm
PagePhaseListener のメモ
ADFには、JSFのPhaseEventとは別に、ページライフサイクルの各フェーズの開始・終了で発火するPagePhaseEventというのがあります。
こいつのリスナ、PagePhaseListener は、呼ばれるフェーズを選択できず、毎フェーズの開始/終了で必ず呼ばれます(JSFでいうところの「ANY_PHASE」)。ページ生成時に一回だけ呼ばれる前提でリソースを確保すると、えらいことになるので注意。
フェーズの順序は、開発者ガイドの以下のページで説明されています。
http://otndnld.oracle.co.jp/document/products/jdev/11/doc_cd/web.1111/B52028-01/adf_lifecycle.htm
…とまあここまで書いておいてなんですが、大抵の用途ではJSFのPhaseListenerだけで事足りると思います(というか、PagePhaseListenerのアドバンテージが今ひとつ思い浮かばない)。
それから、JSFでも同様ですが、複数コンポーネントの書き換えが同時に発生する場合は、ひとつのスレッドで上記のライフサイクルが複数回回ったりするのでこれも注意。
DB接続のようなリソースを、1リクエストの中で確保して解放、というような時は、ここではなく、素直にServletFilterとして組み込んだほうが安全ですね。
PhaseEvent, PagePhaseEvent をそれぞれ拾って、フェーズ名をログに出力するコードを置いときます。両方組み込んで実行すると、JSF,ADFの各フェーズがどういう絡みになっているか、なんとなく読み解けるかも。
https://github.com/shout-poor/Smallcodes/tree/master/study_adf