#チュートリアル

0 フォロワー · 39 投稿

技術的な機能またはソリューションの段階的な説明を含む投稿。

記事 Toshihiko Minamoto · 5月 31, 2021 3m read

以下の記事では、DeepSee のより柔軟なアーキテクチャ設計の概要を説明します。 前の例で説明したとおり、この実装には、DeepSee キャッシュや DeepSee の実装と設定、および同期グローバル用の個別のデータベースが含まれています。 この例では、DeepSee インデックスを保存するための新しいデータベースを紹介します。 DeepSee インデックスがファクトテーブルや次元テーブルとともにマッピングされないように、グローバルマッピングを再定義します。

例 3: 完全に柔軟なアーキテクチャ

データベース

APP-FACT データベースはファクトテーブルと次元テーブルしか保存しないのに対し、アナリティクスサーバーは、中間的な例で前に定義したデータベースに加え、インデックスを保存する APP-INDEX データベースを導入します。 インデックスをファクトテーブルから分離するのは、インデックスのサイズが大きくなる可能性があるため、パフォーマンスを向上させるために APP-FACT のブロックサイズを変更することができるからです。

前の例のように、ファクトテーブルとインデックスのジャーナリングはオプションで有効にできます。 詳細については、前の記事の注意事項をお読みください。

グローバルマッピング

次のスクリーンショットは、上記の実装例のマッピングを示しています。 ^DeepSee.Index グローバルのマッピングは、新たに作成された APP-INDEX データベースに保存されるように変更されています。 中間の例と同様に、^DeepSee.Fact* と ^DeepSee.Dimension* グローバルのマッピングは引き続き、ファクトテーブルと次元テーブルを APP-FACT データベースに保存するために使用されています。 クエリログと最後の MDX クエリは、オプションで DeepSee キャッシュとともに保存されます。

コメント

このアーキテクチャの例は最も高い柔軟性を備えていますが、ネームスペースごとに 5 つのデータベースを作成する必要があります。 2 つ目の例のように、DeepSee キャッシュはジャーナリングが無効になっている専用のデータベースにマッピングされており、同期グローバルは APP-DSTIME にマッピングされています。

ファクトテーブルとインデックスをマッピングすると、DeepSee の実装と設定をジャーナリングされる専用のデータベース( APP-DEEPSEE)に保存できるため、DeepSee 実装の復元を簡単に行えるようになります。 多くの場合、対応するグローバルをファクトテーブルと共に APP-FACT に保存するだけで充分であるため、インデックス用のデータベースを個別に作成するのはオプションです。

この連載の最後の記事には、3 つの例で使用したデータベースの要約とリストを記載します。

0
0 154
記事 Toshihiko Minamoto · 5月 27, 2021 5m read

以下の記事では、DeepSee の中程度の複雑さのアーキテクチャ設計を説明します。 前の例で説明したとおり、この実装には、DeepSee キャッシュや DeepSee の実装と設定用の個別のデータベースが含まれています。 この記事では、同期に必要なグローバルの保存用と、ファクトテーブルとインデックスの保存用に、2 つの新しいデータベースを紹介します。

例 2: より柔軟な設計

データベース

前の例で紹介した APP-CACHE と APP-DEEPSEE データベースのほかに、APP-DSTIME と APP-FACT データベースを定義します。

APP-DSTIME データベースには DeppSee の同期グローバルである ^OBJ.DSTIME と ^DeepSee.Update が含まれます。 これらのグローバルは、運用サーバーにある(ジャーナリングされた)データベースからミラーリングされています。 APP-DSTIME データベースは、^DeepSee.Update を使用して caché バージョンの読み取りと書き込みができる必要があります。

APP-FACT データベースは、ファクトテーブルとインデックスを保存します。 ファクトテーブルからインデックスを分離するのは、インデックスのサイズが大きくなる可能性があるためです。 APP-FACT を定義することで、ジャーナル設定の柔軟性をより高めたり、デフォルト以外のブロックサイズを定義したりすることができます。 APP-FACT データベースのジャーナリングはオプションで有効にできますが、 この選択は主に、中断が発生した場合にキューブを再構築する際に、アナリティクスが利用できないままとなるかどう通って決まります。 この例では、ファクトテーブルとインデックスのジャーナリングは無効になっています。無効にする一般的な理由には、キューブサイズが小さいこと、キューブの再構築を比較的素早く行えること、そして定期的な再構築が頻繁に行われることがあります。 より詳しい説明は、下の方にある注意事項をお読みください。

 

グローバルマッピング

次のスクリーンショットは、上記の実装例のマッピングを示しています。

DeepSee 同期グローバルの ^OBJ.DSTIME と ^DeepSee.Update は、APP-DSTIME データベースにマッピングされています。 ^DeepSee.LastQuery と ^DeepSee.QueryLog グローバルは、実行されるすべての MDX クエリのログを定義します。 この例では、これらのグローバルは DeepSee キャッシュとともに APP-CACHE データベースにマッピングされています。 これらのマッピングはオプションです。

^DeepSee.Fact* と ^DeepSee.Dimension* グローバルは、ファクトテーブルと次元テーブルを保存しますが、^DeepSee.Index グローバルは DeppSee インデックスを定義します。 これらのグローバルは、APP-FACT データベースにマッピングされています。

コメント

基本的な例のように、DeepSee キャッシュは、ジャーナリングが無効になっている専用のデータベースに正しく保存されています。 DeepSee の実装と設定は、DeepSee 実装を復元できるように、ジャーナリングされたデータベースに個別にマッピングされています。

同期をサポートするグローバルは APP-DSTIME にマッピングされ、プライマリでジャーナリングされています。

ファクトテーブルとインデックスを専用のデータベースにマッピングすると、DeepSee の実装と設定をジャーナリングされる専用のデータベース( APP-DEEPSEE)に保存できるため、DeepSee 実装の復元を簡単に行えるようになります。

最後の 3 つ目の例では、APP-FACT データベースのマッピングを再定義し、DeepSee インデックスのデータベースを作成します。

ジャーナリングとキューブの構築に関する注意事項

キューブを構築するとキューブのファクトとインデックステーブルが削除されて再作成されることに注意してください。 つまり、ジャーナリングが有効である場合、^DeepSee.Fact* や ^DeepSee.Index などのグローバルの SET や KILL がジャーナルファイルに記録されるということです。 その結果、キューブを再構築すると、ジャーナルファイルのエントリが膨大化し、ディスク容量に問題が生じる可能性があります。

ファクトテーブルとインデックスを 1 つか 2 つの別々のデータベースにマッピングすることをお勧めします。

ファクトおよびインデックスデータベースにおいては、ジャーナリングはオプションであり、ビジネスのニーズに基づきます。 キューブが比較的小さく、素早く構築できる場合や、キューブの定期的な再構築が計画されている場合には、ジャーナリングを無効にすることをお勧めします。

キューブが比較的大きく、再構築に時間が掛かる場合には、このデータベースのジャーナリングを有効にします。 キューブが安定しており、定期的に同期されるだけで構築は行われない場合には、ジャーナリングを有効にしておくのが理想的と言えます。 キューブを安全に構築する方法の 1 つとして、ファクトデータベースのジャーナリングを一時的に無効にすることが挙げられます。

0
0 226
記事 Toshihiko Minamoto · 5月 24, 2021 3m read

以下の記事は、DeepSee の基本的なアーキテクチャを実装するためのガイドです。 この実装には、DeepSee キャッシュ用のデータベースと DeepSee 実装と設定用のデータベースが含まれています。

例 1: 基本アーキテクチャ

データベース

アナリティクスサーバー用のこの構成には、APP-CACHE と APP-DEEPSEE データベースが含まれています。 DeepSee が円滑に実行するためには、DeepSee キャッシュを決してジャーナリングしないことが重要な設定となります。 ジャーナリングしてしまうと、ハイパージャーナリングやディスク容量の問題が発生するだけでなく、DeepSee エンジンのパフォーマンスが低下してしまいます。 このため、DeepSee キャッシュは、ジャーナリングが無効になっている別の DeepSee Cache データベース「APP-CACHE」に格納されます。

APP-DEEPSEE は、^DeepSee.* グローバルが含まれる、DeepSee の実装と設定用のデータベースです。  これらのグローバルは、定義と設定、Cube Manager、ユーザー設定など、ほとんどの DeepSee 実装を定義しています。 次に示すスクリーンショットに見られるように、すべてのデータベースは読み取り/書き込みが可能であり、APP-DEEPSEE でのみジャーナリングが有効となるように決定されていることに注意してください。 このデータベースにはすべての定義、設定、およびユーザーデータが含まれているため、これをジャーナリングすることをお勧めします。

 

グローバルマッピング

