2026/01/25

Domino 体力測定:#4)ビュー操作 ④ - ビューの列数と値の取得

背景と目的

前回は実運用に近い検証を目的に、ビューから取得する文書のサイズ(フィールド数)とビュー操作を行うメソッドの特定を測定しました。今回は、ビューに定義されている列数とレスポンスの関係について調査します。


測定環境

測定に使用する PC はこれまでと同じ PCを使用します。また、DB は前回作成したダミーフィールドを 200 個追加したものを使って測定します。

今回のテストでは、ビューに配置するダミーフィールドの数を変化させテストします。利用するビューは次の 5 種類です。

View 名 テスト文書の状態
000 ダミー列なし。
050 ダミー列を 50 個追加(Text_00 ~ Text_31)
100 ダミー列を 100 個追加(Text_00 ~ Text_63)
150 ダミー列を 150 個追加(Text_00 ~ Text_95)
200 ダミー列を 200 個追加(Text_00 ~ Text_C7)


手順

測定手順はこれまでと全く同じとします。ビューに対して 20 回の検索を行い、取得したコレクションからエントリと値を取得する操作を 50 回行います。測定回数は 10 回です。

この測定を View 050 ~ 200 まで順に行います(View 000 については前回の結果を流用)。

なお、テスト項目と測定の項目は「#1)ビュー操作 ① - 測定方法」をご確認ください。


結果

測定結果は次の通りでした。

このままではわかりにくいので、測定時間の推移をテスト項目ごとにグラフ化します。

結果が水平となっているのは、D2、D3、E4、E5 です。それ以外のテスト項目については、列数に比例して処理時間が増えているように見えます。

前回に倣って、値の取得(D1 ~ D3、E1 ~ E3 )に絞ってグラフ化ました。全体の処理時間(折れ線グラフ)も、列数にほぼ比例して処理時間が増えています。また、列数の増加に比例して、GetAllEntriesByKey の方が遅くなっていることが確認できます。


考察

今回の測定結果より推察できるビュー操作の特徴を整理します。


◇ 検索時間の違いはコレクション作成時間?

まず、ビューを検索し検索結果のコレクションを確認する D1 と E1 を比較します。処理時間の比率を確認するとダミー列なしで 1.7 倍、200 個で 1.6 倍の差があり、グラフ化すると次の通りでした。

D1 と E1 の測定では、検索キーの指定、検索結果が同じなので、ビュー検索時間は一定と仮定できると思います。グラフはほぼ水平であることから、コレクション作成時間は、ビューの列数に比例しており、列数が増えるほど GetAllEntriesByKey には不利に働くといえます。


◇ NotesViewEntry クラスのオーバヘッド

E2 の NotesViewEntry 取得の結果を見るとダミー列数に比例して、処理時間が増えています。D2 の NotesDocument の取得はダミー列数に依存せず一定な上、ほぼ 0 なので、エントリ取得という点においても、GetAllEntriesByKey が不利といえます。


◇ 列数の調整で逆転はない

前回のテストで、NotesDocument から値を取得すると文書サイズに比例して遅くなる結果が出ました。今回のテストはその特性が顕著に表れるダミーフィールド 200 個のテストデータで行いました。

ところが、上記 2 点のとおり、検索処理自体に加え、検索結果からエントリを取得する処理でも、列数に比例してコストが発生することがわかりました。

NotesViewEntry??? クラスを使用したオーバヘッドを赤の斜線、NotesDocument 使用時のオーバヘッドを青の斜線で表すと次のようになります。

最も有利なダミー列なしでも赤の部分があまりに大きく、列を増やすにつれてその差が開きます。この結果より、今回のテストケースにおいては、ビューの列数の調整で GetAllEntriesByKey が早くなることはないことがわかります。


◇ ビュー索引を活用

アルゴリズムの評価ではオーダ(Order)とい指標があります。これは、入力データ数に対して処理時間の変化するかを示すものです。

ソートを例にするとデータ数を n とすると、最悪で n2(n の 2 乗)、一般的には n log n の処理時間が必要なります。データがソート済みだった場合に限定すると n となるパターンがあります。オーダ n は、データ件数と処理時間が比例の関係になることを示します。一方、n log n や n2 は、データ件数の増加に従い急激に処理時間が増加します。

今回のテストでは、ビューの列数に依存する D1、E1、E2 はほぼ比例の関係になっています。この結果から、今回テストしたメソッドには、実行時にソート処理を行うような要素は含まれていないと推察できます。

よって、今回テストした処理では、すでにソート済みの索引データを順になぞっているだけであり、比例的な特性(オーダ n)になっていると考えられます

ビューの検索処理が、ビュー索引を活用した構造になっていることの一つの証左と言えますね。


まとめ

前回の検証では、

  • 文書から値を取得する処理は、フィールド数に比例して遅くなる
  • ビューからの値取得では、文書内のフィールド数に依存せず一定
という結果が出ました。

今回の検証では、ビューに必要な列を用意すれば GetAllEntriesByKey の方が早くなる可能性を調査したのですが、結果は、

  • ビューの列数とレスポンスは比例の関係
  • NotesViewEntry??? の処理コストがあまりも高い

ことがわかり、優位に立つことはない結果となりました。

ただ、前回、今回の検証では、検索結果(コレクションのサイズ)を 1000 としていました。実運用では、少々大きすぎるかもしれません。コレクションを小さくした場合や GetEntryByKey ではどうなるかの検証が必要かもしれませんね。


前回 Domino 体力測定


2026/01/20

Int と CInt (LotusScript)

LotusScript で実数を整数に変換する関数として Int と CInt 関数の 2 種類があります。PC が一般的になり始めたころに BASIC 言語を使用していた私には Int の方がなじみがあります。その経験から CInt も同じだろうと無頓着に使用してきましたが、微妙に差があったのでまとめておきます。


丸め方の違い

デザイナーヘルプによると次のように説明されています。

