#ベストプラクティス

0 フォロワー · 44 投稿

InterSystemsデータプラットフォーム上でソリューションをより適切に開発、テスト、展開、管理する方法に関するベストプラクティスの推奨事項。

記事 Toshihiko Minamoto · 5月 18, 2021 12m read

第2部: インデックス処理

クラスにどのようなインデックスが必要であるのか、それをどのように定義するのかについて理解できたので、 次に、どのように処理するのかについて確認しましょう。

クエリプラン

注意: クラスに変更を適用する場合と同様に、ライブシステムにインデックスを追加する場合にもリスクが伴います。インデックスが入力されているときに、ユーザーがデータにアクセスしたり更新したりすると、クエリ結果が空になったり誤った結果が生じることがあります。また、構築中のインデックスが破損する場合もあります。 ライブシステムでインデックスを定義したり使用したりするには追加の手順があり、それについてはこのセクションで触れていますが、詳細はドキュメントに記載されています。)

新しいインデックスの準備ができたら、SQLオプティマイザが、クエリを実行する上で最も効率的に読み取れるインデックスであると判断するかどうかを確認できます。 プランを確認するために実際にクエリを実行する必要はありません。 クエリがあれば、プランをプログラムで確認することができます。

            Set query = 1

     Set query(1) = “SELECT SSN,Name FROM Sample.Person WHERE Office_State = 'MA'”

D $system.SQL.ShowPlan(.query)

また、システムエクスプローラー -> SQLより、システム管理ポータルのインターフェースに従って確認することもできます。

ここから、どのインデックスが、テーブルデータ(または「マスターマップ」)をロードする前に使用されているのかを確認できます。 プランの新しいクエリは期待どおりに動作しているのか、 プランのロジックが合理的であるのかを検討しましょう。

SQLオプティマイザが作成したインデックスを使用していることを確認できたら、これらのインデックスが適切に機能しているかどうかを確認できます。

インデックスの構築

(現時点では計画しているだけの段階であり、データが存在しない場合は、ここに説明されている手順は必要ありません。)

インデックスを定義しても、テーブルのデータが自動的に入力または「構築」されるわけではありません。 まだ構築されていない新しいインデックスをクエリプランに使用すると、クエリ結果が誤りとなったり空になったりするリスクがありますが、 マップの選択可能性を0に設定すると、インデックスの使用準備が整う前にそのインデックスを「無効」にすることができます。0に設定するというのは、基本的に、SQLオプティマイザに、クエリの実行にはそのインデックスを使用できないことを指示していることになります。

write $SYSTEM.SQL.SetMapSelectability("Sample.Person","QuickSearchIDX",0) ; Set selectability of index QuickSearchIDX false

上記の呼び出しは、新しいインデックスを追加する前でも使用できることに注意してください。 SQLオプティマイザは、この新しいインデックスの名前と、それが非アクティブであることを認識するため、クエリには使用しません。

インデックスの入力は、 %BuildIndices メソッド(##class(<class>).%BuildIndices($lb("MyIDX")))を使用するか、システム管理ポータルのSQLページ(アクションドロップダウン)で行うことができます。

インデックスの構築にかかる時間は、テーブル内の行数とインデックスの種類によって異なります。通常、bitsliceインデックスの作成には時間が掛かります。

このプロセスが完了したら、もう一度SetMapSelectivityメソッドを使用して、新たに入力されたインデックスを有効にできます(この場合は 1 に設定)。

インデックスを作成するというのは、基本的に、KILLコマンドとSETコマンドを発行して、インデックスを入力することです。 そのため、このプロセスを実施する間は、ディスク領域がいっぱいにならないように、ジャーナリングを無効にすることを検討してください。  

新しいインデックスと既存のインデックスを作成する詳細な手順について、特にライブシステムでの手順については、以下にリンクされているドキュメントをご覧ください。

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_build_readwrite

インデックスの管理

ようやく、実際にインデックスを使用できるようになりました。 クエリがどれほど効率的に実行するのか、ある特定のインデックスがどれくらいの頻度で使用されるのか、そしてインデックスの一貫性が崩れた場合にどのように対処するのかといったことを考慮する必要があります。

パフォーマンス

まず、このインデックスがクエリのパフォーマンスにどのような影響を与えたのかを検討できます。 SQLオプティマイザがクエリに新しいインデックスを使用していることがわかっている場合は、クエリを実行して、そのパフォーマンスの統計(グローバル参照数、読み取られた行数、クエリの準備時間と実行時間、およびディスクで費やされた時間)を収集することができます。

前の例に戻りましょう。

SELECT SSN,Name,DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J'

このクエリのパフォーマンスを支援するために、次のインデックスがあります。

Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];

NameIDXインデックスは、すでにNameプロパティにあります。

直感的には、クエリはQuickSearchIDXを使って実行していることがわかります。NameIDXは、Nameプロパティに基づいていますが、SSNまたはDOBのデータ値が含まれていないため、2番目の選択肢である可能性があります。

QuickSearchIDXを使ったこのクエリのパフォーマンスは、実行するだけで簡単に確認できます。

(余談ですが、パフォーマンスの違いを分かりやすくするために、クエリを実行する前に、クエリキャッシュを消去しています。 SQLクエリが実行されると、次回実行時のパフォーマンスを改善できるように、実行に使用されたプランが保存されます。この記事の目的から外れてしまうため詳しい説明はしませんが、SQLパフォーマンスに関するその他の考慮事項として、この記事の最後に資料を記載しておきます。)

Row count: 31 Performance: 0.003 seconds  154 global references 3264 lines executed 1 disk read latency (ms)

(率直に)クエリの実行に必要なのは、QuickSearchIDXのみです。

QuickSearchIDXではなくNameIDXを使ったパフォーマンスを比較して見ましょう。これは、クエリキーワードである%IGNOREINDEXを追加して行います(SQLオプティマイザが特定のインデックスを選択できないようにします)。

クエリを次のように記述します。

SELECT SSN,Name,DOB FROM %IGNOREINDEX QuickSearchIDX Sample.Person WHERE Name %STARTSWITH 'Smith,J'

クエリプランはNameIDXを使用するようになったので、インデックスを介して見つかった関連する行IDを使って、Sample.Personのデータグローバル(または「マスターマップ」)から読み取る必要があることがわかります。

Row count: 31 Performance: 0.020 seconds  137 global references 3792 lines executed 17 disk read latency (ms) 

実行に必要な時間、実行される行数、およびディスクのレイテンシが増加しているのがわかります。

次に、インデックスを全く使用せずに、このクエリを実行してみましょう。 クエリを次のように調整します。

SELECT SSN,Name,DOB FROM %IGNOREINDEX * Sample.Person WHERE Name %STARTSWITH 'Smith,J'

インデックスを使用しない場合、条件を満たすかどうか、データの行を確認する必要があります。

Row count: 31 Performance: 0.765 seconds  149999 global references 1202681 lines executed 517 disk read latency (ms)

特殊なインデックスであるQuickSearchIDXが、インデックスを使用しない場合よりも100倍以上速く、また一般的なNameIDXを使用した場合よりもほぼ10倍速く、クエリを実行できています。さらに、NameIDXを使用した場合は、インデックスを全く使用しない場合よりも30倍以上速く実行しています。

この特定の例では、QuickSearchIDXとNameIDXのパフォーマンスの差はわずかですが、数百万行に対して実行するクエリを1日に数百回実行するのであれば、貴重な時間を節約できることがわかるでしょう。

SQLUtilitiesを使った既存のインデックスの分析

%SYS.PTools.SQLUtilitiesには、IndexUsage、JoinIndices、TablesScans、TempIndicesなどのプロシージャが含まれています。 これらは、特定のネームスペースにある既存のクエリを分析し、特定のインデックスが使用される頻度、テーブルの各行で反復を選択しているクエリ、そしてインデックスをシミュレートする一時ファイルを生成しているクエリに関する情報を報告します。

これらのプロシージャを使用することで、インデックスが対処できる可能性があるギャップと、使用されていないまたは非効率であるために削除することを検討した方がよいインデックスを特定することができます。

これらのプロシージャと使用例についての詳細は、以下のクラスに関するドキュメントをご覧ください。

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_optquery_indexanalysis

インデックスに関する問題?

インデックスを検証することで、インデックスが存在し、クラスの各行で正しく定義されているのかを確認できます。 インデックスが破損した状態になるクラスはありませんが、クエリが空の結果セットや誤った結果セットを返しているようであれば、クラスの既存のインデックスが現在有効であるかを確認することをお勧めします。

インデックスは、プログラムで次のように検証できます。

                Set status = ##class(<class>).%ValidateIndices(indices,autoCorrect,lockOption,multiProcess)  

ここで、インデックスパラメーターはデフォルトで空の文字列です。つまり、すべてのインデックスかインデックスの名前を含む$listbuildオブジェクトを検証します。

autoCorrectは、デフォルトで0になることに注意してください。 1である場合、検証プロセスで発生するすべてのエラーは修正されます。 機能的にはインデックスを再構築することに変わりませんが、ValidateIndicesのパフォーマンスは比較的に遅くなります。

詳細は、%Library.Storageクラスのドキュメントをご覧ください。

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Library.Storage#%ValidateIndices

インデックスの削除

あるインデックスが不要になった場合、またはテーブルに大規模な変更を行って、後で関連するインデックスを構築する際にパフォーマンスへの影響がないようにする場合は、Studioでインデックスの定義をクラスから取り除き、該当するインデックスグローバルノードを削除することができます。  または、DDLを介してDROP INDEXコマンドを実行することもできます。このコマンドでも、インデックスの定義とデータを消去することができます。 その後、キャッシュ済みクエリをパージすることで、確実に、削除されたインデックスが既存のプランによって使用されなくなります。

さて今度は?

インデックスは、SQLパフォーマンスの一部にしかすぎません。 この流れに並行し、インデックスのパフォーマンスと使用状況を監視するオプションは他にもあります。 SQLパフォーマンスを理解するには、次のことについても学ぶことをお勧めします。

Tune Tables - テーブルに代表的なデータが入力されてから、またはデータの分布が大幅に変化した場合に実行するユーティリティです。 このユーティリティは、クラスの定義に、フィールドの長さや1つフィールドに含まれる一意の値の数など、SQLオプティマイザが効率的に実行するためにクエリプランを選択しやすくするメタデータを提供します。