次のスクリーンショットは、APP ネームスペース上のこの基本アーキテクチャ実装のマッピングを示しています。 ^DeepSee.Cache.* と ^DeepSee.JoinIndex は DeepSee キャッシュを APP-CACHE データベースにマッピングしています。 ^DeepSee.* グローバルはとりわけ、DeepSee の実装と設定を APP-DEEPSEE データベースにマッピングしています。

 

コメント

基本アーキテクチャのこの例では、DeepSee キャッシュは専用のデータベースに保存されています。 このため、^DeepSee.Cache* と ^DeepSee.JoinIndex グローバルのジャーナリングを無効にすることができます。

中断が発生した場合に DeepSee 実装(キューブ、サブジェクトエリア、DeepSee アイテム、ユーザー設定など)の復元を実現できるのが、APP-DEEPSEE データベースのジャーナリングです。

この例に説明されている構成には、いくつかの欠点があります。 まず、同期をサポートするグローバルが処理されていない点です。 2 つ目は、APP-DEEPSEE データベースには、ファクトテーブル、インデックス、およびその他の DeepSee グローバルも含まれている点です。 そのため、APP-DEEPSEE のサイズが肥大し、ジャーナリングと復元が実用的でなくなる可能性があります。 この構成は、たとえばキューブに大量のデータが含まれていない場合などに適用できます。

この連載の次の例では、キューブ同期グローバル、ファクトテーブル、およびインデックスを個別のデータベースにマッピングする方法を説明します。

0
0 242
記事 Toshihiko Minamoto · 5月 20, 2021 9m read

以下の記事は、この連載の締めくくりとして、完全に柔軟なアーキテクチャの例で確認されたすべてのデータベースのリストを掲載しています。

データベースとマッピング

以下で説明するデータベースは、ネームスペース間で共有する必要のあるアプリケーションコードを除き(例では APP-CODE データベースに格納されています)、ネームスペースごとに定義されている必要があります。 DeepSee 実装が実行するすべてのネームスペースはグローバルマッピングを使用し、グローバルが正しいデータベースに保存されて読み取られるようにする必要があります。

データベース 1: DeepSee キャッシュ

このデータベースはすべての DeepSee キャッシュ(^DeepSee.Cache.* および ^DeepSee.JoinIndex グローバル)を保存する必要があります(注意: ドキュメントのこちらのページに、さらに多くのグローバルが DeepSee キャッシュとしてリストされていますが、^DeepSee.Cache.* グローバルが明らかに最も重要なグローバルです)。

DeepSee キャッシュグローバルを専用のデータベースにマッピングすることを強くお勧めします。 DeepSee キャッシュグローバルは決してジャーナル化されてはいけません。ジャーナル化してしまうと、DeepSee のパフォーマンスが低下し、ジャーナルファイルが巨大化する可能性があります。

^DeepSee.Cache.* と ^DeepSee.JoinIndex グローバルをこのデータベースにマッピングします。 必要に応じて、^DeepSee.LastQuery と ^DeepSee.QueryLog グローバルもこのデータベースにマッピングします。これらは実行されたすべての MDX クエリのログを保存するグローバルです。

データベース 2: 実装と設定

このデータベースには、DeepSee 実装のほとんどが含まれている ^DeepSee.* グローバルが含まれています。 このデータベースには、すべての DeepSee キューブまたはサブジェクトエリアの定義のほか、Cube Manager(^DeepSee.CubeManager*)、キューブの定義と設定(^DeepSee.Cubes、^DeepSee.Dimensions)、DeepSee のアイテム(^DeepSee.Folder*、^DeepSee.FolderItem*)、ピボット変数(^DeepSee.Variables)、用語リスト(DeepSee.TermList)、ユーザー設定(^DeepSee.DashboardSettings)、DeepSee オーバーライド(^DeepSee.Overrides)などの多数の機能に関する情報も含まれています。

これらの機能は別の読み取り/書き込み可能なデータベースに保存し、そのデータベースにジャーナリングを実行して定期的にバックアップすることをお勧めします。 そうすれば、何らかの中断が生じた場合でも、すべての定義、設定、およびユーザーデータを復元することが可能になります。

残りのすべての ^DeepSee* グローバルをこのデータベースにマッピングします。

データベース 3: DeepSee の更新

DeepSee は、ソーステーブルでキューブを最新の状態に維持するために、^OBJ.DSTIME と ^DeepSee.Update グローバルを使用しています。 運用データベースでは、このデータベースに ^OBJ.DSTIME グローバルを保存し、アナリティクスサーバーにミラーリングします。 システムがアドホックまたは最新バージョンの Caché で実行している場合、これらには ^DeepSee.Update が使用されているため(通常、Caché 2016.1.2 以降で利用可能)、このデータベースにも ^DeepSee.Update が保存されます。 この場合、^OBJ.DSTIME を保存しているアナリティクスサーバーのデータベースは、読み取り/書き込み可能であり、^OBJ.DSTIME が ^DeepSee.Update にコピーされた後に、それをパージできる必要があります。 データベースホスティングデータ(この例では APP-DATA)が読み取り専用の場合、このデータベースを使用する必要があることに注意してください。使用しない場合、^OBJ.DSTIME をパージするのは不可能です。

運用サーバーでは、ジャーナリングが有効になっている必要があります。 ^OBJ.DSTIME と ^DeepSee.Update をこのデータベースにマッピングします。

データベース 4: ファクトテーブル

DeepSee のキューブはソースクラスに基づいていますが、ファクトテーブルと次元テーブルにデータを入力して使用します。 これらのテーブルには、キューブに組み込まれた各レコードの情報が含まれており、ランタイム時に DeepSee によって使用されます。

ファクトテーブル、次元テーブル、およびインデックス用の専用データベースを定義するのは通常、データベースごとに異なるジャーナリングの設定を適用するためです。 ジャーナリングが有効である場合のキューブの構築について、以下の注意事項をお読みください。 ファクトテーブル、次元テーブル、およびインデックスを別のデータベースにマッピングするもう 1 つの理由は、デフォルト以外のブロックサイズを定義することができるからです(デフォルトの 8000 ブロックではなく 16000 ブロックにするなど)。 異なるブロックサイズを使用することで、MDX クエリのパフォーマンスを向上させることができます。

ファクトテーブルと次元テーブルは、^DeepSee.Fact* と ^DeepSee.Dimension* グローバルに保存されています。 DeepSee インデックスは ^DeepSee.Index に保存され、キューブがリレーションを定義するときに ^DeepSee.JoinIndex グローバルが使用されます。 これらのグローバルはこのデータベースにマッピングします。

データベース 5: DeepSee インデックス

DeepSee インデックスは、キューブのファクトテーブルのインデックスです。

DeepSee インデックスを別のデータベースに保存するのは、^DeepSee.Index グローバルのサイズが大きくなる可能性があるためです。 異なるジャーナリング設定を使用し、デフォルト以外のブロックサイズを定義すると、復元を簡単に行えるようになり、パフォーマンスの改善にも役立ちます。

ジャーナリングはオプションです。前のデータベースと同じ設定を選択してください。

^DeepSee.Index グローバルはこのデータベースにマッピングします。

ジャーナリングとキューブの構築に関する注意事項

キューブを構築するとキューブのファクトとインデックステーブルが削除されて再作成されることに注意してください。 つまり、ジャーナリングが有効である場合、^DeepSee.Fact* や ^DeepSee.Index などのグローバルの SET や KILL がジャーナルファイルに記録されるということです。 その結果、キューブを再構築すると、ジャーナルファイルのエントリが膨大化し、ディスク容量に問題が生じる可能性があります。

ファクトテーブルとインデックスを 1 つか 2 つの別々のデータベース(上記のデータベース 4 とデータベース 5)にマッピングすることをお勧めします。

ファクトおよびインデックスデータベースにおいては、ジャーナリングはオプションであり、ビジネスのニーズに基づきます。 キューブが比較的小さく、素早く構築できる場合や、キューブの定期的な再構築が計画されている場合には、ジャーナリングを無効にすることをお勧めします。

キューブが比較的大きく、再構築に時間が掛かる場合には このデータベースのジャーナリングを有効にします。 キューブが安定しており、定期的に同期されるだけで構築は行われない場合には、ジャーナリングを有効にしておくのが理想的と言えます。 キューブを安全に構築する方法の 1 つとして、ファクトデータベースとインデックスデータベース(順にデータベース 4 と 5)のジャーナリングを一時的に無効にすることが挙げられます。

要約

  <td width="125">
    マッピングするグローバル
  </td>
  
  <td width="119">
    機能
  </td>
  
  <td width="143">
    設定
  </td>
</tr>