Int 引数に指定した数値に等しいか、それよりも小さい整数値のうち、指定した数値に最も近い値を返します。
CInt 値を Integer データ型に変換して返します。
最も近い整数に丸め、Integer 型の値として返します。

ニュアンスから関数の立ち位置が見えてきますね。Int は整数値に変換することを主題にしてています。CInt は型変換が主題で、その副次的機能として丸め処理が発生しているようです。

そして、小数値の処理である丸め方の表現に違いがあることがわかります。


丸め方の動作検証

それでは実際に処理の違いを確認しましょう。簡単なエージェントを作成し、Int と CInt の差を確認します。

Option Declare

Sub Initialize
   xTest 1.5
End Sub

Function xTest(vdVal As Double)
   MsgBox "Int(" & CStr(vdVal) & ") = " & CStr(Int(vdVal))
   MsgBox "CInt(" & CStr(vdVal) & ") = " & CStr(CInt(vdVal))
End Function

引数を変えながら動作検証した結果は次の通りでした。

1.1 1.5 1.9 2.1 2.5 2.9
Int 1 1 1 2 2 2
CInt 1 2 2 2 2 3

Int は切り捨て処理であることが確認できます。しかし、CInt では 0.5 の処理が単純な四捨五入とは違います。この部分の動作を詳しく確認します。

1.49 1.50 1.51 2.49 2.50 2.51
Int 1 1 1 2 2 2
CInt 1 2 2 2 2 3

プログラミングや統計の世界では、5 ちょうどの扱いを「常に切り上げない」といった仕様が使われることがあり、統計的に偏りが出にくいことを目的としています。

CInt はこの仕様を採用しており、ヘルプでは ”四捨五入” と表現せず、”最も近い整数に丸める” という表現になっているということですね。


負の数の対応

続いては、負の数の処理に関して確認します。

CInt の動作はわかりやすく、単に符号が付いただけの動作になります。

ところが、Int の場合は、絶対値が変化しています。これは、指定した値よりも ”小さい” 最も近い整数となるためです。

-1.49 -1.50 -1.51 -2.49 -2.50 -2.51
Int -2 -2 -2 -3 -3 -3
CInt -1 -2 -2 -2 -2 -3

負の数はビジネスでは使用頻度が低いかもしれません。ただ、頻度が低いと間違い(確認漏れ)が発生しやすいので注意しましょう。


イレギュラーの対応

最後に、これら関数に対してイレギュラーな値を与えた場合の動作について整理します。

テストデータ Int CInt
Integer の範囲を超える数値 77777.7 77777 エラー
数値に変換できる文字列 "2.50" 2 2
数値に変換できない文字列 "14.5 FP1" エラー エラー
日付/時刻 Now 2026/1/18 エラー
Empty な Variant 型変数 0 0
配列 エラー エラー

まず、この結果より Int 関数は名称から Integer 型と思いがちですが、Integer 型を超えた値の処理が可能です。そのおかげで日付/時刻データから時刻部分(小数値)を削除する演算としても使用可能です。

式言語の @TextToNumber で文字列を数値に変換する場合、変換できる範囲で実行する機能があります。例えば "14.5 FP1" は、14.5 に変換してくれるのですが、LotusScript では単純にエラーとなります。

Variant 型変数を引数にする場合には、Empty(変数の宣言だけして値を代入し忘れた状態)は 0 となり、配列やオブジェクトが入っている場合にはエラーとなります。

このような、イレギュラー処理が発生すること自体あまり良いコーディングとは言えないと思います。必要な場合はコメントに記すなど、後から見てわかるようにしておきましょう。


2026/01/16

@Sort は高性能!

@Sort を使えば、リスト値をソートすることができます。 先日、この関数のヘルプを見ていると、さまざまな機能が入っていることに気付いたので紹介します。


@Sort の構文

Domino デザイナーヘルプで @Sort を調べると構文は次のようになっています。

   @Sort( list ; [ order ]; customSortExpression )

今回紹介するのは 2 つ目の引数 order です。

ここにキーワードを指定するとソート動作を詳細にコントロールできるようになります。キーワードは 2 つずつが対になっています。

キーワード(〇:デフォルト) 機能
[ASCENDING] 昇順 / 降順を指定
[DESCENDING]
[ACCENTSENSITIVE] 濁点、半濁点の区別
[ACCENTINSENSITIVE]
[CASESENSITIVE] 大文字、小文字の区別
[CASEINSENSITIVE]
[PITCHSENSITIVE] 全角、半角の区別
[PITCHINSENSITIVE]

デフォルトは ”昇順” かつ ”区別する” になっていて、区別させたくない場合に [???INSENSITIVE] を指定することになります。

キーワードの指定は :(コロン)で複数指定することができるので、さまざまなパターンのソートが実現できるということですね。


動作検証

大文字/小文字、全角/半角については結果が想像できるので、濁点/半濁点を検証します。

まず、キーワード未指定と [ACCENTINSENSITIVE] を指定した場合を比較すると次のようになります。[ACCENTINSENSITIVE] では濁点/半濁点だけでなく、小書きも無視してソートしてくれるようです。

ただ、これだけではひらがなとカタカナには効果がありません。区別なくソートするためには [CASEINSENSITIVE] も指定します。

残念ながら、カタカナの全角/半角については、正しく判定されません。モノは試しと [PITCHINSENSITIVE] も指定してみましたが効果がありませんでした。半角カタカナでは濁点/半濁点が 2 文字になるためだと思われます。


まとめ

今回は @Sort のソート順を指定するオプションについて紹介しました。Notes/Domino は海外の製品ですが、日本語の扱いにここまで対応しているんですね。『ソートといえば文字コード順』と思い込んでいたので、正直驚きました。どのような仕組みで実現しているのか ”中の人” に聞いてみたくなりました。

なお、@Sort のキーワードには [CUSTOMSORT] というものがあります。こちらについては、まだ不明瞭な点もあるため触れませんでした。解説できるぐらい理解できたら、改めてまとめたいと思います。


2026/01/10