Kyle Baxterが、これに関する記事を執筆しています(https://community.intersystems.com/post/one-query-performance-trick-you-need-know-tune-table)。 

クエリプラン - 基盤のコードがSQLクエリをどのように実行するか論理的に表現したプランです。 クエリが遅い場合、どのクエリプランが生成されているのか、クエリに適しているのか、このクエリをさらに最適化するために何を行えるのかを検討することができます。

キャッシュドクエリ - 準備済みの動的SQLステートメント - キャッシュドクエリは、基本的にクエリプランの下にあるコードです。

参考文献

インデックスの定義と構築に関するドキュメント。 読み取り/書き込みのライブシステムで検討する必要のある追加手順が含まれています。 https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices

ISC SQLコマンド – DDLを介したインデックス処理の構文関連資料について、CREATE INDEXとDROP INDEXを参照してください。 これらのコマンドを実行するための適切なユーザー権限が含まれています。 https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_COMMANDS

ISCクラスでのSQL照合に関する詳細。 デフォルトでは、文字列の値は、インデックスグローバルにSQLUPPER (“ STRING”) として格納されます。

https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_collation

[最終更新日: 2020年5月6日 - インデックス構築パフォーマンスとDROP INDEXコマンドの修正。]

0
0 365
記事 Toshihiko Minamoto · 5月 12, 2021 12m read

これは、SQLインデックスに関する2部構成の記事の前半です。

第1部 - インデックスを理解する

インデックスとは?

最後に図書館に行った時のことを思い出してください。 通常そこには、分野別(そして作者順と題名順)に整理された本が並び、それぞれの棚には、本の分野を説明したコードが記載された本立てがあります。 特定の分野の本を収集する場合、すべての通路を歩いて一冊ずつ本の表紙を読む代わりに、目的の分野の本棚に直接向かって選ぶことができるでしょう。

SQLインデックスにもこれと同じ機能があります。テーブルの各行にフィールドの値へのクイック参照を提供することで、パフォーマンスを向上させています。

インデックスの設定は、最適なSQLパフォーマンスを得られるようにクラスを準備する際の主なステップの1つです。

この記事では、次のことについて説明します。

  1. インデックスとは何か。いつ、なぜそれを使用するか。
  2. どのようなインデックスが存在するか、どのようなシナリオに適しているのか。
  3. インデックスの例
  4. 作成方法
  • インデックスが存在する場合、どのように扱うのか。
  • この記事では、Sampleスキーマのクラスを参照します。 このスキーマは以下に示すGitHubリポジトリにあります。また、CachéとEnsembleでインストールされるSamplesネームスペースでも提供されています。

    https://github.com/intersystems/Samples-Data

    基本

    永続プロパティと、永続データから確実に計算されるプロパティにインデックスを作成できます。

    Sample.CompanyのTaxIDプロパティにインデックスを作成するとしましょう。 StudioまたはAtelierで、以下のコードをクラス定義に追加します。

                    Index TaxIDIdx On TaxID;

    これに相当するDDL SQLステートメントは、次のようになります。

                    CREATE INDEX TaxIDIdx ON Sample.Company (TaxID);

    デフォルトのグローバルインデックス構造は、次のようになります。

                    ^Sample.CompanyI("TaxIDIdx",<TaxIDValueAtRowID>,<RowID>) = ""

    通常のデータグローバルのフィールドより、読み取るサブスクリプトが少ないところに注目してください。

    SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349'」というクエリを見てみましょう。 論理的に単純なクエリです。このクエリを実行するためのクエリプランは、これを反映しています。

    このプランは基本的に、指定されたTaxID値を持つ行のインデックスグローバルをチェックし、データグローバル(「マスターマップ」)を参照して一致する行を取得するように指定しています。

    ここで、同じクエリを、TaxIDXにインデックスを使わずに考察してみましょう。 クエリプランの効率は、予想どおり、低下します。

    インデックスがない場合、IRISの基盤のクエリ実行は、メモリを読み取って、テーブルの各行にWHERE句の条件を適用します。論理的に言って、TaxIDを共有する会社はないと思うため、この作業をたった1行のためだけに行っているのです!

    もちろん、インデックスを使用するということは、インデックス行データがディスクにあるということですので、 条件の内容とテーブルに含まれるデータの量によっては、インデックスを作成してデータを入力する際に、それ固有の問題が生じる可能性もあります。

    では、プロパティにはいつインデックスを追加すればよいのでしょうか。

    一般的なケースとしては、あるプロパティを頻繁に条件とする場合が挙げられます。 例として個人のSSN(社会保障番号)または銀行口座番号といった識別情報があります。 また、生年月日や口座の資金も考慮できるでしょう。  Sample.Companyに戻ると、高収益の組織に関するデータを収集する場合は、およらくRevenueプロパティのインデックスを作成するとメリットがあるかもしれません。 逆に、条件を付ける可能性が低いプロパティは、インデックス作成にあまり適していません。会社のスローガンや説明などです。

    インデックスの種類も検討する必要がある場合を除けば、「シンプル イズ ベスト」なのです!

    インデックスの種類

    ここでは、6つの主要なインデックスの種類について説明します。標準、ビットマップ、複合、コレクション、ビットスライス、およびデータです。 また、ストリームに基づくiFindインデックスについても簡単に説明します。 上記の例で、標準のインデックスについてすでに触れているため、内容が重複するかもしれません。

    クラス定義にインデックスを作成する方法の例をいくつか紹介しますが、新しいインデックスをクラスに追加するというのは、クラス定義に行を追加するだけではありません。 その他の考慮事項については、この記事の第2部で説明します。

    それでは、例としてSample.Personを使用しましょう。 PersonにはサブクラスのEmployeeがあることに注目してください。いくつかの例を理解する上で関わってくることです。 EmployeeはデータグローバルストレージをPersonと共有し、PersonのすべてのインデックスはEmployeeに継承されます。つまり、Employeeはこれらの継承されたインデックスに、Personのインデックスグローバルを使用しているということです。

    これらのクラスにあまり詳しくない場合のために簡単に説明すると、Personには、SSN、DOB、Name、Home(StateとCityを含む埋め込みAddressオブジェクト)、およびOffice(Address)プロパティがあり、FavoriteColorsというリストコレクションがあります。 Employeeには、さらに私が定義したSalaryプロパティもあります。

    標準

    Index DateIDX On DOB;

    ここでは、「標準」を大まかに使用して、プロパティのプレーンな値(2進数表現ではなく)を格納するインデックスを参照しています。 値が文字列である場合、照合順序(デフォルトではSQLUPPER)で格納されます。

    ビットマップやビットスライスのインデックスに比べ、標準のインデックスは人間が読み取れる形式であり、比較的簡単に管理することができます。 グローバルノードは、テーブルの各行に1つずつあります。

    以下に、DateIDXがグローバルレベルでどのように格納されるのかを示しています。

    ^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81

    インデックスの名前の後にある最初のサブスクリプトは日付値で、最後のサブスクリプトはそのDOBを持つPersonのID、そしてグローバルインデックスに格納された値は、このPersonがサブクラスSimple.Employeeのメンバーであることを示していることに注目してください。 このPersonがどのサブクラスのメンバーでもない場合、ノードの値は空の文字列になります。

    この基本構造は、ほとんどの非ビットインデックスと一致します。この場合、複数のプロパティのインデックスはグローバルでサブスクリプトをさらに作成し、ノードに複数の値が格納されると、$listbuildオブジェクトが生成されます。以下はその例です。

                    ^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2)

    ビットマップ - プロパティの値に対応するIDセットのビット単位の表現。

    Index HomeStateIDX On Home.State [ Type = bitmap];

    ビットマップインデックスは、行ごとに格納される標準のインデックスとは対照的に、ユニーク値ごとに格納されます。

    上記の例をさらに詳しく見てみましょう。ID 1のPersonがマサチューセッツ州(MA)に、ID 2がニューヨーク州(NY)に、ID 3がマサチューセッツ州(MA)に、そしてID 4がロードアイランド州(RI)に住んでいるとします。 HomeStateIDXは基本的に次のように格納されます。

    ID

    1

    2

    3

    4

    (…)

    (…)

    0

    0

    0

    0

    -

    MA

    1

    0

    1

    0

    -

    NY

    0

    1

    0

    0

    -

    RI

    0

    0

    0

    1

    -

    (…)

    0

    0

    0

    0

    -

    ニューイングランド州にする人のデータを返すクエリが必要な場合、システムはビットマップインデックスの関連する行にビット単位のORをを実行します。 少なくとも、ID 1、3、および4のPersonオブジェクトをメモリに読み込む必要があることにすぐに気づくでしょう。

    ビットマップは、WHERE句のAND、RANGE、またはOR演算子に対して効率的です。 

    ビットマップインデックスの効率が標準インデックスより低くなる前にプロパティに指定できるユニーク値の数に、公式の上限はありませんが、一般的には、最大10,000個程度の値が経験則とされています。 そのため、ビットマップインデックスは、米国の州において効果的であっても、米国の市または郡のビットマップインデックスにはあまり有用とは言えません。

    また、ストレージの効率についても、考慮する必要があります。 テーブルへの行の追加や行の削除を頻繁に行う予定であれば、ビットマップインデックスのストレージはあまり効果的ではありません。 上記の例を考察してみましょう。何らかの理由で多数の行を削除し、ワイオミング州やノースダコタ州といった人口の少ない州に住む人がテーブルから消えたとします。 つまり、ビットマップには、ゼロのみの行がいくつか存在することになります。 一方で、ビットマップストレージが大きければ、より多くのユニーク値を格納しなければならなくなるため、大型のテーブルに新しい行を作成していけば、いずれは減速していく可能性があります。

    これらの例では、Sample.Personに約150,000行があります。 各グローバルノードには、最大64,000個のIDが格納されるため、ビットマップインデックスグローバルは、MAで3つの部分に分割されます。

          ^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...))

    ^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…))

    ^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…))

    特殊ケース: エクステントビットマップ 

    $<ClassName>とされることの多いエクステントビットマップは、クラスのIDにおけるビットマップインデックスです。IRISはこれを使って行が存在するのかをすばやく検出し、COUNTクエリまたはサブクラスのクエリに役立てています。 これらのインデックスは、ビットマップインデックスがクラスに追加される際に自動的に生成されますが、次のように、クラス定義にビットマップエクステントインデックスを手動で作成することも可能です。


    Index Company [ Extent, SqlName = "$Company", Type = bitmap ];

    DDLのBITMAPEXTENTキーワードを使うこともできます。

    CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company

    複合 - 2つ以上のプロパティに基づくインデックス

    Index OfficeAddrIDX On (Office.City, Office.State);

    複合インデックスは通常、2つ以上のプロパティを条件とするクエリが頻繁に発生する場合に使用できます。

    インデックスはグローバルレベルで格納されるため、複合インデックスでは、プロパティの順序が重要になります。 インデックスグローバルの最初のディスク読み取りは保存されるため、選択する頻度の高いプロパティを最初に指定すると、高いパフォーマンス効率を得ることができます。この例では、米国の州の数より都市の数の方が多いため、Office.Cityが最初に指定されています。

    あまり選択しないプロパティを最初に指定すると、スペースの効率性が高くなります。 グローバル構造に焦点を当てれば、Stateを最初に指定すると、インデックスツリーのバランスがより高まります。 考えてみれば、各州には多数の市がありますが、1つの州にしか存在しない市もあるのです。

    また、いずれかのプロパティのみを条件としたクエリを頻繁に実行するのかどうかを検討することもお勧めします。別のインデックスを定義する手間を省けるからです。

    複合インデックスのグローバル構造の例を以下に示します。

    ^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~"

    余談: 複合インデックスかビットマップインデックスか

    複数のプロパティで条件付けするクエリの場合、個別のビットマップインデックスを使った方が1つの複合インデックスよりも効果的かどうかを検討することもできます。

    ビットマップインデックスが各プロパティに適切に適合するのであれば、2つの異なるインデックスに対してビット演算した方が効率的になる可能性があります。

    複合ビットマップインデックスを作成することもできます。これらはユニーク値が、インデックスを作成している複数のプロパティの共通した値となるビットマップインデックスです。 前のセクションで示したテーブルを考察してみましょう。ただし、州の代わりに、州と市のすべての可能な組み合わせ(マサチューセッツ州ボストン、マサチューセッツ州ケンブリッジ、マサチューセッツ州ロサンゼルスなど)を用いたテーブルです。両方の値に適合する行のセルは1となります。

    コレクション - コレクションプロパティに基づくインデックス

    次のように定義されたFavoriteColorsプロパティがあります。

    Property FavoriteColors As list Of %String;

    実演の目的で、インデックスは次のように定義されています。

    Index fcIDX1 On FavoriteColors(ELEMENTS);
    Index fcIDX2 On FavoriteColors(KEYS);

    ここでは、複数の値を含む単一セルのプロパティをより広く参照するために、「コレクション」を使用しています。 ここでは、List OfとArray Ofプロパティが重要で、必要に応じて区切り付きの文字列も指定できます。

    コレクションプロパティは自動的に解析され、インデックスが構築されます。 電話番号などの区切り付きのプロパティでは、このメソッドを明示的に <PropertyName>BuildValueArray(value, .valueArray) と定義する必要があります。

    上記のFavoriteColorsの例で考えると、お気に入りの色が青と白であるPersonのfcIDX1は、次のようになります。

    ^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~"

    (…)

    ^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~"

    そしてfcIDX2は次のようになります。

             ^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~"      

    ^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~"

    この場合、FavoriteCoslorsはListコレクションであるため、キーに基づくインデックスの有用性は、要素に基づくインデックスよりも低くなります。

    コレクションプロパティのインデックスの作成と管理に関するより詳しい考慮事項については、ドキュメントをご覧ください。

    https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_collections

    ビットスライス - 数値データのビット文字列表現のビットマップ表現


    Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee

    ビットスライスインデックスは、どの行に特定の値が含まれるかを表すフラグを含むビットマップインデックスとは異なり、最初に数値を10進数から2進数に変換し、その後で2進数値の各桁にビットマップを作成します。

    上記の例を見てみましょう。現実的に考えられるよう、Salaryを$1000単位として単純化します。つまり、従業員の給与が65であれば、65,000ドルということになります。

    ID 1のEmployeeのSalaryを15、ID 2のSalaryを40、ID 3のSalaryを64、ID 4のSalaryを130とします。 この場合、対応するビット値は次のようになります。

    15

    0

    0

    0

    0

    1

    1

    1

    1

    40

    0

    0

    1

    0

    1

    0

    0

    0

    64

    0

    1

    0

    0

    0

    0

    0

    0

    130

    1

    0

    0

    0

    0

    0

    1

    0

    ビット文字列は8桁を超えています。 対応するビットマップ表現(ビットスライスインデックス値)は、基本的に次ように格納されます。

    ^Sample.PersonI("SalaryIDX",1,1) = "1000" ; Row 1 has value in 1’s place

    ^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Rows 1 and 4 have values in 2’s place

    ^Sample.PersonI("SalaryIDX",3,1) = "1000" ; Row 1 has value in 4’s place

    ^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Rows 1 and 2 have values in 8’s place

    ^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc…

    ^Sample.PersonI("SalaryIDX",6,1) = "0100"

    ^Sample.PersonI("SalaryIDX",7,1) = "0010"

    ^Sample.PersonI("SalaryIDX",8,1) = "0001"

    Sample.Employeeまたはその行の給与を変更する演算(INSERT、UPDATES、DELETE)では、これらの各グローバルノードまたはビットスライスを更新する必要があることに注意してください。 ビットスライスインデックスをテーブルの複数のプロパティまたは頻繁に変更されるプロパティに追加すると、パフォーマンスにリスクが生じる可能性があります。 一般的に、ビットスライスインデックスの管理には、標準またはビットマップインデックスの管理よりもコストがかかります。

    ビットスライスインデックスは非常に特殊であるため、ユースケースも特殊であり、SUM、COUTN、またはAVGなどの集計計算を実行する必要のあるクエリで使用します。

    さらに、数値に対してのみ効果を発揮するため、文字列は2進数の0に変換されます。

    クエリの条件をチェックするために、インデックスではなくデータテーブルを読み取る必要がある場合、クエリの実行にビットスライスインデックスは選択されません。 Sample.PersonのNameにインデックスがないとします。 Smithという姓の従業員の平均給与を計算する場合(SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' )、WHERE条件を適用するためにデータ行を読み取る必要があるため、ビットスライスは実際には使用されません。

    行が頻繁に作成または削除されるテーブルのビットスライスとビットマップインデックスについても、同様のストレージに関する懸念があります。

    データ - グローバルノードに格納されているデータのインデックス。

    Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];

    前のいくつのかの例で、「~Sample.Employee~」という文字列がノード自体に値として格納されていることに気づいたかもしれません。 Sample.Employeeは、Sample.Personからインデックスを継承していることを思い出してください。 特にEmployeesをクエリする場合、プロパティ条件に一致するインデックスノードの値を読み取り、そのPersonがEmployeeでもあることを確認します。

    また、格納する値を明示的に定義することもできます。 インデックスグローバルノードでデータを定義すると、データグローバルの読み取りも保存できます。頻繁な選択クエリや順序付けクエリに役立ちます。

    上記のインデックスを例とすると、 名前の全部または一部を指定された人物に関する識別情報を取得する場合(フロントデスクアプリケーションでクライアントの情報を検索する場合など)、「SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name」というクエリを実行できます。 Nameをクエリ条件としており、取得しようとしている値はすべてQuickSearchIDXグローバルノード内に格納されているため、このクエリを実行するには、グローバルのみを読み取る必要があります。

    データ値は、ビットマップまたはビットスライスインデックスと保存できないことに注意してください。

    ^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.")

    iFindインデックス

    このようなインデックスを聞いたことがあるでしょうか? 私にもありません。iFindインデックスは、ストリームプロパティで使用されますが、これを使用するには、クエリにキーワードで名前を指定する必要があります。

    もう少し説明することもできますが、このことについては、Kyle Baxterがすでに有用な記事を執筆しています。

    フリーテキスト検索:SQL開発者が秘密にしているテキストフィールドの検索方法 

    [最終更新日: 2020年4月16日 - 可読性を調整。]

    0
    0 579
    記事 Toshihiko Minamoto · 5月 4, 2021 2m read

    ObjectScriptには、エラー(ステータスコード、例外、SQLCODEなど)を処理する方法が少なくとも3つあります。 ほとんどのシステムコードにはステータスが使用されていますが、例外は、いくつかの理由により、より簡単に処理することができます。 レガシーコードを使用している場合、さまざまな手法の変換にいくらか時間が掛かりますが、 参考として、次のスニペットをよく使用しています。 皆さんのお役にも立てればと思います。

    ///SQLCODEのステータス:set st = $$$ERROR($$$SQLError, SQLCODE, $g(%msg))  //埋め込みSQLset st = $$$ERROR($$$SQLError, rs.%SQLCODE, $g(rs.%Message)) //動的SQL///SQLCODEの例外:throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) //埋め込みSQLthrow ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) //動的SQLthrow:(SQLCODE'=0)&&(SQLCODE'=100) ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) //クエリに成功する場合、またはデータがない場合はスローしない///ステータスの例外:$$$ThrowOnError(st)///例外のステータス:set st = err.AsStatus()///カスタムエラーステータスの作成:set st = $$$ERROR($$$GeneralError,"Custom error message")///カスタム例外をスロー:$$$ThrowStatus($$$ERROR($$$GeneralError,"Custom error message"))///ステータス付きのSOAPエラーの処理:try {  //SOAPリクエストコード} Catch err {  If err.Name["ZSOAP" {    Set st = %objlasterror  } Else {    Set st = err.AsStatus()  }}return st///カスタム例外クラスを定義Class App.Exceptions.SomeException Extends %Exception.AbstractException{Method OnAsStatus() As %Status{  return $$$ERROR($$$GeneralError,"Custom error message")}}///カスタム例外のスローとキャッチtry {  throw ##class(App.Exceptions.SomeException).%New()} catch err {  if err.%ClassName(1) = ##class(App.Exceptions.SomeException).%ClassName(1) {    //この種の例外に特有の処理  }}</pre></body></html>
    0
    0 588
    記事 Toshihiko Minamoto · 4月 28, 2021 5m read

    数年ほど前、Caché Foundationsの講座(現「Developing Using InterSystems Objects and SQL」)において、%UnitTestフレームワークの基礎を講義していたことがあります。 その時、ある受講者から、ユニットテストを実行している間に、パフォーマンス統計を収集できるかどうかを尋ねられました。 それから数週間後、この質問に答えるために、%UnitTestの例にコードを追加したのですが、 ようやく、このコミュニティでも共有することにしました。

    Processクラスの%SYSTEMには、プロセスについて収集可能なメトリクスが(所要時間以外に)いくつか提供されています。

    • 所要時間
    • 実行された行
    • グローバル参照
    • システムCPU時間
    • ユーザーCPU時間
    • ディスク読み取り時間
    上記の統計を収集する任意のユニットテストを有効にするには、%UnitTest.TestCaseのサブクラスを作成してプロパティを追加します。
     
    Class Performance.TestCase Extends %UnitTest.TestCase
    {
    Property Duration As %Time;
    Property Lines As %Integer;
    Property Globals As %Integer;
    Property SystemCPUTime As %Integer;
    Property UserCPUTime As %Integer;
    Property DiskReadTime As %Integer;
    }
     
    作成する特定のユニットテストは、%UnitTest.TestCaseではなく、新しいサブクラスから継承されている必要があります。
     
    サブクラスではOnBeforeOneTest()を使って、各ユニットテストの統計データを初期化します。 DiskReadTime以外、現在値でプロパティを初期化します。
     
    /// パフォーマンス統計を初期化します
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">Method OnBeforeOneTest(testname As %String) As %Status</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">{</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // 現在の値で初期化します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Duration = $zh</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Lines = $system.Process.LinesExecuted()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Globals = $system.Process.GlobalReferences()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><span style="">    set ..SystemCPUTime = $piece(CPUTime, ",", 1)</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..UserCPUTime = $piece(CPUTime, ",", 2)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ディスク時間を0にリセットし、カウントを開始します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.ResetDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.EnableDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    return $$$OK</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">}</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
     
    <div>
      <span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none">OnAfterOneTest() を使って、各ユニットテストの統計データを確定します。 DiskReadTime以外、現在の値から初期値を減算します。</span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
       
    </div>
    
    <div>
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">/// パフォーマンス統計を確定します </font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">/// ここに、分析用にカウンターを別のテーブルに保存するためのコードを追加できます。</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">Method OnAfterOneTest(testname As %String) As %Status</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">{</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><span style="">    set ..Duration = $zh - ..Duration</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Lines = $system.Process.LinesExecuted() - ..Lines</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Globals = $system.Process.GlobalReferences() - ..Globals</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <div>
          <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set CPUTime = $system.Process.GetCPUTime()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
        </div>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..SystemCPUTime = $piece(CPUTime, ",", 1) - ..SystemCPUTime</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..UserCPUTime = $piece(CPUTime, ",", 2) - ..UserCPUTime</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ディスク読み取り時間を取得し、カウントを停止します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..DiskReadTime = $system.Process.DiskReadMilliseconds()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.DisableDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ユニットテストログにメッセージを追加します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set msg = "Performance: " _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span> 
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              "Duration: " _           ..Duration _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Lines: " _            ..Lines _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Globals: " _          ..Globals _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", System CPU Time: " _ (..SystemCPUTime / 1000) _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", User CPU Time: " _   (..UserCPUTime / 1000) _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Disk Read Time: " _  (..DiskReadTime / 1000)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $$$LogMessage(msg)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    return $$$OK</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">}</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
    </div>
    
     
    ちょっとした技がもう1つあります。 統計を収集するかしないかを指定して、ユニットテストを実行したほうが良いかもしれません。 したがって、ユニットテストを呼び出すコードに引数(%Boolean 1または0)を追加し、何らかの方法で渡す必要があります。 テストを実際に実行するメソッド(RunTest() またはほかのRun*() メソッドの1つ)は、第3引数に配列を取り、参照形式で渡します。 次は、そのサンプルコードです。
     
        // logging引数(1または0)を保持する配列を作成し、それを参照で渡します
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set p("logging") = logging</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do ##class(%UnitTest.Manager).RunTest(test, qualifiers, .p)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
     
    配列に渡す値は、OnBeforeOneTest() と OnAfterOneTest() でアクセスできます。 これを両方のメソッドの最初の行に追加します。
     
       if (..Manager.UserFields.GetAt("logging") = 0) { return $$$OK }
     
    以上です! 皆さんのご質問、コメント、その他のアイデアをぜひお聞かせください。
    0
    0 174
    記事 Toshihiko Minamoto · 4月 5, 2021 11m read

    はじめに

    Webで行われるサーバーとクライアント間のほとんどの通信は、リクエストとレスポンスの構造に基づいており、 クライアントがサーバーにリクエストを送信すると、サーバーがそのリクエストに対するレスポンスを送信します。 WebSocketプロトコルは、サーバーとクライアント間の双方向通信チャンネルを提供するプロトコルで、サーバーがリクエストを受信しなくても、クライアントにメッセージを送信することができます。 WebSocketプロトコルと、InterSystems IRISでの実装についての詳細は、以下のリンクをご覧ください。

    このチュートリアルは、「非同期WebSocket -- クイックチュートリアル」を、Caché 2016.2以上とInterSystems IRIS 2018.1以上向けに更新したものです。

    非同期動作と同期動作

    InterSystems IRISでは、WebSocket接続を同期的または非同期的に実装することができます。 クライアントとサーバー間のWebSocket接続がどのように動作するかは、%CSP.WebSocketクラスの「SharedConnection」プロパティによって決まります。

    • SharedConnection=1: 非同期動作

    • SharedConnection=0: 同期動作

    クライアントとInterSystems IRISインスタンスがホスティングされているサーバーとの間のWebSocket接続には、IRISインスタンスとWebゲートウェイとの間のコネクションが含まれます。 WebSocketの同期動作では、そのコネクションはプライベートチャンネルが使用されますが、 非同期動作では、複数のWebSocketクライアントで、IRISインスタンスとWebゲートウェイとの間のコネクションが共有されます。 WebSocketの非同期動作は、各クライアントを処理するWebゲートウェイとIRISインスタンス間のコネクションを排他的に行う必要がないため、同一のサーバーに多数のクライアントが接続する場合に、そのメリットを発揮します。

    このチュートリアルでは、WebSocketの非同期動作を行います。 したがって、開いているすべてのチャットウィンドウは、Webゲートウェイと、WebSocketサーバークラスをホストするIRISインスタンス間のコネクションを共有することになります。

    チャットアプリケーションの概要

    WebSocketの「hello world」は、ユーザーがそのアプリケーションにログインしているすべてのユーザーにブロードキャストされるメッセージを送信できるチャットアプリケーションです。 このチュートリアルでは、チャットアプリケーションには次のコンポーネントが含まれます。

    • サーバー: %CSP.WebSocketを継承したクラスに実装されています。

    • クライアント: CSPページで実装されています。

    このチャットアプリケーションの実装では、次の内容を実現します。

    • ユーザーは、開いているすべてのチャットウィンドウにメッセージをブロードキャストできます。

    • オンラインユーザーは、開いているすべてのチャットウィンドウの「オンラインユーザー」リストに表示されます。

    • ユーザーは、「alias」キーワードで始まるメッセージを作成することで、ユーザー名を変更できます。このメッセージはブロードキャストされませんが、「オンラインユーザー」リストの内容を更新します。

    • ユーザーがチャットウィンドウを閉じると、「オンラインユーザー」リストから削除されます。

    チャットアプリケーションのソースコードについては、こちらのGitHubリポジトリをご覧ください。

    クライアント

    チャットアプリケーションのクライアント側は、チャットウィンドウのスタイル定義、WebSocket接続の宣言、サーバー間との通信を処理するWebSocketのイベントとメソッド、およびサーバーに送信されるメッセージをパッケージ化し、受信メッセージを処理するヘルパー関数を含むCSPページによって実装されます。

    まず、アプリケーションが、JavaScript WebSocketライブラリを使用して、WebSocket接続をどのように初期化するのか確認しましょう。

        ws = new WebSocket(((window.location.protocol === "https:")? "wss:":"ws:")
                        + "//"+ window.location.host + "/csp/user/Chat.Server.cls");
    

    new は、WebSocketクラスの新しいインスタンスを作成します。 これにより、「wss」(WebSocket通信チャンネルにTLSを使用することを示します)または「ws」プロトコルを使って、サーバーにWebSocket接続が開きます。 サーバーは、Chat.Server クラスを定義するウェブサーバーのポート番号とインスタンスのホスト名で指定されてます(この情報は window.location.host 変数に格納されます)。 サーバークラスの名前(Chat.Server.cls)は、サーバー上のリソースのGETリクエストとして、WebSocketの開始URIに含まれます。

    WebSocket接続の確立に成功すると ws.onopen イベントが発生し、**接続中から接続**に状態が移行します。

        ws.onopen = function(event){
            document.getElementById("headline").innerHTML = "CHAT - CONNECTED";
        };
    

    このイベントにより、チャットウィンドウの見出しが変更され、クライアントとサーバーが接続されていることが示されるようになります。

    メッセージの送信

    ユーザーがメッセージを送信するアクションによって、send 関数がトリガーされます。 この関数は ws.send メソッドを囲むラッパーとして機能し、クライアントのメッセージをWebSocket接続を介してサーバーに送信する仕組みが含まれます。

    function send() {
        var line=$("#inputline").val();
        if (line.substr(0,5)=="alias"){
            alias=line.split(" ")[1];
            if (alias==""){
                alias="default";
            }
            var data = {}
            data.User = alias
            ws.send(JSON.stringify(data));
            } else {
            var msg=btoa(line);
            var data={};
            data.Message=msg;
            data.Author=alias;
            if (ws && msg!="") {
                ws.send(JSON.stringify(data));
            }
        }
        $("#inputline").val("");
    }
    

    send は、サーバーに送信される情報(エイリアスの更新または一般的なメッセージ)をJSONオブジェクトにパッケージ化し、送信される情報の種類に応じたキー/値ペアを定義します。 btoa は、一般メッセージの内容を、base-64でエンコードされたASCII文字列に変換します。

    メッセージの受信

    クライアントがサーバーからメッセージを受信すると、ws.onmessage イベントがトリガーされます。

    ws.onmessage = function(event) {
        var d=JSON.parse(event.data);
        if (d.Type=="Chat") {
            $("#chat").append(wrapmessage(d));
                $("#chatdiv").animate({ scrollTop: $('#chatdiv').prop("scrollHeight")}, 1000);
        } else if(d.Type=="userlist") {
            var ul = document.getElementById("userlist");
            while(ul.firstChild){ul.removeChild(ul.firstChild)};
            $("#userlist").append(wrapuser(d.Users));
        } else if(d.Type=="Status"){
            document.getElementById("headline").innerHTML = "CHAT - connected - "+d.WSID;
        }
    };
    

    クライアントが受信するメッセージの種類(「チャット」、「ユーザーリスト」、「ステータス」)に応じて、onmessage イベントによって、wrapmessage または wrapuser が呼び出され、チャットウィンドウの該当するセクションに受信データを取り込みます。 受信メッセージがステータス更新であれば、チャットウィンドウのステータスの見出しがWebSocket IDで更新されます。このIDは、チャットウィンドウに関連付けられた双方向WebSocket接続を識別するものです。

    その他のクライアントコンポーネント

    クライアントとサーバー間の通信でエラーが発生すると、WebSocketの onerror メソッドがトリガーされ、エラーの発生を通知するアラートの発行とページのステータス見出しの更新が行われます。

    ws.onerror = function(event) {
        document.GetElementById("headline").innerHTML = "CHAT - error";
        alert("Received error"); 
    };
    

    クライアントとサーバー間のWebSocket接続が閉じると、onclose メソッドがトリガーされ、ステータスの見出しが更新されます。

    ws.onclose = function(event) {
        ws = null;
        document.getElementById("headline").innerHTML = "CHAT - disconnected";
    }
    

    サーバー

    チャットアプリケーションのサーバー側は、%CSP.WebSocket を拡張した Chat.Server クラスで実装されます。 サーバークラスは、%CSP.WebSocketのプロパティとメソッドを継承しています。これについては以下の方で説明します。 Chat.Server は、クライアントからのメッセージを処理するカスタムメソッドと、クライアントにメッセージをブロードキャストするカスタムメソッドも実装します。

    サーバーの起動前処理

    OnPreServer() は、WebSocketサーバーが作成されて %CSP.WebSocket クラスから継承される前に実行されます。

    Method OnPreServer() As %Status
    {
        set ..SharedConnection=1
        if (..WebSocketID '= ""){ 
            set ^Chat.WebSocketConnections(..WebSocketID)=""
        } else {
            set ^Chat.Errors($INCREMENT(^Chat.Errors),"no websocketid defined")=$HOROLOG 
        }
        Quit $$$OK
    }
    

    このメソッドは、SharedConnection クラスのパラメーターを1に設定し、WebSocket接続が非同期であり、InterSystems IRISインスタンスとWebゲートウェイ間の接続を定義する複数のプロセスでサポートされることを示します。 SharedConnection パラメーターは、OnPreServer() でしか変更できません。 また、OnPreServer() は、クライアントに関連付けられているWebSocket IDを ^Chat.WebSocketConnections グローバルに格納します。

    Serverメソッド

    サーバーが実行するロジックの本文は、Server() メソッドに含まれます。

    Method Server() As %Status
    {
        do ..StatusUpdate(..WebSocketID)
        for {       
            set data=..Read(.size,.sc,1) 
            if ($$$ISERR(sc)){
                if ($$$GETERRORCODE(sc)=$$$CSPWebSocketTimeout) {
                    //$$$DEBUG("no data")
                }
                if ($$$GETERRORCODE(sc)=$$$CSPWebSocketClosed){
                    kill ^Chat.WebSocketConnections(..WebSocketID)
                    do ..RemoveUser($g(^Chat.Users(..WebSocketID))) 
                    kill ^Chat.Users(..WebSocketID)
                    quit  // Client closed WebSocket
                }
            } else{
                if data["User"{
                    do ..AddUser(data,..WebSocketID)
                } else {
                    set mid=$INCREMENT(^Chat.Messages)
                    set ^Chat.Messages(mid)=data
                    do ..ProcessMessage(mid)
                }
            }
        }
        Quit $$$OK
    }
    

    このメソッドは、クライアントからの受信メッセージを読み取り(%CSP.WebSockets クラスの Read メソッド)、取得したJSONオブジェクトを ^Chat.Messages グローバルに追加し、接続されているその他すべてのチャットクライアントにメッセージを転送するための ProcessMessage() を呼び出します。 ユーザーがチャットウィンドウを閉じると(つまり、サーバーへのWebSocket接続が終了すると)、Server() メソッドの Read の呼び出しによって、それが評価するエラーコードが $$$CSPWebSocketClosed マクロに返され、メソッドはそれに応じて閉鎖の処理に進みます。

    メッセージの処理と配信

    ProcessMessage() は、受信チャットメッセージにメタデータを追加して SendData() を呼び出し、メッセージをパラメーターとして渡します。

    ClassMethod ProcessMessage(mid As %String)
    {
        set msg = ##class(%DynamicObject).%FromJSON($GET(^Chat.Messages(mid)))
        set msg.Type="Chat"
        set msg.Sent=$ZDATETIME($HOROLOG,3)
        do ..SendData(msg)
    }
    

    ProcessMessage() は、^Chat.Messages グローバルからJSONの書式付きメッセージを取得し、%DynamicObject クラスの %FromJSON メソッドを使って、そのメッセージをInterSystems IRIS オブジェクトに変換します。 この変換により、メッセージが接続されているすべてのチャットクライアントに転送される前に、データを編集しやすくなります。 Type 属性を値「Chat」で追加し、クライアントはこれを使用して、受信メッセージの処理方法を決定しアンス。 SendData() は、接続されているその他すべてのチャットクライアントにメッセージを送信します。

    ClassMethod SendData(data As %DynamicObject)
    {
        set c = ""
        for {
            set c = $order(^Chat.WebSocketConnections(c))
            if c="" Quit
            set ws = ..%New()
            set sc = ws.OpenServer(c)
            if $$$ISERR(sc) { do ..HandleError(c,"open") } 
            set sc = ws.Write(data.%ToJSON())
            if $$$ISERR(sc) { do ..HandleError(c,"write") }
        }
    }
    

    SendData() は、InterSystems IRISオブジェクトをJSON文字列に変換し直し(data.%ToJSON())、すべてのチャットクライアントにメッセージをプッシュします。 SendData() は、クライアントとサーバー間のそれぞれの接続に関連付けられたWebSocket IDを ^Chat.WebSocketConnections グローバルから取得し、そのIDを使って、%CSP.WebSocket クラスの OpenServer メソッドで、WebSocket接続を開きます。 OpenServer メソッドを使ってこの処理を実行できるのは、WebSocket接続が非同期であるからです。IRIS-Webゲートウェイの既存のプールからプロセスをプルし、特定のチャットクライアントへのサーバー接続を識別するWebSocket IDを割り当てています。 最後に、 Write()%CSP.WebSocket メソッドによって、JSON 文字列に変換されたメッセージがクライアントにプッシュされます。

    まとめ

    このチャットアプリケーションでは、クライアントとInterSystems IRISをホストするサーバー間にWebScocket接続を確立する方法が示されています。 InterSystems IRISにおけるプロコルとその実装については、引き続き、「はじめに」セクションに記載されているリンクをご覧ください。

    0
    0 3627
    記事 Toshihiko Minamoto · 1月 5, 2021 9m read

    はじめに

    多くのアプリケーションに共通する要件は、データベース内のデータ変更のログ記録です。どのデータが変更されたか、誰がいつ変更したかをログに記録する必要があります(監査ログ)。 このような質問について書かれた記事は多く存在し、Caché で行う方法の切り口もさまざまです。

    そこで、データ変更を追跡して記録するためのフレームワークを実装しやすくする仕組みを説明することにします。 これは、永続クラスが「監査抽象クラス」(Sample.AuditBase)から継承すると「objectgenarator」メソッドを介してトリガーを作成する仕組みです。 永続クラスは Sample.AuditBase から継承されるため、永続クラスをコンパイルすると、変更を監査するためのトリガーが自動的に生成されます。


    監査クラス  

    次は、変更が記録されるクラスです。

      Class Sample.Audit Extends %Persistent
    {
              Property Date As %Date;
              Property UserName As %String(MAXLEN = "");
              Property ClassName As %String(MAXLEN = "");
              Property Id As %Integer;
              Property Field As %String(MAXLEN = "");
              Property OldValue As %String(MAXLEN = "");
              Property NewValue As %String(MAXLEN = "");
    }

    ### 監査抽象クラス  

    これは、永続クラスの継承元となる抽象クラスです。 このクラスには、監査テーブル(Sample.Audit)に変更を書き込むほか、どのフィールドが変更されたのか、誰が変更したのか、変更前と後の値は何であるかなどを識別する方法を知っているトリガーメソッド(objectgenerator)が含まれています。

        Class Sample.AuditBase [ Abstract ]
    {
    Trigger SaveAuditAfter [ CodeMode = objectgenerator, Event = INSERT/UPDATE, Foreach = row/object, Order = 99999, Time = AFTER ]
    {
              #dim %compiledclass As %Dictionary.CompiledClass
              #dim tProperty As %Dictionary.CompiledProperty
              #dim tAudit As Sample.Audit
              Do %code.WriteLine($Char(9)_"; get username and ip adress")
              Do %code.WriteLine($Char(9)_"Set tSC = $$$OK")
              Do %code.WriteLine($Char(9)_"Set tUsername = $USERNAME")
              Set tKey = ""
              Set tProperty = %compiledclass.Properties.GetNext(.tKey)
              Set tClassName = %compiledclass.Name
              Do %code.WriteLine($Char(9)_"Try {")
              Do %code.WriteLine($Char(9,9)_"; Check if the operation is an update - %oper = UPDATE")
              Do %code.WriteLine($Char(9,9)_"if %oper = ""UPDATE"" { ")
              While tKey '= "" {
                        set tColumnNbr = $Get($$$EXTPROPsqlcolumnnumber($$$pEXT,%classname,tProperty.Name))
                        Set tColumnName = $Get($$$EXTPROPsqlcolumnname($$$pEXT,%classname,tProperty.Name))
                        If tColumnNbr '= "" {
                                  Do %code.WriteLine($Char(9,9,9)_";")
                                  Do %code.WriteLine($Char(9,9,9)_";")
                                  Do %code.WriteLine($Char(9,9,9)_"; Audit Field: "_tProperty.SqlFieldName)
                                  Do %code.WriteLine($Char(9,9,9)_"if {" _ tProperty.SqlFieldName _ "*C} {")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit = ##class(Sample.Audit).%New()")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.ClassName = """_tClassName_"""")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Id = {id}")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.UserName = tUsername")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Field = """_tColumnName_"""")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Date = +$Horolog")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.OldValue = {"_tProperty.SqlFieldName_"*O}")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.NewValue = {"_tProperty.SqlFieldName_"*N}")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tSC = tAudit.%Save()")
                                  do %code.WriteLine($Char(9,9,9,9)_"If $$$ISERR(tSC) $$$ThrowStatus(tSC)")
                                  Do %code.WriteLine($Char(9,9,9)_"}")
                        }
                        Set tProperty = %compiledclass.Properties.GetNext(.tKey)
              }
              Do %code.WriteLine($Char(9,9)_"}")
              Do %code.WriteLine($Char(9)_"} Catch (tException) {")
              Do %code.WriteLine($Char(9,9)_"Set %msg = tException.AsStatus()")
              Do %code.WriteLine($Char(9,9)_"Set %ok = 0")
              Do %code.WriteLine($Char(9)_"}")
              Set %ok = 1
    }
    }

    ### データクラス(永続クラス)

    これは、ユーザー(アプリケーション)が変更を加えたり、レコードを作成したり、レコードを削除したりなど、ユーザーに許可したことをすべて実行するユーザーデータクラスです。 :)。 つまり、通常は %Persistent クラスということです。   変更を追跡して記録するには、この永続クラスを抽象クラス(Sample.AuditBase)から継承する必要があります。

          Class Sample.Person Extends (%Persistent, %Populate, Sample.AuditBase)
    {
              Property Name As %String [ Required ];
              Property Age As %String [ Required ];
              Index NameIDX On Name [ Data = Name ];
    }

    ### テスト

    監査抽象クラス(Sample.AuditBase)からデータクラス(Sample.Person)を継承しているため、データの挿入、変更の追加、および変更内容の確認を監査クラス(Sample. Audit)で行うことができます。

    これをテストするには、Sample.Person クラスまたは他の任意のクラスに Test() クラスメソッドを作成する必要があります。

    ClassMethod Test(pKillExtent = )
    {
              If pKillExtent '= 0 {
                        Do ##class(Sample.Person).%KillExtent()
                        Do ##class(Sample.Audit).%KillExtent()
              }
              &SQL(INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01'))
              Write "INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01')",!
              Write "SQLCODE: ",SQLCODE,!!!
              Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person")
              Do tRS.%Display()
              &SQL(UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE')
              Write !!!
              Write "UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE'",!
              Write "SQLCODE:",SQLCODE,!!!
              Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person")
              Do tRS.%Display()
              Quit
    }
    Test() メソッドを実行しましょう。
            d ##class(Sample.Person).Test(1)

    パラメータの「1」は、Sample.Person と Sample.Audit クラスからエクステントを削除します。

    このテストクラスメソッドは次の内容を実行します。

    • 「TEST」という名前で新しい人を挿入する
    • 挿入の結果を表示する
    • 「TEST」という人を「TEST ABC」に更新する
    • 更新の結果を表示する

    ここで、監査ログテーブルを確認してみましょう。 これを行うには、システム管理ポータル -> システムエクスプローラ -> SQL を開きます。 (ネームスペースを忘れずに切り替えてください)

    次の SQL コマンドを実行して結果を確認します。

    SELECT * FROM Sample.Audit 

    OldValue が「TEST」、NewValue が「TEST ABC」であることに注意してください。これ以降は、「TEST ABC」を自分の名前に変更したり、年齢の値を変更したりして、独自のテストを行ってみると良いでしょう。 次に例を示します。

    UPDATE Sample.Person SET Name = 'Fabio Goncalves' WHERE Name = 'TEST ABC'

    ### 生成されるコード

    以下の監査メカニズムを実装しているとした上で、コンピュータで Studio(または Atelier)を起動し、永続クラス(Sample.Person)を開いて、Sample.Person クラスをコンパイルした後に生成される中間コードを調べてみましょう。 これを行うには、Ctrl + Shift + V(ほかのソースコードを表示)を押して、.INT を検査します。 zSaveAuditAfterExecute ラベルまでスクロールし、生成されたコードを確認します。

    ### メリット

    古いデータのロールアウトに基づいて監査ログ機能を実装するのは簡単です。 追加のテーブルは必要ありません。 メンテナンスも簡単で、 古いデータを削除するのであれば、1 つの SQL で済みます。 ほかのテーブルでも監査ログ機能を実装する必要がある場合は、抽象クラス(Sample.AuditBase)から継承するだけです。 必要に応じて変更してください。 例: ストリームの変更を記録する。 変更されたフィールドのみを記録し、 変更したレコード全体を保存しないでください。

    デメリット

    データが変更されると、レコード全体がコピーされるため、変更されていないデータもコピーされてしまうことが問題となる場合があります。 テーブル Person に写真が含まれるバイナリデータ(stream)を持つ「photo」という列がある場合、ユーザーが写真を変更するたびに、ストリーム全体が記録されてしまいます(ディスクスペースが消費されてしまいます)。 もう 1 つの難点は、監査ログをサポートする各テーブルの複雑さが増すところにあります。 レコードの取得は容易にはいかないことを肝に銘じておきましょう。 SELECT 句は必ず条件「...WHERE Status = active」とともに使用するか、「DATE INTERVAL」などを検討するようにしてください。 すべてのデータ変更は共通テーブルにログされます。 トランザクションをロールバックとして考えるようにしましょう。

     


     アプリケーションの効率化には、監査は重要な要件です。 通常、データ変更を判別するには、アプリケーション開発者が、トリガー、タイムスタンプ列、およびその他のテーブルを組み合わせてアプリケーションにカスタムの追跡メソッドを実装することが必要です。 こういった仕組みを作成するには大抵、多くの作業を実装する必要があり、スキーマの更新や高パフォーマンスのオーバーヘッドが生じることがよくあります。 この記事は簡単な例として、独自のフレームワークを作成し始める際に役立ててください。
    0
    0 261
    記事 Toshihiko Minamoto · 12月 23, 2020 3m read

    新しい動的 SQL クラス(%SQL.Statement および %StatementResult)のパフォーマンスは %ResultSet より優れてはいますが、%ResultSet の使用方法をせっかく学習したので、しばらくの間新しい方を使用せずにいましたが、 やっとチートシートを作ったので、新しいコードを書いたり古いコードを書き直す際に役立てています。 皆さんのお役に立てればいいなと思っています。

    次に示すのは、私のチートシートの詳細版です。

    <th>
      %ResultSet::%New()
    </th>
    
    <th>
      %SQL.Statement::%New()
    </th>
    
    <td>
         Prepare() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         %Prepare() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         Execute() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         %Execute() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         前のステップが %SQL.StatementResult のインスタンスを返すので、次のステップでそれを使用
    </td>
    
    <td>
         Next() インスタンスメソッドを呼び出す(while ループでイテレートなど)
    </td>
    
    <td>
         %Next() インスタンスメソッドを呼び出す(while ループでイテレートなど)
    </td>
    
    <td>
         GetData() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    <td>
         %GetData() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    <td>
         %Get() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    1
    2
    3
    4
    5
    6
    7
       Get() または Data() インスタンスメソッドを呼び出して、列番号で列を取得

     

    そして、これが私が実際に使用している簡易版チートシートです。

    <th>
      %ResultSet::%New()
    </th>
    
    <th>
      %SQL.Statement::%New()
    </th>
    
    <td>
         Prepare()
    </td>
    
    <td>
         %Prepare()
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         Execute()
    </td>
    
    <td>
         %Execute()
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         %Execute の戻り値を次のステップに使用
    </td>
    
    <td>
         Next()
    </td>
    
    <td>
         %Next()
    </td>
    
    <td>
         GetData()
    </td>
    
    <td>
         %GetData()
    </td>
    
    <td>
         %Get()
    </td>
    
    1
    2
    3
    4
    5
    6
    7
       Get() または Data()
    0
    0 226
    記事 Toshihiko Minamoto · 12月 21, 2020 9m read

    $LIST のフォーマットと%DynamicArray、%DynamicObject クラス

    IRIS には、様々なデータ値を含むシーケンスを作成する方法がいくつかあります (以前は Cache にもありました)。  長年に渡り使用されているデータシーケンスの 1 つに $LIST の文字列があります。  より最近のデータシーケンスには %DynamicArray クラスと %DynamicObject クラスがあり、両者ともに JSON の文字列表現に対応する IRIS サポートの一部となっています。  これら 2 つのシーケンスにはそれぞれ非常に異なるトレードオフがあります。

    $LIST の文字列形式

    $LIST 形式は、かつてメモリアドレスのスペースが小さいだけでなく、ディスクドライブも小さく、読み取り速度が遅かった時代に考案されました。  $LIST の形式は、複数の異なるデータ型で構成されるシーケンスをバイト数を可能な限り抑えながら 8 ビットの一般的な文字列にパッキングするためにデザインされました。

    $LIST のシーケンスは、ObjectScript の $LISTBUILD 関数を使って作成します。 

    $LIST の文字列の最も重要な機能は、データを 8 ビットの値で構成される最小のシーケンスにぎっしりパッキングできるという点です。  $LIST の文字列には、ObjectScript の様々なデータ型の複数の異なる表現を含めることができます。  それらのデータ型には、文字列型 (ObjectScript では引用符で囲んだ文字列リテラルを使って作成)、10 進浮動小数点型 (ObjectScript では数値リテラルを使って作成)、および IEEE 規格の 2 進浮動小数点型 (ObjectScript では $DOUBLE 関数を数値式に適用して作成) が含まれます。  ObjectScript の oref 型は $LIST の文字列によってサポートされていません。  $LIST の要素が作成されるとき、これらの値の内部表現に対しバイナリとバイトの非常にシンプルな圧縮化が行われます。  

    $LIST の文字列の 2 つ目の重要な機能は、これらの 8 ビットの値を IRIS インスタンスまたは転送メディアのエンディアン特性 (ビッグなのかリトルなのか) を気にせずに、両立させながら同インスタンス間を送信できるという点です。  特に明記すべきは、ビッグエンディアンを使うデータベースとリトルエンディアンを使うデータベースの間で $LIST の文字列を移動させる際に、$LIST のコンポーネントの値が変更されないという点です。

    データのパッキングと転送機能に次いで重要なのがパフォーマンスです。  すべての $LIST オペレーション ($LISTVALID を除く) は、実行するマシンインストラクションの数を最小限に抑えようとします。  $LIST の構造が無効なために、セグメントフォールトや他のシステム例外が起り得る場合を除き、$LIST オペレーションは $LIST のデータ構造が有効であるという想定の基に実行されます。  $LIST オペレーションは、それを有効性を確認するためのインストラクションは実行しません。  

    空の $LIST は空の文字列です。  $LIST の文字列を連結するには、文字列の一般的な連結方法を用います。

    $LIST の文字列をタイトにパッキングするということは、$LIST の i 個目の要素 ($LIST(ListString,i)) に直接ジャンプするのに効果的な情報はなく、先行する $LIST の要素をまず最初にスキャンする必要があることを意味します。  $LIST のすべての要素をスキャンする場合は、以下のコードを使ってはいけません     

    Set N=$LISTLENGTH(ListString)
    For i=1:1:N {
       Write $LIST(ListString,i),!
    }
    

    なぜなら、上の ObjectScript コードは、$LIST の値を O(N**2) の時間計算量でループするからです。  代わりに以下を使います。

    Set P=0
    While $LISTNEXT(ListString,P,value) {
        Write value,!
    }
    

    このコードは $LIST の値を O(N) の時間計算量でループします。  

    %DynamicArray クラス

    最近では、%DynamicArray クラス (および %DynamicObject クラス) が作成されています。  今は、はるかに大きなメモリを使用できるようになり (現在のメモリは $LIST の文字列がデザインされた時代のディスクドライブよりも大きくなっている)、ディスクドライブも大幅にサイズアップしています。  今は構造化されたデータを異なるシステム間で送信するための標準的な形式があります。  これらには、XML や JSON が含まれます。  %DynamicArray クラス (および %DynamicObject クラス) には、JSON の値と ObjectScript の値 (oref 型の値も含む) を正確に表現する機能があります。  JSON の値と ObjectScript の値は非常によく似ている上に、1 つの種類の値が別の種類の値に変換されても、形式はほとんど変わりません (例外は、ObjectScript の oref 型の値で、JSON 特有の表現には変換できません)。

    JSON の仕様では、JSON 配列リテラルは角括弧で囲まれると説明されています。  例えば、 [0.1,"One tenth",2.99792E8,"speed of light in m/s"] のように囲みます。  JSON のオブジェクトリテラルは、中括弧で囲まれます。  例えば、 {"Name":"IRIS", "Size":64} のように囲みます。  ObjectScript 言語では、JSON の配列リテラルとオブジェクトリテラルを使用できる他、JSON 配列または JSON オブジェクトが持つ要素の値を丸括弧で囲み、ObjectScript のランタイム式として使用できるという拡張機能が 1 つあります。  例えば、 [(.1),("One " _ "tenth"),(2.99793*1000)] のようにできます。  丸括弧の中では、JSON の構文の代わりに ObjectScript の構文が使用されることに注意してください。

    $LIST の文字列と %DynamicArray オブジェクトにはいくつか相違点があります。 (%DynamicObject オブジェクトのプロパティは %DynamicArray オブジェクトのプロパティと似ているので、以下のディスカッションで %DynamicObjectについて毎回言及するのは控えます。)

    %DynamicArray の最初の要素は、インデックス 0 である一方で、$LIST の最初の要素はインデックス 1 となります。

    %DynamicArray 要素は、ObjectScript の式で評価されるか、JSON 文字列に変換されるまでは、それが持つ元々の JSON の値または元々の ObjectScript の値を正確に表現でき、変換オペレーションにより小さな変化が生じることは一切ありません。

    $LIST の文字列の最大サイズ (結果的にその要素のサイズ) は、現時点で 3641144 文字というObjectScript 文字列の最大長により制限されています。  %DynamicArray の最大サイズ (結果的にその要素のサイズ) はIRIS プロセスのメモリ空間のサイズにのみ制限されます。  これを踏まえ、多数の大きな %DynamicArray オブジェクトを同時に持つことは避けてください。これは、仮想アドレス空間を無制限で使用すると、過度のメモリーページングを引き起こす可能性があり、システムのパフォーマンスに影響するためです。

    $LIST の文字列は、ObjectScript の文字列を格納できる場所であれば、どこにでも格納できます。 これは、文字列の長さが 3641144 文字をオーバーしないローカル変数やグローバル変数を含んでいます。  %DynamicArray オブジェクトをグローバル変数、ストリーム、またはファイルに移動する前には、%DynamicArray を他のデータ型 (通常は、JSON の表現を使用する文字列) に変換する必要があります。  JSON 文字列を持つ ObjectScript の文字列を返すには、引数なしの %ToJSON() メソッドを使用できます。  しかし、%DynamicArrays が大きいと、ObjectScript のグローバル変数や ObjectScript 式の中に収まりきらないほど長い JSON の文字列が生成される場合があります。  この場合、%ToJSON(output) メソッドを実行すると、%DynamicArray が JSON 文字列に変換され、%ToJSON メソッドの引数によって指定される %Stream もしくはファイルに送付されます。

    新しい %DynamicArray を効率よく作成するには、ObjectScript コンストラクタを使ったり、%FromJSON(input) メソッドを呼び出したりすると良いでしょう。  %DynamicArray のコンポーネントを調べる際に %Get メソッドを使用するのも効率の良い方法です。  特に、$LIST(ListVar,i) を評価する場合とは違い、DynArray.%Get(i) の評価にかかる時間が 'i' の値や DynArray.%Size() の値に左右されない点に効率の良さが伺えます。  例えば、ObjectScript を使った次のループを実行すると、

    Set N=DynArray.%Size()
    For i=O:1:N-1 {
        Write DynArray.%Get(i),!
    }
    

    %DynamicArray に含まれるすべての要素が O(N) の時間計算量で出力され、$LIST(ListVar,i) メソッドを使いアクセスした要素が出力されるループを実行した場合に発生する O(N**2) の時間計算量は避けることができます。

    次のようなループを書くこともできます。

    Set iter = DynObject.%GetIterator()
    While iter.%GetNext(.key , .value ) {
        Write "key = "_key_" , value = "_value,!
    }
    

    このループは、'DynObject' の未定義の要素をスキップする上に、%DynamicObject の要素と %DynamicArray の要素を出力するのに便利でもあります。

    しかし、既に作成済みの %DynamicArray の内部要素を変更するのに 'Do DynArray.%Set(i,newvalue)' を使用すると、DynArray に割り当てられたメモリの圧縮がある程度必要になる可能性があるほか、様々な内部インデックスの変更が必要になると思われるため、処理に時間がかかる可能性があります。  配列要素を大幅に変更する場合は、ObjectScript の多次元配列変数を使ってデータを表す方が無難と言えます。それは、ObjectScript の多次元配列は、配列要素の変更、挿入、削除に必要な時間を最小限に短縮できるようデザインされているためです。

    IRIS 2019.1 では、%Get(key,default,type) メソッドと %Set(key,value,type) メソッドの機能が拡張されています。  引数 'default' と 'type' は省略可能です。  'default' 引数には、指定された 'key' を持つ要素が未定義である場合に、DynObject.%Get(key,default) が返す値が入ります。  'type' パラメータとして使用できる値に、"stream" があります。これを使うことで、文字列値要素が ObjectScript の文字列に収まりきらないほど大きい場合に、%DynamicArray/%DynamicObject 要素の値を %Stream オブジェクトとして取得することができます。  'type' パラメータには、"json" という値も使用できます。これは、ObjectScript の値に使用される表現への変換を防止する JSON 仕様に従ってフォーマットされた文字列にアクセスします。 詳細は、クラスリファレンスのウェブページをご覧ください。  サポートされる 'type' の値は、IRIS の今後のリリースでさらに追加されると思われます。 

    0
    0 976
    記事 Mihoko Iijima · 12月 17, 2020 4m read

    みなさん、こんにちは。 昨日、Apache Spark、Apache Zeppelin、そして InterSystems IRIS を接続しようとしたときに問題が発生したのですが、有用なガイドが見つからなかったので、自分で書くことにしました。

    はじめに

    Apache Spark と Apache Zeppelin とは何か、そしてどのように連携するのかを理解しましょう。

    Apache Spark はオープンソースのクラスタコンピューティングフレームワークです。暗黙的なデータ並列化と耐障害性を備えるようにクラスタ全体をプログラミングするためのインターフェースを提供しています。そのため、ビッグデータを扱う必要のある場合に非常に役立ちます。 一方の Apache Zeppelin はノートブックです。分析や機械学習に役立つ UI を提供しています。組み合わせて使う場合、IRIS がデータを提供し、提供されたデータを Spark が読み取って、ノートブックでデータを処理する、というように機能します。

    注意: 以下の内容は、Windows 10 で行っています。

    Apache Zeppelin

    では、必要なすべてのプログラムをインストールしましょう。

    まず、Apache Zeppelin の公式サイトから Apache Zeppelin をダウンロードします。私は zeppelin-0.8.0-bin-all.tgz を使用しました。このファイルには、ApacheSparkScala、および Python が含まれます。

    ダウンロードファイルを任意のフォルダに解凍します。解凍後、Zeppelin フォルダのルートから \bin\zeppelin.cmd を呼び出して、Zeppelin を起動します。 「Done, zeppelin server started」が表示されたら、ブラウザでhttp://localhost:8080 を開きます。 すべてうまくいった場合、「Welcome to Zeppelin!」メッセージが表示されます。

    注意: InterSystems IRIS がインストール済みであることを前提としています。まだインストールしていない場合は、次のステップに進む前に IRIS をダウンロードしてインストールしてください。

    Apache Spark

    ブラウザウィンドウに Zeppelin ノートブックが開いている状態です。右上の「anonymous」をクリックし、「Interpreter」をクリックします。下にスクロールして「spark」を見つけてください。

    「spark」の横に「 edit 」ボタンがあるので、それをクリックしましょう。 下にスクロールして、intersystems-spark-1.0.0.jarintersystems-jdbc-3.0.0.jar に依存関係を追加します。 私の環境は InterSystems IRIS を C:\InterSystems\IRIS\ ディレクトリにインストールしているため、追加しなければならないものは以下の場所にあります。

    私の環境でのファイルは以下の通りです。

    そして保存します。

    動作確認

    動作確認してみましょう。 新しいノートを作成し、段落に次のコードを貼り付けます。

    var dataFrame=spark.read.format("com.intersystems.spark").option("url", "IRIS://localhost:51773/NAMESPACE").option("user", "UserLogin").option("password", "UserPassword").option("dbtable", "Sample.Person").load()
    
    // dbtable - name of your table
    

    URL - IRIS アドレスを 次の書式で指定します。IRIS://ipAddress:superserverPort/namespace

  • **プロトコル「IRIS」** は、Java 共有メモリ接続を提供する TCP/IP 経由の JDBC 接続です。
  • **ipAddress** - InterSystems IRIS インスタンスの IP アドレス。 ローカルで接続している場合は、localhost の代わりに 127.0.0.1 を使用してください。
  • **superserverPort** - IRIS インスタンスのスーパーサーバーのポート番号。Web サーバーのポート番号とは異なります。 スーパーサーバーのポート番号を見つけるには、管理ポータルを開き、画面上部にある「概要」のリンクをクリックして表示される画面の「Superserver Port」を確認してください。
  • **namespace** - InterSystems IRIS インスタンスの既存のネームスペースを指定します。 このデモでは、USER ネームスペースに接続しています。
  • この段落を実行しましょう。 うまくいけば、「FINISHED」が表示されます。

    私のノートブックでの実行例です。

    まとめ

    この記事の内容をまとめると、Apache Spark、Apache Zeppelin、および InterSystems IRIS がどのように連携できるかがわかりました。 次の記事では、データ分析についてお話しします。

    リンク

    0
    0 332
    記事 Toshihiko Minamoto · 12月 16, 2020 1m read

    Windows と Mac で InterSystems IRIS 2019.1 (および 2018.1.2) の SSL/TLS 設定に認証局 (CA) の証明書を簡単に追加する新しい方法ができました。  IRIS にオペレーティングシステムの証明書ストアを使用することを要求するために、

    %OSCertificateStore

    を "信頼された証明書機関 X.509 証明書を含むファイル" のフィールドに入力します。   以下はポータルでそれを実行する方法を示した画像です。

    また、これについて説明したドキュメントへのリンクはこちらです。  "信頼された証明書機関の証明書を含むファイル" のオプションの中を探してください。

    必要な操作はこれだけです!  これで、OS 証明書ストアに載っているすべての CA の証明書をこの設定に使用することができます。

    0
    0 277
    記事 Tomohiro Iwamoto · 12月 10, 2020 17m read

    この記事は、GitHub Actions を使って GKE に InterSystems IRIS Solution をデプロイするの継続記事で、そこではGitHub Actions パイプラインを使って、 Terraform で作成された Google Kubernetes クラスタにzpm-registry をデプロイしています。 繰り返しにならないよう、次の項目を満たしたものを開始点とします。

    訳者注) 上記の記事を読まれてから、本記事に進まれることをお勧めしますが、GKE上のサービスにドメイン名を紐づける方法を解説した単独記事としてもお読みいただけます。

    0
    0 389
    記事 Toshihiko Minamoto · 12月 8, 2020 4m read

    インスタンスのデータに基づくビジネスインテリジェンスを実装しようと計画中です。 DeepSee を使うには、データベースと環境をどのようにセットアップするのがベストですか?

    このチュートリアルでは、DeepSee の 3 つのアーキテクチャ例を示しながら、上記の質問を解決します。 基本的なアーキテクチャモデルを、その制限を重点に説明するところから始めましょう。 以降のモデルは、複雑さが中程度のビジネスインテリジェンスアプリケーションに推奨されており、ほとんどのユースケースで十分なはずです。 チュートリアルの最後には、高度な実装を管理できるように、アーキテクチャの柔軟性を強化する方法を説明します。

    このチュートリアルに含まれる例では、新しいデータベースとグローバルマッピングを紹介し、それらをセットアップする理由とタイミングについて説明します。 アーキテクチャを構築する際には、より柔軟な例から得られるメリットについて説明します。

    始める前に

    プライマリサーバーと分析サーバー

    データの高可用性を実現する場合、InterSystems では一般的にミラーリングとシャドウイングを使用して、ミラー/シャドウサーバーに DeepSee を実装することをお勧めしています。 データの元のコピーをホストするマシンを「プライマリサーバー」と呼び、データとビジネスインテリジェンスアプリケーションのコピーをホストするマシンを「分析(またはレポーティング)サーバー」と呼びます。

    プライマリサーバーと分析サーバーを用意しておくことは非常に重要です。これは主に、いずれのサーバーにおいてもパフォーマンスに関する問題を回避するためです。 推奨アーキテクチャに関するドキュメントをご覧ください。

    データとアプリケーションコード

    ソースデータとコードを同じデータベースに保存することは、通常、規模の小さなアプリケーションでのみうまく機能します。 より大規模なアプリケーションでは、ソースデータとコードをそれぞれの専用データベースに保存することが推奨されます。専用のデータベースを使用することで、データを分離しながらも、DeepSee が実行するすべてのネームスペースでコードを共有することができます。 ソースデータ用のデータベースは、本番サーバーからミラーリングできるようにしておく必要があります。 このデータベースは、読み取り専用または読み取り/書き込みのいずれかです。 このデータベースでは、ジャーナリングを有効にしておくことをお勧めします。

    ソースクラスとカスタムアプリケーションは、本番サーバーと分析サーバーの両方にある専用データベースに保存します。 これら 2 つのソースコード用データベースは同期している必要がなく、同じバージョンの Caché を実行している必要もありません。 コードが別の場所で定期的にバックアップされているのであれば、ジャーナリングは通常必要ではありません。

    このチュートリアルでは、次の構成を使用しています。 分析サーバーの APP ネームスペースには、デフォルトのデータベースとして APP-DATA と APP-CODE があります。 APP-DATA データベースは、プライマリサーバーにある ソースデータ用データベースのデータ(ソーステーブルのクラスとファクト)にアクセスできます。 APP-CODE データベースは、Caché コード(.cls と .INT ファイル)とほかのカスタムコードを保存します。 このようにデータとコードを分離するのは典型的なアーキテクチャであり、ユーザーは、DeepSee コードとカスタムアプリケーションを効率的にデプロイすることができます。

    異なるネームスペースでの DeepSee の実行

    DeepSee を使用したビジネスインテリジェンス実装は、異なるネームスペースから実行されることがよくあります。 この記事では単一の APP ネームスペースのセットアップ方法を示しますが、同じ手順を使えば、ビジネスインテリジェンスアプリケーションが実行するすべてのネームスペースをセットアップすることも可能です。

    ドキュメント

    ドキュメントに含まれる初回セットアップの実行に関するページの内容を理解しておくことをお勧めします。 このページには、Web アプリケーションのセットアップ、DeepSee グローバルを個別のデータベースに配置する方法、および DeepSee グローバルの代替マッピングのリストが含まれています。


    このシリーズの第 2 部では、基本的なアーキテクチャモデルの実装について説明します。

    0
    0 281
    記事 Toshihiko Minamoto · 12月 4, 2020 5m read

    みなさん、こんにちは。

    数日前、SOAP(Web)サービスを使用して、REST に基づく新しいアプリケーション API と同じ認証を使用できるように、既存のレガシーアプリケーションを拡張したい、とお客様から伺いました。 新しいアプリケーションは OAuth2 を使用しているため、課題は明らかでした。SOAP リクエストを含むアクセストークンをどのようにしてサーバーに渡すか、ということです。

    Google でしばらく調べてみたところ、SOAP エンベロープにヘッダー要素を追加してから、アクセストークンを検証するために必要なことを Web サービス実装が実行できるようにするのが 1 つの実現方法であることがわかりました。

    幸い、私たちは、SOAP リクエストにカスタムヘッダーを提供するための仕組みを提供しています。 そこで、ドキュメントを確認したところ(詳細はこちら)、次のクラスが出てきました。

    カスタムヘッダークラス

    Class API.SOAP.OAuth2Header Extends %SOAP.Header
    {
    
    Property accessToken As %String(MAXLEN = "");
    
    }
    

    確かに、非常に単純です。 必要なのはアクセストークンを渡すことだけですが、ほかの情報も渡すように拡張することもできます。

    Webサービス実装クラス

    /// API.SOAP.MyService
    Class API.SOAP.MyService Extends %SOAP.WebService [ ProcedureBlock ]
    {
    
    /// Webサービスの名前。
    Parameter SERVICENAME = "MyService";
    
    /// TODO: これを実際の SOAP ネームスペースに変更してください。
    /// Webサービスの SOAP ネームスペース
    Parameter NAMESPACE = "http://tempuri.org";
    
    /// 参照先クラスのネームスペースが WSDL に使用されます。
    Parameter USECLASSNAMESPACES = 1;
    
    /// TODO: 引数と実装を追加してください。
    /// GetVersion
    Method GetAccountBalance(pAccNo As %String) As API.SOAP.DT.Account [ WebMethod ]
    
    {
      #define APP       "ANG RESOURCES"
      try {
        #dim tAccessTokenHeader as API.SOAP.OAuth2Header=..HeadersIn.GetAt("OAuth2Header")
    
        $$$THROWONERROR(tSC,##class(%SYS.OAuth2.AccessToken).GetIntrospection($$$APP,tAccessTokenHeader.accessToken,.jsonObjectAT))
    
        /* service specific check */
        // check whether the request is asking for proper scope for this service
        if '(jsonObjectAT.scope["account") set reason="scope not supported" throw 
            
        if '(##class(%SYS.OAuth2.Validation).ValidateJWT($$$APP,tAccessTokenHeader.accessToken,,,.jsonObjectJWT,.securityParameters,.tSC)) {
          set reason="unauthorized access attempt"
          throw
        }           
        set tAccountObject=##class(API.SOAP.DT.Account).%New()
        set tAccountObject.accno=pAccNo
        set tAccountObject.owner=jsonObjectJWT."acc-owner"
        set tAccountObject.balance=$random(200000)
      } catch (e) {
        set fault=..MakeFault($$$FAULTServer,"SECURITY",reason)
        Do ..ReturnFault(fault)
      }
      Quit tAccountObject
    }
    
    XData AdditionalHeaders
    
    {
    <parameters xmlns="http://www.intersystems.com/configuration">
    <request>
    <header name="OAuth2Header" class="API.SOAP.OAuth2Header"/> 
    </request>
    </parameters>
    }
    }
    

    アクセストークンの確認について、少し説明しましょう。 ご覧のとおり、最初に行うタスクは、カスタムヘッダーからアクセストークンを取得して、オブジェクト表現に逆シリアル化することです。

    その後は、スコープをチェックするかどうか、JWT トークンの検証などさらなる検証を実行するのかを決めることができます(OAuth2 認証サーバーの設定方法や、私が非常にお勧めしているOpenID を使用するかどうかに依存しています)。

    では、クライアント側を確認してみましょう。

    クライアントが OAuth2 認証サーバーのアクセストークンをどのように受け取るのかについては他の記事で説明しているので、ここでは深く言及せずにおきますが、代わりに、アクセストークンを Web サービスクライアントに提供する方法を確認することにしましょう。 (Web サービスクライアントクラスは、Atelier または Studio IDE から標準の SOAP ウィザード/クライアントオプションを実行して生成します。)

    次は私のクライアントのコードスニペットです。

      set tWSClient=##class(Web.WSC.MyServiceSoap).%New()
      set tWSHeader=##class(Web.WSC.s0.OAuth2Header).%New()
    
      set tWSHeader.accessToken=accessToken
      do tWSClient.HeadersOut.SetAt(tWSHeader,"access-token")
      #dim tAccountObject as Web.WSC.s0.Account=tWSClient.GetAccountBalance(tAccNo)
    

    リソースサーバーにおけるセキュリティ設定に関する考慮事項

    リソースサーバー(Web サーバー)で CSP アプリケーションをセットアップするには、認証されていないユーザーを許可するのが最も簡単なやり方ですが、これにはリスクが伴います。サービスやメソッドごとにアクセストークンをチェックし検証するのはあなた次第です。 アクセストークンが存在しないか有効でない場合は、SOAP フォルトを返さなければなりません。

    より優れた代替手段としては、委任認証を使用し、ZAUTHENTICATE ルーチンでアクセストークンを取得して、何らかの意図的なユーザー名(アクセストークンリクエストのスコープ付きで OpenID プロファイルが提供されている場合は JWT から取得可能)を、Web サーバーメソッドを実行するために最低限必要なロールで割り当てる方法があります。

    0
    0 809
    記事 Toshihiko Minamoto · 12月 1, 2020 6m read

    %Net.SSH.Session クラスを使用すると、SSH を使ってサーバーに接続することができます。 一般的にはSFTP、特に FTP インバウンドアダプタとFTPアウトバウンドアダプタで使用されています。

    この記事では、簡単な例を示しながら、このクラスを使用して SSH サーバーに接続する方法、認証のオプションを記述する方法、そして問題が発生した場合のデバッグ方法について説明します。

    次は接続を行う例です。

    Set SSH = ##class(%Net.SSH.Session).%New()
    Set return=SSH.Connect("ftp.intersystems.com")​
    

    上記のコードは新しい接続を作成してから、ftp.intersystems.com の SFTP サーバーにデフォルトのポートで接続します。 この時点で、クライアントとサーバーは暗号化アルゴリズムとオプションを選択済みですが、ユーザーはまだログインしていません。

    接続したら、認証方法を選択できます。 選択できるメソッドには次の 3 つがあります。

    • AuthenticateWithUsername
    • AuthenticateWithKeyPair
    • AuthenticateWithKeyboardInteractive

    上記はそれぞれ異なる認証方式です。 各方式を簡単に説明します。

    AuthenticateWithUsername

    これは、ユーザー名とパスワードを使用します。

    AuthenticateWithKeyPair

    これは、公開鍵と秘密鍵のペアを使用します。 公開鍵は事前にサーバーに読み込まれている必要があり、それに一致する秘密鍵が必要となります。 秘密鍵がディスク上で暗号化されている場合、メソッドへの呼び出しで、それを復号化するためのパスフレーズを指定します。 注意: 秘密鍵を他人に送信してはいけません。

    公開鍵は OpenSSH 形式であり、秘密鍵は PEM で暗号化されている必要があります。 OpenSSH の形式は次のような書式です。

    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfi2Vq+u0rtt2OC84pyrkq1k7WkrS+s76u3a+2gdD43KQ2Z3vSUUfksymJjp11JBZEpOtBVIAy221UKdc7j7Qk6sUjZaK8LIy+bzDVwMyFWgVvQge7EjdWjrJLBRCDXYML6y1Y25XexThkTWSGyXzGNdr+wfIHYn/mIt0hfvrusauvT/9Wz8K2MGAj4BL7UQZpFJrlXzGmewe6++6cZDQQYi0aztwLK798oc9j0LsccdMpqWrjqoU1uANFhYIuUu/T47TEhT+e6M+KFYK5TR998eJTO25IjdN2Tgw0feXhQFF/nngbol0bA4auSPaZQsgokKK+E+Q/8UtBdetEofuV user@hostname
    

    PEM で暗号化されている秘密鍵には、ファイルの上部に次のようなヘッダーがあります。

    -----BEGIN RSA PRIVATE KEY-----
    

    そして、最後に次の行があります。

    -----END RSA PRIVATE KEY-----
    

    AuthenticateWithKeyboardInteractive

    これは、Cache 2018.1 以降で提供されている新しいオプションです。 チャレンジレスポンス認証を実行できます。 たとえば、テキストメッセージで送信されるか、Google 認証システムアプリで生成されたワンタイムパスワードを要求することがあるでしょう。 この認証方式を使用するには、サーバーが送信するプロンプトを処理するためのラムダ関数を記述する必要があります。

    この認証方式を、ユーザーのパスワード認証と同じように、ユーザー名とパスワードのプロンプトのみで使用しているサーバーに遭遇することがあるかもしれませんが、 以下に説明する SSH デバッグフラグを使えば、これに遭遇しているかどうかを判定しやすくなります。

    認証に関する注意事項: 1 つの接続で 2 つの認証方式の使用を検討している方は、Cache 2018.1 または InterSystems IRIS を使用してください。 これらのバージョンには、鍵ペアとユーザー名といった、複数の方式を使用できるようにするための更新があります。

    問題が発生した場合

    発生する可能性のある一般的なエラー

    バナーの取得に失敗しました(Failed getting banner)

    これは次のように表示されます。

    ERROR #7500: SSH Connect Error '-2146430963': SSH Error [8010100D]: Failed getting banner [FFFFFFFF8010100D] at Session.cpp:231,0
    

    SSH クライアントが最初に行うのは、バナーの取得です。 このエラーが発生した場合、適切なサーバーに接続しており、それが SFTP サーバーであることを確認してください。

    たとえば、サーバーが実際には FTPS サーバーであった場合に、このエラーが発生します。 FTPS サーバーは SSH ではなく SSL を使用するため、%Net.SSH.Session クラスでは動作しません。 FTPS サーバーに接続するには、%Net.FtpSession クラスを使用してください。

    暗号化鍵を交換できません(Unable to exchange encryption keys)

    このエラーは次のように表示されます。

    ERROR #7500: SSH Connect Error '-2146430971': SSH Error [80101005]: Unable to exchange encryption keys [80101005] at Session.cpp:238,0
    

    このエラーは通常、クライアントとサーバーの暗号化または MAC アルゴリズムが合致しなかったことを指しています。 これが発生した場合は、新しいアルゴリズムのサポートを追加するために、クライアントかサーバーのいずれかをアップグレードする必要があるかもしれません。

    2017.1 より前のバージョンの Cache を使用している場合は、2017.1 以降を使用することをお勧めします。 libssh2 ライブラリは 2017.1 でアップグレードされており、新しいアルゴリズムがいくつか追加されています。

    詳細については、以下に説明するデバッグフラグが提供するログを参照してください。

    提供された公開鍵の署名が無効です(Invalid signature for supplied public key)

    Error [80101013]: Invalid signature for supplied public key, or bad username/public key combination [80101013] at Session.cpp:418
    

    これは非常に誤解を招きやすいエラーです。 サーバーが 2 つの認証方式を必要としているにも関わらず、1 つしか提供しなかった場合に発生します。 この場合は、そのまま続けて次の方式を試しましょう! このエラーがあっても、すべてうまく動作する可能性があります。

    Error -37

    エラー -37 に関するメッセージが表示されることがあります。 たとえば、次のデバッグログを見てください。

    [libssh2] 0.369332 Failure Event: -37 - Failed getting banner
    

    エラー -37 が示されている場合は必ず、失敗した操作が再試行されます。 このエラーが最終的な失敗の原因であることはありません。 ほかのエラーメッセージを確認してください。

    SSH デバッグフラグ

    接続に SSH デバッグフラグを使うと、SSH 接続の詳細なログを取得できます。 このフラグを有効にするには、SetTraceMethod メソッドを使います。 次に、このフラグを使った接続の例を示します。

    Set SSH = ##class(%Net.SSH.Session).%New()
    Do SSH.SetTraceMask(511,"/tmp/ssh.log")  
    Set Status=SSH.Connect("ftp.intersystems.com")​ 
    

    SetTraceMask の最初の引数は、何を収集するかを指示します。 ビットの 10進表現です。 511 は 512 を除くすべてのビットを要求しており、最も一般的に使用される設定です。 各ビットに関する詳細については、%Net.SSH.Session クラスのクラスドキュメントをご覧ください。

    2 つ目の引数は、接続に関するログ情報をどのファイルに格納するかを指示します。 この例では、/tmp/ssh.log ファイルを指定しましたが、任意の絶対または相対パスを使用できます。

    上記の例では、Connect メソッドのみを実行しました。 認証に問題がある場合は、該当する認証方式も実行する必要があります。

    テストを実行したら、ログファイルで情報を確認できます。 ログファイルの解釈に不安がある場合は、WRC をご覧ください。

    0
    0 1742