<tr style="height:0pt">
  <td>
    1 - ソースデータ
  </td>
  
  <td>
     
  </td>
  
  <td>
    本番システムからデータを取得する
  </td>
  
  <td>
    本番システムからミラーリング<br>すべてのネームスペースで共有
  </td>
</tr>

<tr style="height:0pt">
  <td>
    2 - ソースコード
  </td>
  
  <td>
     
  </td>
  
  <td>
    コードをデータから切り離す
  </td>
  
  <td>
    すべてのネームスペースで共有
  </td>
</tr>

<tr style="height:0pt">
  <td>
    3 - DeepSee キャッシュ
  </td>
  
  <td>
    ^DeepSee.Cache.*<br>^DeepSee.JoinIndex<br>^DeepSee.LastQuery<br>^DeepSee.QueryLog
  </td>
  
  <td>
    ほかのデータベースのジャーナリングを有効にしたまま、DeepSee キャッシュのジャーナリングを無効にできる
  </td>
  
  <td>
    ジャーナリングを無効化
  </td>
</tr>

<tr style="height:0pt">
  <td>
    4 - 実装と設定
  </td>
  
  <td>
    ^DeepSee.*
  </td>
  
  <td>
    DeepSee の実装とユーザー設定の復元を可能にする
  </td>
  
  <td>
    ジャーナリングを有効化、定期的にバックアップ
  </td>
</tr>

<tr style="height:0pt">
  <td>
    5 - DeepSee の更新
  </td>
  
  <td>
    ^OBJ.DSTIME<br>^DeepSee.Update
  </td>
  
  <td>
    キューブを最新の状態に維持する
  </td>
  
  <td>
    本番システムからミラーリング<br>読み取り/書き込みを維持
  </td>
</tr>

<tr style="height:0pt">
  <td>
    6 - ファクトテーブル
  </td>
  
  <td>
    ^DeepSee.Dimension*<br>^DeepSee.Fact<br>^DeepSee.JoinIndex
  </td>
  
  <td>
    ジャーナリングはオプション<br>ブロックサイズを変更可能
  </td>
  
  <td>
    ジャーナリングはオプション
  </td>
</tr>

<tr style="height:0pt">
  <td>
    7 - DeepSee インデックス
  </td>
  
  <td>
    ^DeepSee.Index
  </td>
  
  <td>
    キューブが大きく、クエリや構築のパフォーマンスを改善する必要がある場合はこのデータベースを定義。そうでない場合は、ファクトテーブルとともに保存(データベース 5)
  </td>
  
  <td>
    ファクトテーブルデータベースと同様のジャーナリング
  </td>
</tr>
データベース

最後に

この連載では、Caché と DeepSee を使用したビジネスインテリジェンスの実装に関して考慮する必要のあるデータベースとマッピング関連のベストプラクティスを説明しました。 この連載で推奨したデータベースより少ない数のデータベースを使って DeepSee 実装をデプロイすることはもちろん可能ですが、実装に制限がかかる可能性があります。

0
0 146
記事 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 · 3月 10, 2021 9m read

デベロッパーの皆さん、こんにちは!

最近、当社は InterSystems Package Manager (ZPM) をリリースしました。 ZPM を開発した理由の 1 つは、ソリューションをパッケージ化して ZPM レジストリに提出することにより、そのデプロイを「install xxx package」のようなコマンドを実行するだけの単純な作業にするためです。

これを行うには、InterSystems IRIS パッケージの中身を説明する module.xml ファイルをリポジトリに導入する必要があります。

この記事では、module.xml ファイルの異なる構成要素を説明し、独自のファイルを作成する方法をご紹介します。

まずは、samples-objectscript パッケージから始めます。以下のコマンドを実行すれば、IRIS に ObjectScript のサンプルアプリケーションがインストールされます。

zpm: USER>install samples-objectscript

おそらく、これほどシンプルなパッケージはないと思います。以下は、パッケージの中身を説明する module.xml です。


<Export generator="Cache" version="25">
  <Document name="samples-objectscript.ZPM">
    <Module>
      <Name>samples-objectscript</Name>
      <Version>1.0.0</Version>
      <Packaging>module</Packaging>
      <SourcesRoot>src</SourcesRoot>
      <Resource Name="ObjectScript.PKG"/>
    </Module>
  </Document>
</Export>

それでは、文書の中身を一行ずつ見ていきましょう。

module.xml は、Cache/IRIS の XML 文書のファミリーに属しているため、この行はその関係性を示すもので、内部ライブラリがこの文書を認識できるようにしています。

次のセクションはこちら。 <Document> 

  <Document name="samples-objectscript.ZPM">

パッケージには名前を付けます。 名前には小文字と「-」の記号を含めることができます。 (この場合なら samples-objectscript)。 パッケージ名は、拡張子に「.ZPM」を使い、Document タグの name 節に入れてください。

Document の内部要素は以下の通りです。

<Name> - パッケージ名。 今回は、

<Name>samples-objectscript</Name>

<Version> - パッケージバージョン。 今回は、

<Version>1.0.0</Version>

<Packaging>モジュール</Packaging> - パッケージの種類。 モジュールパラメーターは以下のように入力します。

<Packaging>module</Packaging>

<SourcesRoot> - ZPM がインポートする ObjectScript を探すフォルダー。 

今回は「/src」フォルダーで ObjectScript を探すよう指示します。

<SourcesRoot>src</SourcesRoot>

<Resource Name> - インポートする ObjectScript の要素。 パッケージやクラス、include、グローバル、dfi などが含まれます。 

SourceRoot フォルダー以下の構造は次のようになります。

/cls - Folder=Package 内にある Class=file.cls 形式のすべての ObjectScript クラス。 サブパッケージはサブフォルダーです。

/inc - file.inc 形式のすべての include ファイル。

/mac - すべての mac ルーチン。 

/int - すべての「中間」ルーチン (別名「他のコード」、mac コードをコンパイルした結果、またはクラスやマクロを持たない ObjectScirpt)。

/gbl - XML 形式でエクスポートされるすべてのグローバル。

/dfi - XML 形式でエクスポートするすべての DFI ファイル。 各ピボットは pivot.dfi ファイルに、各ダッシュボードは dashboard.dfi ファイルに作成されます。

例えば、 ObjectScript ページをインポートするとします。 これをうけて、ZPM は /src/cls/ObjectScript フォルダーの中を見て、そこからすべてのクラスをインポートします。

<Resource Name="ObjectScript.PKG"/>

なので、 パッケージ化するソリューションを作成するには、ObjectScript クラスを「/cls」フォルダー内にあるリポジトリのいずれかのフォルダーに入れ、すべてのパッケージとクラスを class=file.cls 形式の package=folder に入れます。

クラスを違うかたちでリポジトリに保管したいけれども、いちいち手作業で ObjectScript 用に適切なフォルダー構造を作成するのは避けたいという場合、それを解決してくれるツールはたくさんあります。例えば、Atelier や VSCode Object Script は、まさにその条件が満たされるかたちでクラスをエクスポートしますし、他にはパッケージ化する準備ができているアーチファクトをネームスペースからすべてエクスポートしてくれる isc-dev ユーティリティなどがあります。

mac ルーチンのパッケージ化

これはクラスの場合とよく似ています。 ルーチンを /mac フォルダーに入れるだけです。 例はこちら。


<Export generator="Cache" version="25">
  <Document name="DeepSeeButtons.ZPM">
    <Module>
      <Name>DeepSeeButtons</Name>
      <Version>0.1.7</Version>
      <Packaging>module</Packaging>
      <SourcesRoot>src</SourcesRoot>
      <Resource Name="DeepSeeButtons.mac"/>
    </Module>
  </Document>
</Export>

その他の要素

また、以下のような、オプションとして使える要素もあります。
<Author>

<Organization> と <CopyrightDate> の要素が含まれる場合があります。

例:

<Author>
        <Organization>InterSystems</Organization>
        <CopyrightDate>2019</CopyrightDate>
      </Author>

 

CSP/Web アプリケーションのパッケージ化

ZPM はウェブアプリケーションもデプロイできます。

これを成功させるには、CSPApplication 要素を CSPApplication パラメーターの節と一緒に導入します。 

例えば、DeepSeeWeb の module.xml にある CSPApplication タグをご覧ください。

<CSPApplication
        Url="/dsw"
        DeployPath="/build"
        SourcePath="${cspdir}/dsw"
        ServeFiles="1"
        Recurse="1"
        CookiePath="/dsw"
       />

この設定により、/dsw という名前のウェブアプリケーションが作成され、リポジトリの /build フォルダーにあるすべてのファイルが IRIS csp ディレクトリの ${cspdir}****/dsw フォルダーにコピーされます。

REST API アプリケーション