Domino 体力測定:#3)ビュー操作 ③ - フィールド数による影響

背景と目的

前回はビュー操作を行う 2 つのメソッドの基本的な特性の測定結果を紹介しました。そのテストでは、テストデータの文書は小さく軽量、検索用のビューもシンプルでした。より実運用に近い検証となるよう、追加でテストしたいと思います。

今回はフィールド数を変化させ、レスポンスにどのような影響があるのか測定します。


測定環境

まず、測定に使用する PC は前回と同じものを使用します。SSD 搭載の今どきの環境の想定です。

フィールド数とレスポンスの関係を調べることから、今回のテスト DB は、一定のダミーフィールドの個数を変化させた5 種類を用意します。ダミーフィールドには、読み込み用と同じ 100 Byte のテストを設定します。

DB 名 テスト文書の状態 文書サイズ
000 前回と同じ。188 Byte
050 000 の文書にダミーフィールドを 50 個追加(Text_00 ~ Text_31)5288 Byte
100 000 の文書にダミーフィールドを 100 個追加(Text_00 ~ Text_63)10388Byte
150 000 の文書にダミーフィールドを 150 個追加(Text_00 ~ Text_95)15488Byte
200 000 の文書にダミーフィールドを 200 個追加(Text_00 ~ Text_C7)20588Byte

参考までですが、ダミーフィールド作成する部分のコードは次の通りです。

      '取得用の値
      s = Right("0000000000" & s, 10)
      s = s & s & s & s & s & s & s & s & s & s
      Call nd.Replaceitemvalue("Text", s)


      'ダミーフィールド作成(viWait は作成する個数)
      For i = 0 To viWait - 1
         Call nd.Replaceitemvalue("Text_" & Right("00" & Hex(i), 2), s)
      Next

250 個作成しようとするとエラーとなりました。9.0.1 FP8 以降で対応した LargeSummary を有効化する必要があるので、今回は 200 個までで検証としています。


手順

測定手順は前回と全く同じです。ビューに対して 20 回の検索を行い、取得したコレクションからエントリと値を取得する操作を 50 回行います。測定回数も同じ 10 回とします。

この測定を DB 050 ~ 200 まで順に行います(DB 000 については前回の結果を流用)。


結果

測定結果は次の通りでした。

横軸を文書サイズに設定し、各測定項目と DB 毎の結果をグラフ化すると次の通りです。

D3、E5 のグラフは右肩上がりの直線となっており、フィールド数に比例しているといえます。それ以外のグラフは水平で、フィールド数の影響は受けないことがわかります。


考察

今回の測定結果より導くことができるビュー操作の特徴を整理します。


◇ ビュー検索はフィールド数に依存しない

ビューの検索を行う D1、E1 ともフィールド数増減の影響を受けず、ほぼ一定の処理時間となっています。また、コレクションからエントリを取得する処理(D2、E2)も同様です。さらに、NotesViewEntry から NotesDocument を取得する処理(E4)も一定です。

これらの結果より、コレクション、コレクション内のエントリ操作、NotesDocument オブジェクトの取得までは、文書単位で処理をしており、文書内のフィールド数には依存しないことがわかります。

ここまでの処理においては、1 文書は 1 文書で、その内部の状態は関係ないということですね。


◇ 値取得はフィールド数に依存

NotesDocument から値を取得する処理(D3、E5)では、フィールド数に比例して、処理時間が増加します。

今回の検証データは、読み込む Text フィールド作成後、ダミーフィールドを追加しました。内部的に作成順で記録されていると仮定すると、Text フィールドは文書のほぼ先頭に存在します。それでも、フィールド数に比例した結果になるということは、フィールドにアクセスしたタイミングで、すべてのフィールドの一覧を作成していると考えられます。

アプリの仕様変更を行うと無用となるフィールドが発生することがあります。実害がないので、ついつい残しがちなのですが、パフォーマンスという観点でいうと削除しておいた方がよさそうですね。

なお、今回の検証では、ダミーフィールドのサイズを固定したため、フィールド数 ≒ 文書サイズとなっています。今回の特性の要因が、文書サイズである可能性が残ります。


◇ 逆転の可能性

文書から値を取得することだけに限定すると、D1 ~ D3、E1 ~ E3 までで実現できます。この部分に絞って、結果を整理すると次のようになります。

GetAllDocumentsByKey(青)のグラフは右肩上がりで、GetAllEntriesByKey(オレンジ)は水平です。ダミーフィールドなしでは、ほぼダブルスコアですが、200 個ではずいぶん差が小さくなっています。さらにフィールドを増やしたテストを行えば逆転することがあるかもしれません。

なお、今回の検証では、ダミーフィールドのサイズを 100 バイトにしています。そのため 200 個以上フィールドを増やせませんでした(Summary フィールドの上限)。セットする値を小さくし、フィールド数を増やした場合や Domino 9.0.1 FP8 以降で対応した LargeSummary 環境での特性も気になりますね...


まとめ

今回の検証では、GetAllEntriesByKey で検索し ColumnValues で値を取得する限りにおいては、フィールド数に依存せず同じレスポンスになることがわかりました。逆に NotesDocument オブジェクトを介して値を取得するとフィールド数に比例した処理時間が必要とります。ただ、ここまでの検証では GetAllEntriesByKey の方が早くなるパターンは見つかっておらず、GetAllDocumentsByKey が優勢の状態に変わりはありません。

なお、今回の検証で不明瞭な点は、次の通りです。機会があれば検証したいと思います。

  • もっと大量のフィールドが存在する場合、処理時間が逆転することはあるか?
  • フィールドサイズの大小で変化はあるか?
  • LargeSummary 環境において特性に変化はあるか?


前回 Domino 体力測定


2025/12/31

Domino 体力測定:#2)ビュー操作 ② - 基本的な挙動

背景と目的

今回は GetAllDocumentsByKey と GetAllEntriesByKey の基本的な挙動を理解することを目的にビューを検索するメソッドとした検証を行います。