これが REST-API アプリケーションである場合、CSPApplication 要素はディスパッチクラスを含み、MDX2JSON module.xml のように構成される可能性があります。

<CSPApplication
    Path="/MDX2JSON"
    Url="/MDX2JSON"
    CookiePath="/MDX2JSON/"
    PasswordAuthEnabled="1"
    UnauthenticatedEnabled="1"
    DispatchClass="MDX2JSON.REST"
    />

依存関係

モジュールをインストールする際には、ターゲットシステムに別のモジュールがインストールされている場合があります。 これは、複数の <ModuleReference> 要素を含み得る <Document> 要素の中にある <Dependencies> 要素により 説明されている場合があります。 このそれぞれに、<Name> と <Version> があり、 また先にインストールされているべき 他のモジュールとそれらのバージョンが記述されています。 こういった状況では、ZPM はモジュールがインストールされているかどうかを確認し、インストールされていなければ、インストールを実行します。

こちらは、DSW モジュールの MDX2JSON モジュールに対する依存関係を示した例です。

<Dependencies>
        <ModuleReference>
          <Name>MDX2JSON</Name>
          <Version>2.2.0</Version>
        </ModuleReference>
      </Dependencies>

以下も依存関係のです。ThirdPartyPortletsSamples BI(holefoods) に依存しています。

<Dependencies>
        <ModuleReference>
          <Name>holefoods</Name>
          <Version>0.1.0</Version>
        </ModuleReference>
      </Dependencies>

また、任意のコードを実行して、データと環境をセットアップするというオプションもあります。これについては、次回の記事で解説します。

独自のパッケージをビルドする方法

それでは、 module.xml ができたら、パッケージをビルドして、module.xml の構造が正しいかどうかをテストすることができます。

テストは、ZPM クライアントを使って実行できます。 ZPM を IRIS システムにインストールし、以下の読み込みコマンドでパッケージのコードを読み込みます。

zpm: NAMESPACE>load path-to-the-project

パスは、パッケージのリソースを含み、かつルートフォルダーに module.xml を持つフォルダーを指しています。 

例えば、 パッケージのビルドは、こちらのプロジェクトを使ってテストできます。 チェックアウトしたら、docker-compose-zpm.yml を使ってコンテナをビルドします。

SAMPLES ネームスペースでターミナルを開き、ZPM を呼び出します。

zpm: SAMPLES>

zpm: SAMPLES>load /iris/app

[samples-objectscript]  Reload START
[samples-objectscript]  Reload SUCCESS
[samples-objectscript]  Module object refreshed.
[samples-objectscript]  Validate START
[samples-objectscript]  Validate SUCCESS
[samples-objectscript]  Compile START
[samples-objectscript]  Compile SUCCESS
[samples-objectscript]  Activate START
[samples-objectscript]  Configure START
[samples-objectscript]  Configure SUCCESS
[samples-objectscript]  Activate SUCCESS

パスが「/iris/app」になっているのは、docker-compose-zpm.yml の中でプロジェクトのルートをコンテナの「/iris/app」フォルダーにマップすると指定しているためです。 したがって、このパスを使えば、ZPM にプロジェクトの読み込み元を指定することができます。

ついに、 読み込みに成功しました。 つまり、パッケージをデベロッパーコミュニティのリポジトリに提出する際にこの module.xml を使えるということです。

以上が、アプリケーションに使う適切な module.xml ファイルを作成する方法です。 

InterSystems コミュニティのリポジトリにアプリケーションを提出する方法

現時点で、要件は 2 つあります。

  1. アプリケーションが Open Exchange に記載されている。

  2. Community Package Manager のリポジトリにアプリケーションを提出することをご希望の方は、私までダイレクトメッセージをお送りいただくか、この記事のコメント欄よりお知らせください。

module.xml が正常に動作していることをご確認ください!)

0
0 249
記事 Toshihiko Minamoto · 3月 9, 2021 16m read

これまで Caché のリソースアクセスを制御する方法が存在するかどうかを疑問に思っていた方の悩みを解決しました。 バージョン 2014.2 では、開発者がセマフォを操作できるようにする特別なクラスが追加されました。 セマフォは基本的に負ではない整数値の変数であり、次の 2 種類の操作の影響を受ける可能性があります。

  • セマフォの P 操作は、セマフォの値を 1 つ減らそうとするものです。 P 操作を実行する前のセマフォの値が 1 より大きい場合、P 操作は遅滞なく実行されます。 操作前のセマフォの値が 0 の場合、P 操作を実行するプロセスは値が 0 より大きくなるまで待機状態に切り替わります。
  • セマフォの V 操作は、セマフォの値を 1 つ増やすものです。 このセマフォでの P 操作を実行中に遅延したプロセスがあった場合、これらのプロセスのいずれかが待機状態を終了し、P 操作を実行できます。

セマフォには、バイナリとカウンティングの 2 種類があります。 前者の場合は変数が 2 つの値(0 と 1)のみに制限されており、後者の場合は負ではない任意の整数をとることができるという点が異なります。


セマフォは次のようないくつかの事例で役立ちます。

1. セマフォによる排他制御

バイナリセマフォ S は、2 つ以上のプロセスによる共有データの同時変更の防止などの排他制御を実装するために作成されます。 このセマフォの初期値は 1 です。 クリティカルセクション(同時に 1 つのプロセスでのみ実行できるセクション)は、P(S)(最初)と V(S)(最後)の括弧で囲まれています。 クリティカルセクションに入るプロセスは P(S)操作を実行し、セマフォを 0 に切り替えます。 クリティカルセクションに別のプロセスがあるためにセマフォの値がすでに 0 になっている場合、このプロセスは現在のプロセスが終了し、終了時に V(S)操作を実行するまで P 操作がブロックされます。

2. セマフォによる同期

初期値が 0 のバイナリセマフォ S は同期を行う目的で作成されます。 初期値 0 は、イベントがまだ発生していないことを意味します。 イベントについて通知するプロセスは、値を 1 に設定する V(S)操作を実行します。 イベントを待機しているプロセスは、P(S)操作を実行します。 その時点ですでにイベントが発生している場合は、待機中のプロセスが引き続き実行されます。 そうでない場合、プロセスはシグナル送信プロセスが V(S)操作を実行するまで待機状態に切り替わります。

複数のプロセスが同じイベントを待機している場合、P(S)操作を正常に実行したプロセスは即座に V(S)操作を実行し、次のキューに格納されたプロセスに新たなイベント信号を送信する必要があります。

3. セマフォリソースカウンター

特定のリソースが N ユニットある場合、その割り当てを制御する目的で値が N の一般的な S セマフォが作成されます。 リソースは P(S)コマンドで割り当てられ、V(S)コマンドで解放されます。 したがって、セマフォの値には空きリソース単位の数が反映されます。 セマフォの値が 0 の場合は使用可能なユニットがそれ以上存在しないことを意味し、リソースを使用するいずれかのプロセスが V(S)操作を実行してそれを解放するまで、このリソースを要求するプロセスは P(S)操作を待機する状態に切り替わります。

これらのオプションはすべて Caché で負ではない 64 ビットの整数値をカプセル化し、動作中のすべてのプロセスに対してその値を変更するメソッドを提供する %SYSTEM.Semaphore クラスを使用して実装できます。

上記の 3 番目の事例でセマフォカウンターを使用する方法を見てみましょう。

大学内の国際的な科学論文データベースにアクセスできる 10 人分の枠があると仮定しましょう。 このように、アクセスを希望する学生間で共有させる必要のあるリソースがあるとします。 各学生にはデータベースにアクセスするためのログインとパスワードが個別に割り当てられていますが、システムを同時に操作できるのは大学の 10 人以下のユーザーだけです。 学生がデータベースにログインしようとするたびに、使用可能な枠の数を 1 つ減らす必要があります。 したがって、学生がアクセスを終了する際には 1 つの枠を全体のアクセスプールに戻す必要があります。

特定の学生にアクセスを許可するか、待機させるかを確認するため、初期値が 10 のセマフォカウンターを使用します。 このアクセス許可を付与する仕組みを確認するため、ログを記録します。 Main クラスは変数を初期化し、学生の操作を監視します。 別のクラスが %SYSTEM.Semaphore から継承され、セマフォが作成されます。 また、さまざまなユーティリティ用に別のクラスを作成しましょう。 そして最後の 2 つのクラスは、データベースからの学生のログインとログアウトをエミュレートします。 サーバーはシステムへのログインとログアウトを行うユーザーの名前を「認識」しているため、ここではアクティブユーザー(^LoggedUsers)に関する情報を格納するこの「認識」をシミュレートするために個別のグローバルを作成します。 このグローバルを使用してログイン中の学生のログイン名を登録し、ログアウトしている学生にはランダムな名前を選択します。

まずは次の処理を行う Main クラスから始めましょう。

  1. セマフォを作成します。

  2. セマフォを初期値(科学論文データベースにアクセスできる 10 人分の空き枠に対応する 10)に設定します。

  3. プロセスを停止し、セマフォを削除します。

  4. ログを表示します。

    Class SemaphoreSample.Main Extends %RegisteredObject [ ProcedureBlock ] {

     /// サンプルドライバー 
     ClassMethod Run()
     {
         // ログ記録用のグローバルを初期化 
         Do ##class(SemaphoreSample.Util).InitLog() 
         Do ##class(SemaphoreSample.Util).InitUsers()
    
         Set msg = "Process start " 
         Do ..Log(msg)
    
         // セマフォの作成と初期化 
         Set inventory = ##class(SemaphoreSample.Counter).%New() 
         If ('($ISOBJECT(inventory))) { 
             Set msg = "The SemaphoreSample.Counter %New() class method hasn’t worked yet" 
             Do ..Log(msg) 
             Quit 
         }
    
         // 初期セマフォ値を設定 
         if 'inventory.Init(10) { 
             Set msg = "There was a problem initializing the semaphore" 
             Do ..Log(msg) 
             Quit 
         }
    
         // プロセスの終了を待機 
         Set msg = "Press any key to block access..." 
         Do ..Log(msg)
    
         Read *x
    
         //セマフォの削除 
         Set msg = "The semaphore has been removed with the following status:  " _ inventory.Delete() 
         Do ..Log(msg) 
         Set msg = " Process end " 
         Do ..Log(msg)
    
         do ##class(SemaphoreSample.Util).ShowLog()
    
         Quit 
     }
    
     /// ログ書き込み用のユーティリティを呼び出す 
     ClassMethod Log(msg As %String) [ Private ]
     { 
         Do ##class(SemaphoreSample.Util).Logger($Horolog, "Main", msg) 
         Quit 
     }
    

    }

次に作成するクラスは、さまざまなユーティリティを備えたクラスです。 このクラスはテストアプリケーションの動作に必要になります。 また、次の処理を行うクラスメソッドが含まれています。

  1. グローバルにログを記録する準備をする(^SemaphoreLog)。

  2. ログをグローバルに保存する。

  3. ログを表示する。

  4. アクティブユーザーの名前をグローバルに保存する(^LoggedUsers)。

  5. アクティブユーザーのリストからランダムな名前を選択する。

  6. インデックスを指定してグローバルからアクティブユーザーの名前を削除する。

    Class SemaphoreSample.Util Extends %RegisteredObject [ ProcedureBlock ] {

    /// ログの初期化 ClassMethod InitLog() { // ログから古いレコードを削除 Kill ^SemaphoreLog Set ^SemaphoreLog = 0

       Quit 
    

    }

    /// ログの初期化 ClassMethod InitUsers() { //念のため、グローバルから全ユーザーを削除 if $data(^LoggedUsers) '= 0 { Kill ^LoggedUsers
    } Set ^LoggedUsers = 0 }

    /// グローバルにログを書き込み ClassMethod Logger(time As %DateTime, sender As %String, msg As %String) { Set inx = $INCREMENT(^SemaphoreLog) Set ^SemaphoreLog(inx, 0) = time Set ^SemaphoreLog(inx, 1) = sender Set ^SemaphoreLog(inx, 2) = msg Write "(", ^SemaphoreLog, ") ", msg_" в "_$ztime($PIECE(time,",",2), 1), ! Quit }

    /// 画面上にメッセージを表示 ClassMethod ShowLog() { Set msgcnt = $GET(^SemaphoreLog, 0) Write "Message log: number of records = ", msgcnt, !, ! Write "#", ?5, "Time", ?12, "Sender", ?25, "Message", !

       For i = 1 : 1 : msgcnt {
           Set time = ^SemaphoreLog(i, 0) 
           Set sender = ^SemaphoreLog(i, 1) 
           Set msg = ^SemaphoreLog(i, 2) 
           Write i, ")", ?5, $ztime($PIECE(time,",",2), 1), ?15, sender, ":", ?35, msg, ! 
       } 
       Quit 
    

    }

    /// ログイン済みユーザーのリストにユーザーを追加 ClassMethod AddUser(Name As %String) { Set inx = $INCREMENT(^LoggedUsers) set ^LoggedUsers(inx) = Name }

    /// ログイン済みユーザーのリストからユーザーを削除 ClassMethod DeleteUser(inx As %Integer) {
    kill ^LoggedUsers(inx) }

    /// ログイン済みユーザーのリストからユーザー名を選択 ClassMethod ChooseUser(ByRef Name As %String) As %Integer {
    // すべてのユーザーがログアウト中ならば、ログインを待つ必要があります if $data(^LoggedUsers) = 1 { Set Name = "" Quit -1 } else { Set Temp = "" Set Numb = $Random(10)+5 For i = 1 : 1: Numb { Set Temp = $Order(^LoggedUsers(Temp))
    // ループ対象のグローバルの 1 階層のみに制限するため // 通過するたびに最後にポインタを先頭に移動します if (Temp = "") { set Temp = $Order(^LoggedUsers("")) } } set Name = ^LoggedUsers(Temp) Quit Temp } } }

次のクラスはセマフォを実装しています。 また、%SYSTEM.Semaphore システムクラスを拡張しており、次のメソッドが含まれています。

  1. セマフォの一意の名前を返す。

  2. イベントをログに保存する。

  3. セマフォの作成と破棄のイベントをログに記録する(コールバックメソッド)。

  4. セマフォを作成して初期化する。

    Class SemaphoreSample.Counter Extends %SYSTEM.Semaphore {

    /// 各カウンターの名前は一意である必要があります ClassMethod Name() As %String { Quit "Counter" }

    /// ログ書き込み用のユーティリティを呼び出す Method Log(Msg As %String) [ Private ] { Do ##class(SemaphoreSample.Util).Logger($Horolog, ..Name(), Msg) Quit }

    /// 新規オブジェクトを作成するためのコールバックメソッド Method %OnNew() As %Status { Set msg = "Creating a semaphore " Do ..Log(msg) Quit $$$OK }

    /// セマフォの作成と初期化 Method Init(initvalue = 0) As %Status { Try { If (..Create(..Name(), initvalue)) { Set msg = "Created: """ _ ..Name() _ """; Initial value = " _ initvalue Do ..Log(msg) Return 1 } Else { Set msg = "There was a problem creating a semaphore with the name = """ _ ..Name() _ """" Do ..Log(msg) Return 0 } } Catch errobj { Set msg = "There was an error creating a semaphore: "_errobj.Data Do ..Log(msg) Return 0 } }

    /// オブジェクトのクローズに使用されるコールバックメソッド Method %OnClose() As %Status [ Private ] { Set msg = "Closing the semaphore " Do ..Log(msg) Quit $$$OK }

    }

セマフォが作成時にシステムに渡される名前で識別されることを認識しておいてください。 この名前はローカル/グローバル変数の要件に準拠し、一意でなければなりません。 セマフォは一般的に自身が作成されたデータベースインスタンスに保存されており、このインスタンスの他のプロセスからアクセスできます。 グローバル変数の命名要件を満たしているセマフォは、ECP を含むすべてのアクティブなプロセスで使用できるようになります。

最後の 2 つのクラスは、システムにログインおよびログアウトするユーザーをシミュレートします。 説明を簡単にするため、ちょうど 25 人のユーザーがいると仮定しましょう。 プロセスを開始した後にキーが押されるまで新規ユーザーを作成し続けることもできますが、ここでは単純に有限ループを使用することにしました。 どちらのクラスも既存のセマフォに接続し、カウンターを減らしたり(ログイン)、増やしたり(ログアウト)します。 ここでは学生が自分の順番を無期限に待つことを想定しています(学生はなんとしても図書室に入りたいと思っている)。そのため、待機時間を無期限に設定できる Decrement 関数を使用しています。 それ以外の場合は、正確なタイムアウト時間をミリ秒単位で指定できます。 全ユーザーが同時ログインを試みることのないよう、ログインを試みる前に適当な一時停止処理を追加しましょう。