ビューを検索し結果のコレクションを作成、そこからエントリを取得して、値を取得する一連の処理のどこで処理時間がかかっているのか、特性を理解します。

今回は、最新の PC を使用して測定し、今どきの実環境に近い結果を目指して検証します。


測定環境

検証に使用する環境は次の通りです。参考までに CrystalMark Retro を使用して測定したベンチマーク結果を添付します。

CPU Ryzen 7 9700X ベンチマーク
メモリサイズ 128 GB
ストレージ SSD
OS Windows 11

Server Domino 14.5 FP1
Client Notes 14.5 FP1


◇ テスト DB

前回紹介したテスト DB を Domino サーバ内に配置します。パフォーマンスに関係しそうな情報を整理すると次の通りです。

文書数 100,000 文書
検索ビュー 000000 ~ 099999 の上 3 桁で検索、1 検索当たり 1000 文書がヒット
文書サイズ 13 フィールド、188 バイト
値取得用フィールドは 100 バイト


手順

前回紹介した関数を利用して D1 ~ D3、E1 ~ E5 を順に測定します。関数の引数は次のように設定します。

viFm 0
ビューを ”000” ~ ”019” の合計 20 回検索
作成されるコレクションは毎回 1000 件
viTo 19
viLoop 50 コレクションの前から 50 件を取得

測定のメインルーチンは次のような感じとなります(抜粋)。このプログラムをエージェントに記述してノーツクライアントから実行して測定します。

   '測定用ビュー取得
   Set nv = ndb.GetView("Category3")
   nv.AutoUpdate = False
   Call nv.Refresh()    '索引を更新してから測定

   '測定パラメータ
   iFm = 0
   iTo = 19
   iLoop = 50

   '測定実行
   For iTest = 1 To 10     '測定回数
      'GetAllDocumentsByKey の測定
      dD1 = xTest_D1(nv, iFm, iTo, iLoop)
      dD2 = xTest_D2(nv, iFm, iTo, iLoop)
      dD3 = xTest_D3(nv, iFm, iTo, iLoop)
      'GetAllEntriesByKey の測定
      dE1 = xTest_E1(nv, iFm, iTo, iLoop)
      dE2 = xTest_E2(nv, iFm, iTo, iLoop)
      dE3 = xTest_E3(nv, iFm, iTo, iLoop)
      dE4 = xTest_E4(nv, iFm, iTo, iLoop)
      dE5 = xTest_E5(nv, iFm, iTo, iLoop)

      '測定結果の記録
               ・・・
   Next

測定結果はメッセージボックスで表示するだけでも構いませんが、結果の分析に備えて Excel シートに出力すると便利ですね。


結果

10 回の測定結果から平均値を算出し、棒グラフと積み上げ折れ線グラフで表示します。


ビューを検索して値を取得する検証 3 までの結果を比較すると GetAllDocumentsByKey の方が約 2 倍早い結果となりました。


考察

今回の結果より導くことができるビュー操作の特徴を整理します。


◇ GetAllDocumentsByKey vs GetAllEntriesByKey

結果に記載した通り、GetAllDocumentsByKey 方が 2 倍早い結果となりました。文書を取得する必要がある場合においては、さらに差が開きます。少なくとも今回のテスト環境においては、GetAllDocumentsByKey を使用すべきと言えます。

GetAllEntriesByKey は、ビューのソート順の通りに取得したい、ビューの列値を取得したいという特殊な要件があるときの特殊用途だということだと考えられます。


◇ GetAllDocumentsByKey の挙動

検索結果のコレクションを作成する処理(D1)が最大で、処理時間の 70% を占めていて、残りが値の取得(D3)となっています。コレクションからエントリ(文書)の取得(D2)はほぼ 0 でした。

この結果より、NotesDocumentCollection オブジェクトを作成した時点で内部的に NotesDocument オブジェクトが生成されていることが想定できます。以前の記事 の「検証レポートの要点」で紹介した『GetNextDocument はコレクション内のポインタ移動だけで圧倒的に軽量』の証左と言えますね。


◇ GetAllEntriesByKey  の挙動

GetAllEntriesByKey  では、検索結果のコレクションを作成する処理(E1)とエントリを取得する処理(E2)で処理時間が記録されています。値の取得では NotesViewEntry で保持している ColumnValues プロパティ、要は配列から値を取得するだけなので処理コストはほぼ 0 となるということになります。

NotesViewEntryCollection と NotesViewEntry のオブジェクト作成のコストが非常に高いことがわかります。この結果も 以前の記事 の「検証レポートの要点」と合致します。


◇ NotesViewEntry から文書のアクセス

値を取得する検証 D3 と E5 はほぼ同じ結果でした。NotesDocument から値を取得する処理においては、文書を NotesViewEntry から取得しても同等の結果が得られることがわかります。

ただ、NotesDocument を取得する処理(E4)でも処理時間が記録されています。この結果より、NotesViewEntry のオブジェクト内に NotesDocument は含まれておらず、Document プロパティアクセス時にオブジェクトを生成していることがわかります。

NotesViewEntry は、ビューからの値取得を目的にチューニングされていると解釈すべきだと考えられます。


まとめと次回の検証

今回の検証は、近年一般的な SSD 環境で検証しました(CPU は AMD なので一般的とはいいがたいのかもしれませんが...)。その結果、GetAllDocumentsByKey の方が約 2 倍早いという結果を得ました。

テストケースは、文書数が 10 万件と多いもの、文書内には必要最小限のデータしかなく軽量で、ビューは 2 列だけのシンプルなものでした。この結果だけで実運用で応用できる検証結果とはいいきれません。

そこで、今回の結果をひとつの基準として、今後は、テスト環境やテストケースを変化させることで、処理時間や特性がどのように変化するのか調査したいと思います。

次回は、文書サイズにより特性が変化するのか確認したいと思います。


前回 Domino 体力測定 前回


2025/12/30

Domino 体力測定:#1)ビュー操作 ① - 測定方法