Class SemaphoreSample.LogIn Extends %RegisteredObject [ ProcedureBlock ]
{

/// システムへのユーザーログインをシミュレート
ClassMethod Run() As %Status
{

    //データベースへのアクセスを担うセマフォをオープン
    Set cell = ##class(SemaphoreSample.Counter).%New()
    Do cell.Open(##class(SemaphoreSample.Counter).Name())

    // システムへのログインを開始
    // この例では 25 名の異なる生徒を取り上げます
    For deccnt = 1 : 1 : 25 {        
        // ランダムログインを生成
       Set Name = ##class(%Library.PopulateUtils).LastName()

        try
        {
           Set result =  cell.Decrement(1, -1)  
        } catch 
        {
           Set msg = "Access blocked"
           Do ..Logger(##class(SemaphoreSample.Counter).Name(), msg)
           Return   
        }
        do ##class(SemaphoreSample.Util).AddUser(Name)      
        Set msg = Name _ " entered the system "
        Do ..Logger(Name, msg)

        Set waitsec = $RANDOM(10) + 7
        Hang waitsec
    }
    Set msg = "There are no more users waiting to log in to the system"
    Do ..Logger(##class(SemaphoreSample.Counter).Name(), msg)
    Quit $$$OK
}

/// ログ保存用のユーティリティを呼び出す
ClassMethod Logger(id As %String, msg As %String) [ Private ]
{
    Do ##class(SemaphoreSample.Util).Logger($Horolog, id, msg)
    Quit
}

}

このモデルでは、サーバーから切断された際に接続中のユーザーが他にもいるかどうかもチェックする必要があります。 このため、最初にユーザーを含むグローバル(^LoggedUsers)の内容を確認し、空だった場合はしばらく待機してからログインできた人がいるかどうかをチェックしています。

Class SemaphoreSample.LogOut Extends %RegisteredObject [ ProcedureBlock ]
{

/// ユーザーのログアウトをシミュレート
ClassMethod Run() As %Status
{
    Set cell = ##class(SemaphoreSample.Counter).%New()
    Do cell.Open(##class(SemaphoreSample.Counter).Name())
    
    // システムからのログアウト
    For addcnt = 1 : 1 : 25 {
        Set inx = ##class(SemaphoreSample.Util).ChooseUser(.Name)
        while inx = -1
        {
            Set waitsec = $RANDOM(10) + 1
            Hang waitsec
            Set inx = ##class(SemaphoreSample.Util).ChooseUser(.Name)
        }
        try 
        {
            Do cell.Increment(1)
        } catch 
        {
            Set msg = "Access blocked"
            Do ..Logger(##class(SemaphoreSample.Counter).Name(), msg)
            Return   
        }
        
        
        Set waitsec = $RANDOM(15) + 2
        Hang waitsec
    }
    Set msg = "All users have logged out of the system"
    Do ..Logger(##class(SemaphoreSample.Counter).Name(), msg)
    Quit $$$OK
}

/// ログ保存用のユーティリティを呼び出す
ClassMethod Logger(id As %String, msg As %String) [ Private ]
{
    Do ##class(SemaphoreSample.Util).Logger($Horolog, id, msg)
    Quit
}

}

プロジェクトの準備が整いました。 このプロジェクトをコンパイルし、起動して結果を確認してみましょう。 それぞれの作業を 3 つのターミナルウィンドウで別々に行います。

最初のウィンドウでは、必要に応じて目的のネームスペースに切り替えてください(私の場合は USER ネームスペースでプロジェクトを作成しました)。

zn "USER"

その後、Main クラスから作成した「サーバー」を起動するメソッドを呼び出します。

do ##class(SemaphoreSample.Main).Run()

2 番目のウィンドウでは、システムにログインするユーザーを生成する Login クラスから Run メソッドを呼び出します。

do ##class(SemaphoreSample.LogIn).Run()

最後のウィンドウでは、システムからログアウトするユーザーを生成する LogOut クラスから Run メソッドを呼び出します。

do ##class(SemaphoreSample.LogOut).Run()

全員がログインおよびログアウトすると、3 つのウィンドウすべてにログが表示されます。 状況を把握しやすくするため、認証された最初の 10 人のユーザーがシステムにログインするのを待ち、セマフォの値が 0 未満にならないことを実証してから、LogOut のルーチンを開始することをお勧めします。

この例で使用されている Increment メソッドと Decrement メソッドとは別に、待機リストと対応する次のメソッドを使用してセマフォを操作することができます。

  • AddToWaitMany — セマフォ関連の操作をリストに追加する
  • RmFromWaitMany — リストから操作を削除する
  • WaitMany — セマフォを使用したすべての操作が終了するまで待機する

この場合、WaitMany はリスト上のすべてのタスクをループし、操作が正常に完了した時点で WaitCompleted メソッド(開発者が自分で実装する必要があります)を呼び出します。 このメソッドはセマフォが操作にゼロ以外の値を割り当てた場合、またはタイムアウトによって待機終了になった場合に呼び出されます。 カウンターから減らす数は、このメソッドの引数に返されます(タイムアウトの場合は 0)。 メソッドの動作が終了すると、セマフォが WaitMany タスクリストから削除されて次のタスクが実行されます。

Caché のセマフォに関する詳細については、公式ドキュメント(別のセマフォの例も掲載されています)とクラスの説明を参照してください。

このプロジェクトは GitHub で公開されています。

皆様からのコメントやご提案をお待ちしております。

0
0 2834
記事 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 · 11月 3, 2020 123m read

1.本記事の内容

Caché パターンマッチングと同様に、Caché では正規表現を使ってテキストデータのパターンを特定することができますが、後者の場合はより高い表現力を利用できます。 本記事では正規表現を簡単に紹介し、Caché での活用方法について解説します。 本記事の情報は、主に Jeffrey Friedl 氏著作の「Mastering Regular Expressions (詳説 正規表現)」に加え、もちろん Caché のオンラインドキュメンテーションなど、様々なリソースを基に提供しています。 本記事は正規表現のあらゆる可能性や詳細について解説することを意図したものではありません。 更なる詳細にご興味のある方は、チャプター 5 に記載のソースを参照してください。 オフラインで読む場合は、PDF バージョンをダウンロードしていただけます。

パターンを使ったテキストの処理は複雑な作業な作業になることがあります。 正規表現を使用する場合、一般的には、パターンを探すテキスト、パターンそのもの (正規表現)、マッチ (パターンに一致するテキストの部分) など、異なる種類のエンティティを伴います。 こういったエンティティを簡単に区別できるよう、本ドキュメントでは以下のルールを使用しています。

テキストのサンプルは、モノスペース書体で個別に、追加の引用符を使わずに書かれています。

この ″テキスト文字列″ には探している "何か" が含まれています。

区別しにくい場合に限り、本文中にある正規表現はこの例にもあるように、灰色のバックグラウンドで表示されています: ".*?"

マッチした部分は必要に応じて異なる色で強調表示されます。

この "テキスト文字列" には探している "何か" が含まれています。

大きめのコードサンプルは、次の例のようにボックスに分けています。

set t="この ""テキスト文字列"" には探している ""何か"" が含まれています。"
set r="\"".*?\"""
w $locate(t,r,,,tMatch)

2.歴史 (とトリビア) の紹介

1940 当初、神経生理学者により人間の神経系がモデル化され、 その何年後かに、ある数学者が「正規セット」と呼ぶ代数学を用いてこのモデルを説明しました。 そして、この代数学の表記が「正規表現」と名付けられました。

1965 年になり、正規表現は初めてコンピューターの世界で言及されるようになり、 当時 UNIX オペレーティングシステムの一部であったエディター QED に正規表現が導入されるようになりました。 そのエディターの後のバージョンで、すべてのテキスト行で正規表現のマッチを検索し、その結果を出力するコマンドシーケンス g / regular expression / p (global, regular expression, print) が提供されました。 このコマンドシーケンスが、最終的にスタンドアロンの UNIX コマンドラインプログラム「grep」になりました。

今日、正規表現 (RegEx) は、数多くのプログラミング言語において、様々な形で実装されています (セクション 3.3 参照)。

3.正規表現 101

Caché パターンマッチングと同様に、正規表現を使ってテキストデータのパターンを特定することができますが、後者の場合はより高い表現力を利用できます。 以下のセクションでは、正規表現のコンポーネントとその評価をまとめ、使用可能なエンジンをいくつか紹介します。使用方法は、チャプター 4 で詳しく説明します。

3.1.正規表現のコンポーネント

3.1.1.正規表現のメタ文字

以下の文字は正規表現として使用された場合に特別な意味を持ちます。

.  *  +  ?  (  )  [  ]  \  ^ $  |

これらをリテラルとして使用する場合は、バックスラッシュを使ってエスケープする必要があります。 リテラルシーケンスを明示的に指定する場合は、\Q \E を使用します。

3.1.2.リテラル

通常のテキストとエスケープされた文字はリテラルとして扱われます。以下はその一部です。

<td>
  abc
</td>
<td>
  改ページ
</td>
<td>
  改行
</td>
<td>
  行頭復帰
</td>
<td>
  (垂直) タブ
</td>
<td>
  8 進数。Caché (ICU) で使用される正規表現エンジンは、最大で \0377 (10 進法では 255) までの 8 進数に対応しています。 他のエンジンから正規表現を移行させる場合は、8 進数がどう処理されるかを事前に確認しておいてください。
</td>
<td>
  16 進数。ICU ライブラリに 16 進数を処理する別のオプションが記載されています。ICU ドキュメンテーションを参照してください (リンクはセクション 5.8 にあります)
</td>
abc
\f
\n
\r
\v
\0+ 3 桁の数字 (例: \0101)
\x+ 2 桁の数字 (例: \x41)

3.1.3.アンカー

アンカーは、テキスト / 文字列の位置を一致させる場合に使用します。以下はその例です。

  • \A           文字列の先頭
  • \Z           文字列の末尾
  • ^             テキストまたは行の先頭
  • $             テキストまたは行の末尾
  • \b           単語の境界
  • \B           単語の境界ではない
  • <           単語の先頭
  • >           単語の末尾

一部の RegEx エンジンは、単語を構成するものの正確な定義や、単語の区切り文字と見なされる文字の定義によって、動作が異なります。

3.1.4.量指定子

量指定子を使用すると、先行する要素がマッチとみなされるための出現回数を指定できます。

  • {x}          x 回の出現
  • {x,y}      x 回から y 回の出現
  • *             0 回以上、{0,} と同じ
  • +             1 回以上、{1,} と同じ
  • ?             0 回または 1 回
     

最長一致

量指定子は「最長一致」、つまり、できるだけ多くの文字を一致させようとします。 以下のようなテキスト文字列があり、引用符内のテキストを見つける必要があるとしましょう。

This is "a text" with "four quotes".

セレクタは最長一致なため、正規表現 ".*" を使うと、一致するテキストの数が多くなり過ぎてしまいます。

This is "a text" with "four quotes".

この例の正規表現 .* は、引用符のペアの間にある文字をできるだけ多く検出しようとします。 しかし、ドットセレクタ ( . ) により引用符も検出されてしまうため、期待している結果が得られません。

一部の正規表現エンジン (Caché で使用されるものを含む) では、クエスチョンマーク (?) を追加することにより、量指定子の最長一致となることを抑制できます。 そうすることで、正規表現 ".*?" は、引用符で囲まれたテキストの 2 つの部分に一致し、期待通りの結果が得られます。

This is "a text" with "four quotes".

3.1.5.文字クラス (文字の範囲)

文字の範囲や文字のセットは、角括弧を使って [a-zA-Z0-9] または [abcd] のように指定します。正規表現では、これを「文字クラス」と呼んでいます。 範囲の中で一致するのは 1 文字だけです。つまり、範囲定義内にある文字の順番は関係ありません。[dbac][abcd] では同じ文字が一致します。

特定の文字の範囲を省く場合は、範囲定義の前 (角括弧内) に ^ を挿入します: [^ abc] と指定すると、a、b、c 以外のすべてが一致します。

一部の正規表現エンジンでは、事前定義された文字クラス (POSIX) を使用できます。以下はその一部です。

  • [:alnum:]    [a-zA-z0-9]

  • [:alpha:]    [a-zA-Z]

  • [:blank:]    [ \t]

3.1.6.グループ

括弧を使用すると、正規表現の一部をグループ化することができます。 これは、セレクタのグループに量指定子を適用したり、同じ正規表現 (後方参照) および正規表現を呼び出す Caché オブジェクトスクリプトコード (キャプチャバッファ) の両方からグループを参照する場合に便利です。 グループはネストできます。

下の正規表現は、順に 3 桁の数字、ダッシュ、大文字と数字の 3 つのペア、ダッシュ、先頭と同じ 3 桁の数字で構成される文字列に一致します。

([0-9]{3})-([A-Z][[0-9]){3}-\1

この例は、後方参照 (以下を参照) を使用して、構造だけでなく中身にも一致させる方法を示しています。後方参照 (紫) は、先頭の 3 桁の数字を末尾でも検索するようエンジンに指示しています (黄) 。 また、より複雑な構造 (緑色) に量指定子を適用する方法も示しています。

上記の正規表現は、以下の文字列に一致します。

123-D1E2F3-123

以下には一致しません。

123-D1E2F3-456             (末尾の 3 桁の数字が先頭の 3 桁と異なる)

123-1DE2F3-123             (中央部分が3 つの大文字、数字のペアではない)

123-D1E2-123                   (中央部分が2つの大文字、数字のペアしかない)

グループは、いわゆるキャプチャバッファの作成にも使用されます (セクション4.5.1を参照)。 これはとても強力な機能で、情報の一致と抽出を同時に実行できます!

3.1.7. 論理和指定子

論理和指定子を指定するには、skyfall|done のように縦線の文字を使います。 そうすることで、セクション 3.1.5 で説明した文字クラスを使う場合のように、より複雑な式を一致させることができます。

3.1.8.後方参照

後方参照を使用すると、以前に定義されたグループ (括弧内のセレクター) を参照できます。 下の正規表現の例は、同じ文字が 3 回繰り返す場合に一致します。

([a-zA-Z])\1\1

後方参照は「\x」で指定され、「x」は何番目の括弧で囲まれた式を参照するのかを意味します。

3.1.9.優先順位

  1. () よりも [] を優先
  2. シーケンスよりも , + および ? を優先。ab(ab) ではなく、a(b) と同等である
  3. 論理和指定子よりもシーケンスを優先。ab|c は、a(b|c) でなく、(ab)|c と同等である。

3.2.理論

通常、正規表現の評価は、下に紹介する 2 つの手段のどちらかで実施されます (ここでは簡単に説明していますので、詳しい内容はチャプター 5 に記載の文献を参照してください)。

  1. テキストを基にした判定 (DFA – Deterministic Finite Automaton「決定性有限オートマトン」)
    エンジンは入力されたテキスト文字を 1 文字ずつ確認し、それまで確認した文字を一致させようとする。 入力されたテキストの末尾に到達すると、成功とします。
     
  2. Regexを基にした判定(NFA – Non-deterministic Finite Automaton「非決定性有限オートマトン」)
    エンジンは、正規表現のトークンを 1 つずつ確認し、それをテキストに適用しようとします。 最後のトークンに到達(かつ一致)すれば、成功とします。
     

手段 1 は決定的な方法です。実行時間は入力されるテキストの長さによります。 正規表現に使われるセレクタの順序が実行時間に影響を与えることはありません。

手段 2 は非決定的な方法です。エンジンは、一致するかエラーが発生するまで、正規表現に使われているセレクタのすべての組み合わせを確認します。 従ってこの方法は_一致しない_場合は特に時間がかかります (すべての可能な組み合わせを確認する必要があるため)。 セレクタの順番は、実行時間に影響を_与えます_。 ただし、この方法はバックトラックおよびキャプチャバッファを使用できます。

3.3.エンジン

正規表現エンジンは、プログラミング言語やオペレーティングシステムに組み込まれているものから、ほぼどこでも使用可能なライブラリまで、様々なものが存在します。 以下は評価手段別に分けた正規表現エンジンの例です。

  • DFA:      grep、awk、lex
  • NFA:      Perl、Tcl、Python、Emacs、sed、vi、ICU

下のテーブルは、様々なプログラミング言語やライブラリで使用可能な正規表現の機能を比較したものです。

詳細はこちらをお読みください: https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines

4.RegEx と Caché

InterSystems Caché では、正規表現に ICU ライブラリが使用されています。その機能の多くは、Caché オンラインドキュメンテーションでご確認いただけます。 (後方参照などを含む) 完全な詳細は、ICU ライブラリのオンラインドキュメンテーションを参照してください。 – ICU へのリンクはセクション 5.8 をご覧ください。 以下のセクションで、その使用方法を簡単に紹介します。

4.4.$match() と $locate()

Caché ObjectScript (COS) では、2 つの関数 $match()$locate() により、ICU ライブラリが提供する多くの Regex 機能を直接使用できます。$match(String, Regex) は、指定された Regex パターンを基に入力文字列を検索します。 マッチが見つかった場合は 1 を、それ以外の場合は 0 を返します。

例:

  • w $match("baaacd","<span style="background-color:#D3D3D3;">.*(a)\1\1.*</span>") は 1 を返します
  • w $match("bbaacd","<span style="background-color:#D3D3D3;">.*(a)\1\1.*</span>") は 0 を返します

$locate(String,Regex,Start,End,Value) は、$match() と同様に、指定された Regex パターンを基に入力文字列を検索します。 ですが、$locate() は扱いやすい上に、より多くの情報を返します。 Start により、入力文字列内でパターンの検索を開始する位置を $locate に指示します。 $locate() は、マッチを見つけると、その最初の文字の位置を返し、End をマッチの次の文字の位置に設定します。 マッチの内容は Value として返されます。

$locate() は、マッチが見つからないと 0 を返し、EndValue が指定されていも、それを変更することはありません。EndValue は参照渡しで渡されるので、繰り返し使用する場合は注意が必要です (ループ内で使用する場合など)。

例:

  • w $locate("abcdexyz","<span style="background-color:#D3D3D3;">.d.</span>",1,e,x) は 3 を返すと同時に、e は 6 に、x は "cde" に設定されます

$locate() は、パターンマッチングを実行すると同時に、最初のマッチの内容を返すことができます。  すべてのマッチの内容を抽出する必要がある場合は、ループの中で $locate() を繰り返し呼び出すか、%Regex.Matcher のメソッドを実行します (次のセクションで解説)。

4.5.%Regex.Matcher

%Regex.Matcher を使用すると、$match() および $locate() と同様に、ICU ライブラリの正規表現機能を使用できます。 しかし、%Regex.Matcher を使用すると、高度な機能も利用でき、複雑なタスクの扱いがとてもシンプルになります。 次のセクションでは、キャプチャバッファについてもう一度確認し、正規表現を使って文字列を置き換える方法やランタイムの動作を制御する方法について説明します。

4.5.1.キャプチャバッファ

グループや後方参照、$locate() のセクションで説明してきましたが、正規表現を使用すると、テキスト内のパターンを検索すると同時に、一致した内容を返すことができます。 これは、抽出したいパターンの部分を括弧 (グルーピング) に入れて行います。 マッチが成功すると、一致したすべてのグループの内容がキャプチャバッファに入ります。 これはマッチした内容を Value パラメーターで返す $locate() とは少し異なるので注意が必要です。$locate() はマッチ全体を返す一方で、キャプチャバッファは、マッチ (グループ) への_部分的_なアクセスを可能にします。

これを使用するには、%Regex.Matcher クラスのオブジェクトを作成し、それに正規表現と入力文字列を渡します。 それから、%Regex.Matcher が提供するメソッドを 1 つ呼び出せば、実際の作業を実行することができます。 

例 1 (シンプルなグループ):

set m=##class(%Regex.Matcher).%New("(a|b).*(de)", "abcdeabcde")
w m.Locate() 1 を返す
w m.Group(1) a を返す
w m.Group(2) de を返す

例 2 (ネストされたグループと後方参照):

set m=##class(%Regex.Matcher).%New("((a|b).*?(de))(\1)", "abcdeabcde")
w m.Match()                   1 を返す
w m.GroupCount                4 を返す
w m.Group(1)                  abcde を返す
w m.Group(2)                  a を返す
w m.Group(3)                  de を返す
w m.Group(4)                  abcde を返す

(ネストされたグループの順番に注目してください。始め括弧がグループの始まりを意味するため、インデックス番号は外側のグループよりも内側のグループの方が高くなっています)

先ほども触れましたが、キャプチャバッファはパターンを一致させると同時に一致した内容を抽出できる、とても強力な機能です。 正規表現がないと、ステップ 1 として (パターンマッチオペレーターを使用するなどして) マッチを探し、ステップ 2 として何らかの条件を基に、マッチした内容を抽出 (または部分的に抽出) しなくてはならなくなります。

パターンを部分的にグループ化する必要がある (例: その部分に量指定子を適用するため) が、マッチした部分の内容をキャプチャバッファに取り入れたくない場合は、以下の例 3 で示すように、グループの前に疑問符とコロンを順に挿入し、そのグループを "非キャプチャリング (non-capturing)" または "内気 (shy)" なグループとして定義することができます。

例 3 (内気 "shy" なグループ):

set m=##class(%Regex.Matcher).%New("((a|b).*?(?:de))(\1)","abcdeabcde")
w m.Match()                        1 を返す
w m.Group(1)                       abcde を返す
w m.Group(2)                       a を返す
w m.Group(3)                       abcde を返す
w m.Group(4)                       <REGULAR を返すEXPRESSION>zGroupGet+3^%Regex.Matcher.1

4.5.2.置換

%Regex.Matcher は、マッチした内容を即座に置き換えることができる、ReplaceAll()ReplaceFirst() というメソッドも提供します。

set m=##class(%Regex.Matcher).%New(".c.","abcdeabcde")
w m.ReplaceAll("xxxx")        axxxxeaxxxxe を返す
w m.ReplaceFirst("xxxx")     axxxxeabcde を返す

置換文字列でグループを参照することもできます。 前の例のパターンにグループを追加した場合、置換文字列に $1 を含めることでその内容を参照することができます。

set m=##class(%Regex.Matcher).%New("<span style="background-color:#D3D3D3;">.</span><strong><span style="background-color:#D3D3D3;">(</span></strong><span style="background-color:#D3D3D3;">c</span><strong><span style="background-color:#D3D3D3;">)</span></strong><span style="background-color:#D3D3D3;">.</span>","abcdeabcde")
w m.ReplaceFirst("xx$1xx")   axxcxxeabcde を返す

マッチした完全な内容を置換文字列に含めるには、$0 を使用します。

w m.ReplaceFirst("xx$0xx")    axxbcdxxeabcde を返す

4.5.3.OperationLimit

セクション 3.2 で、正規表現を評価する 2 つの方法 (DFA と NFA) について解説しました。 Caché で使用される正規表現エンジンは、非決定的有限オートマトン (NFA) です。 したがい、特定の入力文字列において様々な正規表現を評価するのにかかる時間は異なる場合があります。 [1]

%Regex.Matcher オブジェクトのプロパティ OperationLimit を使えば、(_クラスタ_と呼ばれる) 実行単位の数を制限することができます。 クラスターの実行にかかる正確な時間は、環境によって異なります。 通常、クラスターの実行はわずか数ミリ秒で完了します。 しかし、OperationLimitは 0 (制限なし) にデフォルト設定されています。

4.6.実例: Perl から Caché への移行

このセクションでは、Perl から Caché への移行において、正規表現が関連する部分について説明します。 Perl スクリプトは、文字のマッチと抽出の両方に使用される数十個の多少複雑な正規表現で構成されていました。

もし、Caché で正規表現の機能を使用できなかったとしたら、Caché への移行プロジェクトは大掛かりな作業となったことでしょう。 幸い、Caché では正規表現の機能を使用できる上に、Perl スクリプトの正規表現は、ほぼ何の変更も加えずに Caché で使用することができました。

以下のリンクから Perl スクリプトを一部ご覧いただけます。

正規表現を Perl から Caché に移行する上で、唯一必要となった変更は (正規表現に大文字と小文字を区別させる) 修飾子 /iに関するもので、正規表現の末尾から先頭に移動させる必要がありました。

Perl では、キャプチャバッファの中身は特別な変数にコピーされます (上の Perl コードでいう $1$2)。 Perl プロジェクトのほぼ全ての正規表現で、このメカニズムが使用されていました。 これに似た作業を行えるよう、Caché Object Script ではシンプルなラッパーメソッドが作成されました。 %Regex.Matcher を使ってテキスト文字列に対し正規表現を評価し、キャプチャバッファの中身をリストとして返すというものです ($lb())。

以下がその Caché Object Script コードです。

if ..RegexMatch(
       tVCSFullName,
       "(?i)[\\\/]([^\\^\/]+)[\\\/]ProjectDB[\\\/](.+)[\\\/]archives[\\\/]",
       .tCaptureBufferList)
       {
             set tDomainPrefix=$zcvt($lg(tCaptureBufferList,1), "U")
             set tDomain=$zcvt($lg(tCaptureBufferList,2), "U")
       }
…

Classmethod RegexMatch(pString as %String, pRegex as %String, Output pCaptureBuffer="") {

       #Dim tRetVal as %Boolean=0
       set m=##class(%Regex.Matcher).%New(pRegex,pString)
       while m.Locate() {
             set tRetVal=1
             for i=1:1:m.GroupCount {
                    set pCaptureBuffer=pCaptureBuffer_$lb(m.Group(i))
             }
       }
       quit tRetVal
}

5.リファレンス情報

5.7.一般情報

一般情報およびチュートリアル

チュートリアルおよび例

正規表現エンジンの比較

クイックガイド

書籍

5.8.Caché オンラインドキュメンテーション

5.9.ICU

上述のとおり、InterSystems Caché は ICU エンジンを使用します。 包括的なドキュメンテーションはオンラインでご利用いただけます。

5.10.ツール

正規表現を作成するにあたり、開発者をサポートするツールは、無料で使用できるものから、商用ライセンスが付属するものまで数多く存在します。 私の個人的なお気に入りは RegexBuddy (http://www.regexbuddy.com/) です。インタラクティブで視覚的な機能を使用できるので、正規表現の作成とテストを様々な方法で行えます。

0
1 814