先日の記事『訂正記事:GetAllEntriesByKey は本当に速いのか? - 再検証でわかった“逆転の真実”』 では、ビューを操作するメソッド GetAllDocumentsByKey と GetAllEntriesByKey をパフォーマンスの観点から比較しました。

その中で『GetAllDocumentsByKey が GetAllEntriesByKey より有利』と結論付けましたが、その要因としてストレージの I/O や CPU などの影響が考えられることに触れるにとどまり、具体的な数値までは示していませんでした。そのため「実際にどの程度の差があるのか?」という疑問を持たれた方もいるかと思います。

そこで、この新連載『Domino 体力測定』では、Notes/Domino の機能や性能、挙動などについて、スポーツテストのように測定し、記録していきます。検証は手元の PC を使用して行い、測定条件や環境についても可能な限り明記します。

ビジネスでの実運用環境、とくにクラウド環境や仮想環境では異なる結果となる可能性もありますが、基準となる情報があれば、別の環境であっても仮説を立てることができるかと思います。この記事が何らかの参考になれば幸いです。


ビュー操作の測定

最初の検証は、この連載のきっかけにもなった「ビューの検索や結果の取得のパフォーマンス」です。実験に使用するメソッドは、もちろん GetAllDocumentsByKey が GetAllEntriesByKey です。今回は測定準備として、テスト用 DB とテストデータ(文書)、測定項目と測定プログラム(関数)を紹介します。


テスト項目

まずは測定項目です。以前の記事とほぼ同じですが改めて掲載します。


検証項目 D)GetAllDocumentsByKey E)GetAllEntriesByKey
1 コレクション作成 GetAllDocumentsByKey で検索し、コレクションを取得GetAllEntriesByKey で検索し、コレクションを取得
2 エントリ取得 GetFirstDocument、GetNextDocument で文書を取得GetFirstEntry、GetNextEntry でエントリを取得
3 値の取得 文書から値を取得ColumnValues で値を取得
4 文書の取得 Document プロパティで文書を取得
5 文書から値の取得 文書から値を取得

測定項目は、検索方法のイニシャル(D または E)と検証番号の組み合わせて表現します。例えば、D1 は ”GetAllDocumentsByKey のコレクション作成” となります。

ビューを検索して値を取得する時間の比較であれば、D1 ~ D3 と E1 ~ E3 を比較します。ビューの列にないフィールド値を取得したりフィールド値を更新する場合には、文書が必要となります。このような操作では、D1 ~ D3 と E1, E2, E4, E5 を比較することになります。


テスト DB

検証に使用する DB にはノーツでは少し多めの 10 万文書を用意します。0 ~ 99999 の番号に対して、次のような文書を作成します。

各桁のフィールドやカテゴリの項目は、将来の測定で柔軟にテストできるようにあらかじめ用意しておきます。また、テキストフィールドには値取得用に 100 バイトの文字列を設定します。

Private Function xViewSearch_CreateTestData2(vndb As NotesDatabase)
   Dim l As Long
   Dim s As String
   Dim i As Integer
   Dim nd As NotesDocument

   For l = 0 To 99999    '10万件
      s = Format(l, "000000")

      Set nd = vndb.Createdocument()
      nd.Form = "fTest"

      '検索用の値
      For i = 1 To Len(s)
         Call nd.Replaceitemvalue("Digit_" & CStr(i), Mid(s, Len(s) - i + 1, 1))
         Call nd.Replaceitemvalue("Category_" & CStr(i), Left(s, i))
      Next

      '取得用の値
      s = Right("0000000000" & s, 10)    '10バイト
      s = s & s & s & s & s & s & s & s & s & s     '10 x 10 = 100バイト
      Call nd.Replaceitemvalue("Text", s)

      Call nd.Save(True, True)
   Next
End Function

検索用のビューには、2 列を作成します。1 列目は検索列で Category_3 をソートして配置、2 列目は値取得用の列で Text フィールドを配置します。

例えば、このビューで "012" で検索すると 1000 文書がヒットすることになります。


測定方法

実行時間の測定には Timer 関数を使用します(使い方については過去記事『実行時間の計測(Timer 関数)』を参照)。ただ、この関数は精度が 1/100 秒となっており、処理時間を測定するには粗すぎます。そこで、ループを使って繰り返し実行させて処理時間を稼ぎ、できる限り正確に測定することとします。

各検証項目(D1 ~ D3、E1 ~ E5)毎に測定関数を作成します。各関数のインターフェースは同じとします。

・ 引数

1 vnv NotesView 測定に使用するビュー
・Refresh メソッドで索引更新済み
・AutoUpdate は False
2viFm Integer 検索カテゴリ(開始値)
3viTo  Integer 検索カテゴリ(終了値)
4viLoop Integer コレクションから取得するエントリ数

引数の 2 ~ 4 はループ回数を調整するために使用します。

・ 戻り値

Double 実行時間(秒)


◇ 検証 D1

GetAllDocumentsByKey でビューを検索し、コレクションを取得する時間を測定します。viFm ~ viTo までを順に検索させ、毎回違うコレクションが返ってくるようにしています。

Private Function xTest_D1(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   ReDim ndc(viTo-viFm)

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '計測時間
   xTest_D1 = Timer() - sgST
End Function


◇ 検証 D2

NotesDocumentCollection からエントリを取得する時間を測定します。

計測準備で、検索結果のコレクションを格納用配列 ndc() にあらかじめ取得しておきます。測定開始着、この格納用配列からコレクションにアクセスすることで、エントリ取得時間だけを測定するようにしています。

Private Function xTest_D2(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   Dim nd() As NotesDocument
   ReDim ndc(viTo-viFm)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      '検証 ②:エントリの取得
      Set nd(iCD-viFm, 1) = ndc(iCD).GetFirstDocument()
      For iLoop = 2 To viLoop
         Set nd(iCD, iLoop) = ndc(iCD).GetNextDocument(nd(iCD, iLoop-1))
      Next
   Next

   '計測時間
   xTest_D2 = Timer() - sgST
End Function


◇ 検証 D3

NotesDocument から値の取得時間を測定します。

コレクションごとに viLoop 回取得させます。よって、格納用配列 nd は 2 次元で、測定時のループは 2 重となっています。

Private Function xTest_D3(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   Dim nd() As NotesDocument
   ReDim ndc(viTo-viFm)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nd(iCD-viFm, 1) = ndc(iCD).GetFirstDocument()
      For iLoop = 2 To viLoop
         Set nd(iCD, iLoop) = ndc(iCD).GetNextDocument(nd(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ③:値の取得
         s = nd(iCD, iLoop).Text(0)
      Next
   Next

   '計測時間
   xTest_D3 = Timer() - sgST
End Function


◇ 検証 E1

GetAllEntriesByKey でビューを検索し、コレクションを取得する時間を測定します。D1 とはメソッドが違うだけで、構造は同様になります。

Private Function xTest_E1(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   ReDim nvec(viTo-viFm)

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '計測時間
   xTest_E1 = Timer() - sgST
End Function


◇ 検証 E2

NotesViewEntryCollection から NotesViewEntry オブジェクトを取得する時間を測定します。

Private Function xTest_E2(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '計測時間
   xTest_E2 = Timer() - sgST
End Function


◇ 検証 E3

ColumnValues プロパティを使って、ビューから値を取得する時間の計測です。

Private Function xTest_E3(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ③:値の取得
         s = nve(iCD, iLoop).ColumnValues(1)
      Next
   Next

   '計測時間
   xTest_E3 = Timer() - sgST
End Function


◇ 検証 E4

NotesViewEntry の Document プロパティで文書を取得する時間を測定します。

Private Function xTest_E4(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   Dim nd() As NotesDocument
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ④:文書の取得
         Set nd(iCD, iLoop) = nve(iCD, iLoop).Document
      Next
   Next

   '計測時間
   xTest_E4 = Timer() - sgST
End Function


◇ 検証 E5

NotesViewEntry から取得した NotesDocument 経由でフィールド値を取得する時間を計測します。D3 と比較すれば NotesDocument の取得方法で差があるのかが確認できます。 

Private Function xTest_E5(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   Dim nd() As NotesDocument
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next

      '検証 ④:文書の取得
      For iLoop = 1 To viLoop
         Set nd(iCD, iLoop) = nve(iCD, iLoop).Document
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ⑤:値の取得
         s = nd(iCD, iLoop).Text(0)
      Next
   Next

   '計測時間
   xTest_E5 = Timer() - sgST
End Function


次回の予定

これで測定材料が整いました。これらを使ってビューの検索と値取得の性能調査を順次行います。次回は、GetAllDocumentsByKey と GetAllEntriesByKey 基本的な挙動を調査します。


Domino 体力測定 次回


2025/12/13

訂正記事:GetAllEntriesByKey は本当に速いのか? - 再検証でわかった“逆転の真実”

2025 年 8 月 24 日に投稿した記事『LotusScript でビューを検索するメソッドの違い』で「GetAllDocumentsByKey より GetAllEntriesByKey のほうがパフォーマンス面で有利」と説明しました(以降、”前回の記事”といいます)。

ところが後日、実際に検証を行うとまったく逆の結果が出ました。まさかと思いつつ、改めて HCL テクニカルサポートに問い合わせたところ、私の検証と同じ結論(前回の結果の撤回)が返ってきました。今回の記事では、前回の記事の訂正とともに、両メソッドの特性について最新の知見をまとめておきます。


前回記事の要旨

前回の記事では、両メソッドの特徴を次のようにまとめました

GetAllDocumentsByKey 文書そのものを一つ一つコレクションすることから比較的に遅い
GetAllEntriesByKey ビュー索引を活用する仕様
索引は軽量であり、メモリ消費も少なく高速に動作

2025 年 8 月の Domino Lounge Osaka でこの話をしたところ、同じ理解をされている方が多数いたのでこれが ”定説” だったことがうかがえます。


検証したら...真逆!?

どれぐらいパフォーマンスに差があるのか気になり、簡易的に検証したところ、GetAllEntriesByKey の方が ”数倍遅い!?” という結果がでました。

測定した項目は次の 5 項目です。GetAllEntriesByKey では高速なはずの ColumnValues による値の取得だけでなく、文書オブジェクトを取得して、値を取得するテストも行いました。

シナリオ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 GetAllDocumentsByKey で検索し、コレクションを取得GetAllEntriesByKey で検索し、コレクションを取得
エントリ取得 GetFirstDocument、GetNextDocument で文書を取得GetFirstEntry、GetNextEntry でエントリを取得
値の取得 文書から値を取得ColumnValues で値を取得
文書の取得 Document プロパティで文書を取得
文書から値の取得 文書から値を取得

結果は次の通りで、GetAllDocumentsByKey の方がずいぶん早かったのです。

はじめは、私のテストケース設定が悪く特徴を捕まえ損ねたと考えましが、より明確にメソッドの特性を理解するチャンスとばかりに、検証に使用した NSF(データと検証のコード)をサポートに送付して質問しました。

冒頭に記載した通り、返答は驚きの結果でした。”定説” とは真逆で『GetAllDocumentsByKey の方が高速』とありました。かなり丁寧な検証を実施いただいたようで、複数のテストケースに対して目的、検証手順、結果、考察をまとめた詳細なレポートと検証に利用した NSF が添付されていました。


検証レポートの要点

レポートでは、両メソッドの挙動が処理フェーズごとに整理されていました。

処理フェーズ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 NotesDocumentCollection は内部構造がシンプルで軽量 NotesViewEntryCollection は内部構造が複雑で初期作成コストが高い
エントリ取得 GetNextDocument はコレクション内のポインタ移動だけで圧倒的に軽量 GetNextEntry は移動のたびに複雑なオブジェクトを生成・破棄が発生
値の取得 NotesDocument は読み込むフィールド数に比例してコスト増 NotesViewEntry は列数が増えると内部的な配列生成など処理コストが増加

表を見てわかるように、すべてのフェーズで GetAllDocumentsByKey  が優勢となっています。


”定説” 逆転の背景

HCL サポートの見解では、主に以下の2点の要因が指摘されていました。

I/O コストの低下 かつての HDD 環境では、文書データへのアクセスのようなランダムアクセスは大きな遅延要因であったが、現在の SSD 環境になり劇的に短縮。
ボトルネックの変化I/O コスト低下により処理時間の主たる要因がディスクアクセスからオブジェクト操作などのプログラムの内部処理へ移行
(ストレージの I/O → CPUやメモリ)

文書を開かずビュー索引を使用したほうが早いという ”定説” は、ストレージが HDD だった過去の環境から導き出されたものだったということです。

現在の環境においては、GetAllEntriesByKey の複雑な内部処理が圧倒的に重く、フィールド数が多いなど文書操作が重くなる状況でも逆転することはないとのことでした。


今どきの”定説”

HCL サポートからのレポートは私の簡易的な検証とも一致する結果でした。

現在の一般的な環境では『GetAllDocumentsByKey のほうがパフォーマンス面で有利』という結論です。

前回の記事の内容は、この知見を踏まえて、完全に撤回し訂正いたします。

ちなみに、GetAllEntriesByKey を使う利点は、次のような用途に限られます。

  • ビューのソート順通りに取得したい
  • ビューの列の値にアクセスしたい


検証は大事

今回のような訂正記事を書くハメになったのは、「メーカサポートの回答だから」と無条件に信じてしまったことが原因です。これは ”サポートを信頼できない” と言っているのではありません。自分で検証すると理解が深まり、結果的に応用の幅も広がるということを体験できてよかったと感じました。検証は技術者としてとても重要だと再確認できました。

今回の記事において、パフォーマンスを左右する要素としては

  • ストレージの I/O
  • CPUやメモリ
  • 文書のサイズやフィールド数
  • ビューの列数

があることを知りました。これらがどのように影響するのか、特性を知っておくとより理解が深まります。調査が難しいものありますが、可能な限り検証してまとめたいと思います。


お詫び

ノーツコンソーシアム主催の次世代エース養成ワークショップ(2025 年 7 月、10 月)では、訂正前の内容を紹介しておりました。当時の私にとっては ”新しい知見” だったので、少し得意げに紹介してしまったことを反省しております。

ここでお詫びしても受講者のみなさま全員に届くとは限りませんが、この場を借りてお詫びいたします。今後はより慎重に検証したた上で、研修テキストの作成をしてまいります。


2025/12/07

QR コードの作画:#9)DXL でリッチテキストに表示

LotusScript で QR コードを作画するこの連載は今回が最終回となります。

DXL がビットマップに対応していないことから、暫定策として GIF ファイルに変換する対応をしました。今回は出来上がった GIF ファイルを文書に貼り付ける部分を作成します。


エージェントの作成と関数の追加

前回作成した CreateQRcode_GIF エージェントをコピペして DrawQRcode を作成します。このエージェントに関数を追加して作業を進めます。

リッチテキストフィールドに QR コードをインラインイメージ(見える状態)で貼り付ける処理には DXL を活用しなければなりません。この連載の本題ではないので詳細は割愛しますが、DXL にご興味がある方は以下の連載をご確認ください(一部関数は連載より流用)。


◇ 文書を DXL に変換

既存文書を DXL で操作できるよう変換処理を行う関数です。

Function xGetDOMParser(vnd As NotesDocument) As NotesDOMParser
   'Dominoデータ を DXL に変換する準備
   Dim dexp As NotesDXLExporter
   Set dexp = xns.CreateDXLExporter()
   Call dexp.SetInput(vnd)

   'パーサーに変換する DXL をセット
   Dim dprs As NotesDOMParser
   Set dprs = xns.CreateDOMParser()
   Call dprs.SetInput(dexp)

   'DXL 変換を実行
   Call dexp.Process()

   Set xGetDOMParser = dprs
End Function

この関数は『DXL Step-by-Step:#3)文書を DXL で取得』で紹介しています。


◇ QR コードの表示

リッチテキスト内に QR コードをインラインイメージで貼り付ける関数です。

引数 vsFld で指定された名前のリッチテキストフィールドにセットするのですが、フィールドをいったん削除しています。フィールド内の既存コンテンツはクリアされますので注意してください。

Function xSetDXL_GIF(vdprs As NotesDOMParser, ByVal vsFld As String, ByVal vsFP_GIF As String, viX As Integer, viY As Integer)
   Dim ddn As NotesDOMDocumentNode
   Dim den As NotesDOMElementNode
   Dim denDoc As NotesDOMElementNode
   Dim denItem As NotesDOMElementNode
   Dim denRT As NotesDOMElementNode
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim denGIF As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode
   Dim nst As NotesStream

   Set ddn = vdprs.Document

   'document ノード取得
   Set denDoc = ddn.DocumentElement

   '既存フィールドを削除
   Call xRemoveItemByName(denDoc, vsFld)

   'リッチテキストフィールド作成
   Set den = ddn.CreateElementNode("item")
   Call den.SetAttribute("name", vsFld)
   Set denItem = denDoc.AppendChild(den)

   'リッチテキスト作成
   Set den = ddn.CreateElementNode("richtext")
   Set denRT = denItem.AppendChild(den)

   '段落定義 id='1'
   Set den = ddn.CreateElementNode("pardef")
   Call den.SetAttribute("id", "1")
   Call denRT.AppendChild(den)

   '段落の作成
   Set den = ddn.CreateElementNode("par")
   Call den.SetAttribute("def", "1")
   Set denPar = denRT.AppendChild(den)

   'イメージリソースの追加
   Set den = ddn.CreateElementNode("picture")
   Call den.SetAttribute("align", "baseline")
   Call den.SetAttribute("width", CStr(viX) & "px")
   Call den.SetAttribute("height", CStr(viY) & "px")
   Set denPic = denPar.AppendChild(den)

   '画像の作成
   Set den = ddn.CreateElementNode("gif")
   Set denGIF = denPic.AppendChild(den)

   'GIF をストリームで開く
   Set nst = xns.CreateStream()
   Call nst.Open(vsFP_GIF, "binary")

   '画像の中身
   Set dtn = ddn.CreateTextNode(StreamToBase64(nst))
   Call denGIF.AppendChild(dtn)

   Call nst.Close()
End Function

この関数については合致する記事はありませんが、インラインイメージの貼り付けについては『DXL Step-by-Step:#41)インラインイメージの貼り付け』で触れています。

また、今回は画像サイズは QR コードのサイズであり、事前にわかっています。そこで、画像ファイルから取得するのではなく、関数の引数で受け取り処理を簡略化しています。


◇ 指定したフィールドの削除

xSetDXL_GIF からコールされているサブ関数で、DXL でフィールドを削除する関数です。

Function xRemoveItemByName(vden As NotesDOMElementNode, ByVal vsName As String)
   Dim dn As NotesDOMNode
   Dim den As NotesDOMElementNode
   Dim sName As String
   Dim s As String

   sName = LCase(vsName)

   Set dn = vden.FirstChild
   Do Until dn.Isnull
      If dn.NodeType = DOMNODETYPE_ELEMENT_NODE Then
         If dn.NodeName = "item" Then
            Set den = dn
            s = LCase(den.GetAttribute("name"))

            If sName = s Then
               Call vden.RemoveChild(den)
               Exit Function
            End If
         End If
      End If
      Set dn = dn.NextSibling
   Loop
End Function

この関数の参考記事は現時点でありません。フィールドである item ノードから名前が一致するものを探し出し削除しています。


◇ 画像のエンコード

こちらも xSetDXL_GIF からコールされているサブ関数です。DXL 内の画像は Base64 でエンコードしておく必要があります。そのエンコード処理を行う関数です。

'OpenNTF LotusScript Gold Collection より拝借(StreamToBase64)
Function StreamToBase64(streamIn As NotesStream) As String
   Dim s As String

   On Error GoTo theOldWay
   ' ReadEncoded function is not documented. In case it doesn't work have a backup.
   s = streamIn.ReadEncoded(ENC_BASE64, 76)
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
   Exit Function

theOldWay:
   Dim session As New NotesSession
   Dim db As NotesDatabase
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body")
   streamIn.Position = 0
   Call mime.SetContentFromBytes(streamIn, "image/gif", ENC_NONE)
   mime.EncodeContent(ENC_BASE64)
   s = mime.ContentAsText
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
End Function

コメントに記載した通り、 OpenNTF の LotusScript Gold Collection プロジェクトより拝借した関数です。『DXL Step-by-Step:#10)イメージリソースの新規作成』で紹介しています。


◇ 文書の保存

最後の関数は文書を保存する関数です。

Function DXL_Import(vdprs As NotesDOMParser, ByVal viOption As Integer, ByVal vbIsDesign As Boolean) As Boolean
   Dim nst As NotesStream
   Dim ndb As NotesDatabase
   Dim dimp As NotesDXLImporter

   On Error GoTo Err_Proc

   'DXL の抽出準備
   Set nst = xns.CreateStream()
   Call vdprs.SetOutput(nst)
   Call vdprs.Serialize()

   '保存(インポート)
   Set ndb = xns.CurrentDatabase
   Set dimp = xns.CreateDXLImporter()
   If vbIsDesign = True Then
      '設計の保存
      dimp.DesignImportOption = viOption
   Else
      '文書の保存
      dimp.DocumentImportOption = viOption
   End If

   'DXL の保存
   Call dimp.Import(nst.ReadText(), ndb)
   DXL_Import = True

Exit_Proc:
   Exit Function

Err_Proc:
   MsgBox Error$, 16, "DXL_Import"
   DXL_Import = False

   Resume Exit_Proc
End Function

この関数は『DXL Step-by-Step:#23)サンプルコード(段落と文字の装飾①)』で紹介しています。


メインルーチンの修正

エージェントの Initialize を修正し、今回作成した QR コード表示機能を追加します。

Sub Initialize
                  ・・・
   Dim iX As Integer    '画像の幅
   Dim iY As Integer    '画像の高さ
   Dim dprs As NotesDOMParser
                  ・・・
      '② GIF ファイルの作成
      Call DrawQR_GIF(abQR, "c:\tmp\QR.gif")

      '③ リッチテキストにインラインで貼り付け
      Set dprs = xGetDOMParser(nd)
      iX = UBound(abQR, 1) + 1    '画像の幅
      iY = UBound(abQR, 2) + 1    '画像の高さ
      Call xSetDXL_GIF(dprs, "QRcode", "c:\tmp\QR.gif", iX, iY)
      Call DXL_Import(dprs, 5, False)    '文書を更新
   End If
End Sub


ビューの修正と動作検証

エージェントが完成したら、アクションボタンをビューに追加します。

ビューを保存後、動作検証します。正常に実行されると冒頭の画像のように QR コードが表示されます。


まとめ

今回の連載は、LotusScript だけを使って QR コードをビットマップ画像として描画する方法について解説しました。あわせて、ビットマップ画像のフォーマット仕様についても整理し、仕組みを理解しながら作り上げる流れをご紹介しました。

本来であれば、できる限り幅広い環境で動作させるために LotusScript だけで完結させることを目指していましたが、現状の DXL ではビットマップに対応していないため、最終的な GIF 変換には Windows の機能に頼らざるを得ない部分がありました。その結果、完全に LotusScript だけで完結する構成にはできなかった点は少し心残りです。

機会があれば GIF フォーマットのファイルを LotusScript で生成することにも挑戦してみたいと思います。仕様が理解できたらという制約はありますが...


前回 QR コードの作画