#オブジェクトデータモデル

0 フォロワー · 35 投稿

オブジェクトデータモデルとは、データまたはコードが、データとデータを処理する手順を組み合わせたモジュールで構成されていることです。  詳細はこちら。

記事 Mihoko Iijima · 10月 10, 2023 15m read

開発者の皆さん、こんにちは。

この記事では、複雑なJSON形式の文書を「JSONテンプレートエンジン」を利用して生成させる方法をご紹介します。

「JSONテンプレートエンジン」については、6月のウェビナーで使用例をご紹介しましたが、JSON生成対象として医療情報交換標準規格であるFHIRリソースのJSON(例:Patientリソース)を例に解説しています。

このエンジンは、JSON形式の文書であればどのような種類のデータでもご利用いただけますので、一般的なJSON形式の文書を利用して使い方をご紹介します。

例に使用するJSONはこちら👉 https://api.openbd.jp/v1/get?isbn=978-4-7808-0204-7&pretty

このサンプルから以下の部分を抜き出して、ObjectScriptでJSON形式の文書を組み立てていく方法をご紹介します。

7
0 1403
記事 Mihoko Iijima · 3月 13, 2023 3m read

これは InterSystems FAQ サイトの記事です。

永続クラス定義では、データを格納するグローバル変数名を初回クラスコンパイル時に決定しています。
グローバル変数名は、コンパイル後に表示されるストレージ定義(Storage)で確認できます。

例)

4
0 398
記事 Mihoko Iijima · 3月 4, 2024 9m read

これは InterSystems FAQ サイトの記事です。

永続クラス定義(またはテーブル定義)に対してオブジェクト操作でデータの参照・更新を行うとき、オブジェクトオープンで使用する%OpenId()、オブジェクトの削除に使用する%DeleteId()の第2引数を使用して並行処理の制御方法を選択できます。

ご参考:オブジェクト同時処理のオプション

既定値は1です。(永続クラスのDEFAULTCONCURRENCYクラスパラメータでデフォルト値を指定できます。特に変更していない場合は 1を使用します)

並行処理の基本事項は以下の通りです。

  • アトミックな書き込みを保証したい場合は並行処理の値は0より大きい値を指定する必要があります。
  • 並行処理の値が0より大きい場合、オブジェクトの保存や削除処理中は、ロックの取得と解放を実施します。

並行処理の値別の動作の違いは以下の通りです。(ドキュメントの「並行処理の値」に表がありますので併せてご覧ください)

0
0 171
記事 Toshihiko Minamoto · 7月 28, 2023 1m read

ターミナルにライセンス期限切れの警告メッセージ(「*\* Warning: This Cache license will expire in 3 days **」)が表示されており、そのメッセージを表示したくない場合は、以下のコマンドを実行すると、メッセージの表示を無効(または有効)にできます。

Do ExpirationMessageOff^%SYS.LICENSE - Disable

Do ExpirationMessageOn^%SYS.LICENSE - Enable

 

0
0 159
お知らせ Mihoko Iijima · 11月 10, 2022

開発者の皆さん、こんにちは!

開発者コミュニティのYouTubeプレイリストにEmbedded Pythonの新しいセルフラーニングビデオを公開しましたのでお知らせします📣!

◆ Embedded Pythonでデータベースプログラミング:オブジェクトアクセス編

※YouTubeに移動していただくとプレイリストの中から好きなビデオを選択してご覧いただけます。

0
0 142
記事 Mihoko Iijima · 6月 28, 2022 5m read

開発者の皆さん、こんにちは!

この記事では、Embedded Pythonをご自身の好きなタイミングで学習できる📚セルフラーニングビデオ📚の YouTube プレイリストをご紹介します!

👆こんな具合に👆学習内容別 Embedded Python セルフラーニングビデオを公開しています!

この記事では、これから Embedded Python でプログラミングを開始してみたい方向けに最適なビデオをご紹介します!
​​​​​​

◆ Embedded Python概要から学習を始めたい方はこちら👇

以下の内容を確認できるプレイリスト:1-Embedded Python概要編 - YouTube をご用意しています。

  • Embedded Pythonとは?
  • Python開発者から見た使い道(解説&実演)
  • IRIS開発者からみた使い道(解説&実演)

この後、実際の操作を試されたい場合は、次のプレイリスト:2-Embedded Python利用前の準備 - YouTube が最適です。

◆ Embedded Python利用前の準備 を知りたい方はこちら👇

操作を開始する前に、必要な利用前の準備についてご紹介しているプレイリスト:2-Embedded Python利用前の準備 - YouTube をご用意しています。

0
0 803
記事 Toshihiko Minamoto · 4月 26, 2022 8m read

@Ming Zhou から素晴らしい質問をいただきました。その回答は、まさに私がObjectScriptを愛用している理由を表しています。

初めて誰かに ObjectScript や IRIS を説明する際、必ず、クラスを記述してコンパイルし、テーブルを取得して、オブジェクトまたはリレーショナルのいずれか最も自然な観点からデータを操作できると説明しています。 いずれにせよ、これは単に、グローバルと呼ばれる非常に高速な内部データ構造を囲む薄めのラッパーであり、速度をさらにバーストさせる必要がある場合にも使用できます。

オタクレベルの人と話すときには、ObjectScipt はあらゆる類の凝ったメタプログラミングが可能だと説明します。たった今書いたクラスが、オブジェクトやリレーショナルというアクセス方法から、さらなる高速なアクセスが必要な時に内部データ構造を使う方法まで操作ができるためです。

「継承されたプロパティも含め、クラス内のすべてのプロパティを取得するにはどうすればよいですか?」という質問に対する回答がわかるでしょう。

同じ回答を得られる 3 つの異なる方法を以下に示します。

Class DC.Demo.PropertyQuery Extends %Persistent
{

Property Foo As %String;

Property Bar As %Boolean;

/// 特定の目的を実現するためのすべての方法を紹介します
ClassMethod Run()
{
    for method = "FromRelationship","WithQuery","AsQuicklyAsPossible" {
        write !,method,":"
        kill properties
        do $classmethod($classname(),"GetProperties"_method,.properties)
        do ..Print(.properties)
        write !
    }
}

ClassMethod Benchmark()
{
    for method = "FromRelationship","WithQuery","AsQuicklyAsPossible" {
        write !,method,":",!
        set start = $zhorolog
        set startGlobalRefs = $system.Process.GlobalReferences($job)
        set startLines = $system.Process.LinesExecuted($job)
        for i=1:1:1000 {
            kill properties
            do $classmethod($classname(),"GetProperties"_method,.properties)
        }
        set endLines = $system.Process.LinesExecuted($job)
        set endGlobalRefs = $system.Process.GlobalReferences($job)
        write "Elapsed time (1000x): ",($zhorolog-start)," seconds; ",(endGlobalRefs-startGlobalRefs)," global references; ",(endLines-startLines)," routine lines",!
    }
}

/// %Dictionary.CompiledClass 内のプロパティ関係を使用してプロパティを取得します
ClassMethod GetPropertiesFromRelationship(Output properties)
{
    // 些細な問題: %OpenId と Properties.GetNext() は、絶対的に必要なデータ以上のデータを読み込むため、低速です。
    // グローバル参照が多いほど、処理に時間がかかります。
    set class = ##class(%Dictionary.CompiledClass).IDKEYOpen($classname(),,.sc)
    $$$ThrowOnError(sc)
    set key = ""
    for {
        set property = class.Properties.GetNext(.key)
        quit:key=""
        set properties(property.Name) = $listbuild(property.Type,property.Origin)
        // 余分なメモリの消費を回避します
        do class.Properties.%UnSwizzleAt(key)
    }
}

/// %Dictionary.CompiledProperty に対してクエリし、プロパティを取得します
ClassMethod GetPropertiesWithQuery(Output properties)
{
    // SQL でプロパティを取得することで、不要な参照のオーバーヘッドを回避します
    set result = ##class(%SQL.Statement).%ExecDirect(,
        "select Name,Type,Origin from %Dictionary.CompiledProperty where parent = ?",
        $classname())
    if result.%SQLCODE < 0 {
        throw ##class(%Exception.SQL).CreateFromSQLCODE(result.%SQLCODE,result.%Message)
    }
    while result.%Next(.sc) {
        $$$ThrowOnError(sc)
        set properties(result.Name) = $listbuild(result.Type,result.Origin)
    }
    $$$ThrowOnError(sc)
}

/// ダイレクトグローバル参照をラッピングするマクロを使用してプロパティを取得します
ClassMethod GetPropertiesAsQuicklyAsPossible(Output properties)
{
    // マクロでラップされたダイレクトグローバル参照を介してプロパティを取得する方法は読み取りにくいですが、
    // 一番速い方法です。
    set key = ""
    set class = $classname()
    for {
        set key = $$$comMemberNext(class,$$$cCLASSproperty,key)
        quit:key=""
        set type = $$$comMemberKeyGet(class,$$$cCLASSproperty,key,$$$cPROPtype)
        set origin = $$$comMemberKeyGet(class,$$$cCLASSproperty,key,$$$cPROPorigin)
        set properties(key) = $listbuild(type,origin)
    }
}

ClassMethod Print(ByRef properties)
{
    set key = ""
    for {
        set key = $order(properties(key),1,data)
        quit:key=""
        set $listbuild(type,origin) = data
        write !,"property: ",key,"; type: ",type,"; origin: ",origin
    }
}

Storage Default
{
<Data name="PropertyQueryDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Foo</Value>
</Value>
<Value name="3">
<Value>Bar</Value>
</Value>
</Data>
<DataLocation>^DC.Demo.PropertyQueryD</DataLocation>
<DefaultData>PropertyQueryDefaultData</DefaultData>
<IdLocation>^DC.Demo.PropertyQueryD</IdLocation>
<IndexLocation>^DC.Demo.PropertyQueryI</IndexLocation>
<StreamLocation>^DC.Demo.PropertyQueryS</StreamLocation>
<Type>%Storage.Persistent</Type>
}

}

そしてもちろん、これらいずれかのアプローチを使用しても、答えは同じです。

d ##class(DC.Demo.PropertyQuery).Run()

FromRelationship:property: %%OID; type: %Library.RawString; origin: %Library.RegisteredObjectproperty: %Concurrency; type: %Library.RawString; origin: %Library.Persistentproperty: Bar; type: %Library.Boolean; origin: DC.Demo.PropertyQueryproperty: Foo; type: %Library.String; origin: DC.Demo.PropertyQuery

WithQuery:property: %%OID; type: %Library.RawString; origin: %Library.RegisteredObjectproperty: %Concurrency; type: %Library.RawString; origin: %Library.Persistentproperty: Bar; type: %Library.Boolean; origin: DC.Demo.PropertyQueryproperty: Foo; type: %Library.String; origin: DC.Demo.PropertyQuery

AsQuicklyAsPossible:property: %%OID; type: %Library.RawString; origin: %Library.RegisteredObjectproperty: %Concurrency; type: %Library.RawString; origin: %Library.Persistentproperty: Bar; type: %Library.Boolean; origin: DC.Demo.PropertyQueryproperty: Foo; type: %Library.String; origin: DC.Demo.PropertyQuery

パフォーマンスを比較します。


d ##class(DC.Demo.PropertyQuery).Benchmark()

FromRelationship:Elapsed time (1000x): .78834 seconds; 1056000 global references; 2472003 routine lines

WithQuery:Elapsed time (1000x): .095235 seconds; 28001 global references; 537007 routine lines

AsQuicklyAsPossible:Elapsed time (1000x): .016422 seconds; 25000 global references; 33003 routine lines

これをちょっと分析する限り、まったく期待どおりの結果です。 クラスとプロパティのオブジェクトアクセスは、クラス定義とコンパイルされたクラスのメタデータが多数のグローバルに保存されているため、はるかにコストがかかります。$listbuild リストにすべてのデータが格納されているのではなく(グローバル参照を減らすため)、各グローバルノードに単一の値でツリーに格納されています。 オブジェクトを開くというのは、これらすべてを読み取ることになるため、もちろん「FromRelationship」手法がはるかに最も低速になります。 もちろん、これは IRIS における一般的なオブジェクトアクセスのパフォーマンスを表すものではありません。このケースは、オブジェクトを使用する際の特に悪いケースとしてたまたま発生しただけです。

このクエリと生のグローバルベースのアプローチは、ルーチン行ではなくグローバル参照という意味で似ています。 Dynamic SQL を使用した上記の単純なアプローチには、イテレーションごとに行うクエリ準備のオーバーヘッドがあります。 このオーバーヘッドの一部を回避するには、準備しておいた %SQL.Statement を再利用するか、カーソル付きの埋め込み SQLを使用するか(いくつかの理由で、私は気に入っていません)、次のように厄介な方法を取ることができます。

/// %Dictionary.CompiledProperty に対するクエリを使用して、プロパティを取得します
ClassMethod GetPropertiesWithEmbeddedQuery(Output properties)
{
    set classname = $classname()
    
    // 簡易: 1 行を返すクエリを実行した後に、カーソルの記述を省略して、データの抽出のみを行います。
    // 次のアプローチのパフォーマンスはカーソルを上回ります(演習として残しておきます)。いずれにしても私はカーソルの操作が苦手です。
    &SQL(SELECT %DLIST($ListBuild(Name,Type,Origin)) INTO :allProperties FROM %Dictionary.CompiledProperty WHERE parent = :classname)
    if (SQLCODE < 0) {
        throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg)
    }
    if (SQLCODE = 100) {
        quit
    }
    set pointer = 0
    while $listnext(allProperties,pointer,propertyInfo) {
        set properties($list(propertyInfo)) = $list(propertyInfo,2,3)
    }
}

これをベンチマークに追加すると、次のようになります。

WithEmbeddedQuery:
Elapsed time (1000x): .024862 seconds; 25000 global references; 95003 routine lines

AsQuicklyAsPossible:
Elapsed time (1000x): .016422 seconds; 25000 global references; 33003 routine lines

非常に近い結果です!

0
0 453
お知らせ Mihoko Iijima · 4月 3, 2022

開発者のみなさん、こんにちは!

2021年10月に4回シリーズで開催した「InterSystems IRIS 開発者向けウェビナー」第2回目の「Python Gateway のご紹介」のアーカイブをYouTubeに公開いたしました。

InterSystems IRIS / IRIS for Health バージョン2021.1より、External Language Gateway に Python のサポートが追加されました。また、External Language Gateway の機能強化により外部ストアドプロシージャが利用できるようになり、Java、.Net、Python のコードを SQL から直接実行できるようになりました。

ウェビナーでは、Python からの利用法のついて、デモを交えながらご紹介しています。

ぜひご覧ください!

(IRIS 2021.1新機能全体のご紹介については、こちらのYouTubeをご参照ください。)

【目次】

0:00 ご紹介内容の説明

2:26 PythonからIRISにアクセスする方法

5:07 キーバリュー形式でIRISにアクセスできる「Native API」とは

12:04 Python Gatewayのご紹介

16:21 Python Gateway 使用例

22:12 デモ

30:15 まとめ

0
0 118
記事 Toshihiko Minamoto · 3月 10, 2022 4m read

背景

先日、ObjectScript の永続(Persistent)クラスのプロパティを編集していたところ、ストレージ定義が最後の変更を反映するように更新されていないことに気づきました。

この場合、クラス定義に不要となったプロパティを削除した上で保存し、再コンパイルしましたが、それでもストレージ定義に残ったままになっていました。

それでも焦ることはありませんでした。 ストレージ定義がコンパイル時に自動生成されるのであれば、それを_削除_して、クラスを再コンパイルすればよいからです。 もちろん、この後、削除されたプロパティはストレージ定義に表示されなくなりました。

問題解決... ですよね?

(ブブー、間違いです)

後になって、このアプローチはクラスにデータが保存されていないときに機能することがわかりました。 ただし、既存のデータがあるのであれば、重大なデータ参照の問題が発生する可能性があります。

そもそもストレージ定義とは?

ストレージ定義はクラスプロパティとデータベース内のその物理ストレージ場所をリンクするマップとして機能します。

複数のスロットのある本棚とどのスロットに本が収まっているかを追跡するためのリストがあるとします。 新しい本は、常にリスト上で空いている一番左のスロットに配置され、スロットは昇順で番号が付けられています。

<td>
  ストレージ定義
</td>

<td>
  ステータス
</td>

<td>
  本棚での喩え
</td>
<td>
  クラスにはまだデータが保存されていない状態で、ストレージ定義を変更します。
</td>

<td>
  OK                                                                                             
</td>

<td>
  棚に本がない状態で、後に本が収納されるべきスロットのマップを変更します。 データなし = データ参照の問題はありません。
</td>
<td>
  クラスにデータが保存されている状態で、データを保存せずに新しいプロパティを追加し、その後でプロパティを削除して別のプロパティを追加します。 (つまり、データが保存される前に、最後に追加されたプロパティの名前を変更します)
</td>

<td>
  OK
</td>

<td>
  スロット 3 は、次に空いている棚のスロットです。 本 C がスロット 3 に配置されることを書き留めても、実際にはまだスロット 3 には配置しません。その状態で、スロット 3 は本 D 用に予約されているとしてマップを更新します。
</td>
<td>
  クラスにデータが保存されている状態で、データが保存されたプロパティをストレージ定義から削除します。
</td>

<td>
  エラー
</td>

<td>
  本棚のスロット 1~3 には本が収まっています。 マップには(スロット 1: 本 A、スロット 2: 本 B、スロット 3: 本 C)と示されています。 本 B をマップからのみ削除します。 本 B はまだ棚にありますが、マップではスロット 2 が空になっている状態です。 次に新しい本を追加する際に、マップにはスロット 2 を利用できることが示されているため、無意識にスロットをダブルブッキングすることになります。
</td>
シナリオ
1
2
3

 

シナリオ 3 の続き

この問題は、マップから本 B を削除するときに、物理的にスロットからも取り除くことで回避したいところですが、 _それをやってはいけません。_誤ってデータテーブルを破損してしまう可能性があります。 驚いたことに、データを手動で変更する方が、棚から本を取り出すことよりもはるかに危険です。

<td>
  ストレージ定義
</td>

<td>
  本棚での喩え
</td>
<td>
  何もしません。 データとストレージ定義はそのままにしておきます。
</td>

<td>
  本 B を二度と読むことがないのであれば、それを探すこともないため、マップに存在したままでも問題はありません。
</td>
<td>
  ストレージ定義に使用しなくなったプロパティが残っていることが気になるのであれば、データはそのままにして、使用されなくなったことを示すように、ストレージ定義内のプロパティ名を更新します(例: propertyA => propertyADeprecated)
</td>

<td>
  本 B は棚に置いたままにし、マップ内のスロット 2 のタイトルを「本 B」から「本 B(関連性なし)」に変更します。
</td>
オプション
1
2

 

お読みいただきありがとうございました! 

0
0 126
記事 Mihoko Iijima · 2月 28, 2022 7m read

開発者のみなさん、こんにちは。

今回は、スーパーやコンビニでもらうレシートを写真で撮り、OCR を使ってレシートの画像から文字列を切り出して IRIS に登録する流れを試してみました。

サンプルでは、Google の Vision API を利用してレシートの JPG 画像から購入物品をテキストで抽出しています。

サンプルコード一式 👉 https://github.com/Intersystems-jp/iris-embeddedpython-OCR
 

最初、オンラインラーニングで使っている 「tesseract-OCR」を使ってみようと思ったのですが、レシートには半角カナが混在していたりで、半カナがなかなかうまく切り出せず、あきらめました・・(半角カナがなかったら日本語もばっちり読めていたのですが・・)

もし、tesseract-OCR で半カナを切り出す良い方法をご存知の方いらっしゃいましたら、ぜひ教えてください!

今回試すにあたり、Vision API の使い方を詳しく書いているページがありましたのでコードなど参考させていただきました。ありがとうございました。

​​【Google Colab】Vision APIで『レシートOCR』

0
1 1425
記事 Mihoko Iijima · 2月 4, 2022 7m read

開発者の皆さん、Python好きの皆さん、こんにちは!

ドキュメントをみながら IRIS 2021.2 に追加された Embedded Python を試してみました!

IRIS にログインしてるのに Pythonシェルに切り替えできて Python のコードが書けたり、Python で import iris するだけで SQL を実行できたりグローバルを操作できるので、おぉ!✨という感じです。

ぜひ、みなさんも体感してみてください!

では早速。

まず、IRISにログインします。Windows ならターミナルを開きます。Windows 以外は以下実行します。

IRIS のインストール方法を確認されたい方は、【はじめての InterSystems IRIS】セルフラーニングビデオ:基本その1:InterSystems IRIS Community Edition をインストールしてみよう!をチェックしてみてください!

iris session iris
0
1 838
記事 Toshihiko Minamoto · 10月 12, 2021 11m read

前の記事では、マクロの潜在的なユースケースををレビューしました。そこで、マクロの使用方法についてより包括的な例を見てみることにしましょう。 この記事では、ロギングシステムを設計して構築します。

ロギングシステム

ロギングシステムは、アプリケーションの作業を監視するための便利なツールで、デバッグや監視にかける時間を大幅に節約してくれます。 これから構築するシステムは2つの部分で構成されます。

  • ストレージクラス(レコードをログ記録するためのクラス)
  • 新しいレコードをログに自動的に追加する一連のマクロ

ストレージクラス

保存する必要のあるもののテーブルを作成し、コンパイル中やランタイム時に、このデータを取得できるタイミングを指定しましょう。 これは、システムの2つ目の部分であるマクロで作業するときに必要となります。そこでは、コンパイル中にできるだけ多くの記録可能な詳細を取得することを目指します。

<th>
  取得タイミング
</th>
<td>
  コンパイル
</td>
<td>
  コンパイル
</td>
<td>
  コンパイル
</td>
<td>
  コンパイル
</td>
<td>
  ランタイム
</td>
<td>
  ランタイム
</td>
<td>
  ランタイム
</td>
<td>
  ランタイム
</td>
<td>
  ランタイム
</td>
<td>
  ランタイム
</td>
情報
イベントタイプ
クラス名
メソッド名
メソッドに渡される引数
clsソースコードの行番号
生成されたintコードの行番号
ユーザー名
日付/時刻
メッセージ
IPアドレス

上記のテーブルのプロパティを含むApp.Logクラスを作成しましょう。 App.Logオブジェクトが作成されると、ユーザー名、日付/時刻、およびIPアドレスプロパティは、自動的に入力されます。

App.Logクラス:

Class App.Log Extends %Persistent
{

/// イベントのタイプ
Property EventType As %String(MAXLEN = 10, VALUELIST = ",NONE,FATAL,ERROR,WARN,INFO,STAT,DEBUG,RAW") [ InitialExpression = "INFO" ];

/// クラスの名前、イベントが起きた場所
Property ClassName As %Dictionary.Classname(MAXLEN = 256);

/// メソッドの名前、イベントが起きた場所
Property MethodName As %String(MAXLEN = 128);

/// intコードの行
Property Source As %String(MAXLEN = 2000);

/// clsコードの行
Property SourceCLS As %String(MAXLEN = 2000);

/// Cacheユーザー
Property UserName As %String(MAXLEN = 128) [ InitialExpression = {$username} ];

/// メソッドに渡された引数の値
Property Arguments As %String(MAXLEN = 32000, TRUNCATE = 1);

/// 日付と時刻
Property TimeStamp As %TimeStamp [ InitialExpression = {$zdt($h, 3, 1)} ];

/// ユーザーメッセージ
Property Message As %String(MAXLEN = 32000, TRUNCATE = 1);

/// ユーザーのIPアドレス
Property ClientIPAddress As %String(MAXLEN = 32) [ InitialExpression = {..GetClientAddress()} ];

/// ユーザーIPアドレスの特定
ClassMethod GetClientAddress()
{
    // %CSP.Session source is preferable
    #dim %request As %CSP.Request
    If ($d(%request)) {
        Return %request.CgiEnvs("REMOTE_ADDR")
    }
    Return $system.Process.ClientIPAddress()
}
}

 

ロギングマクロ

通常、マクロは、その定義を含む個別の *.incファイルに保存されます。 必要なファイルは、Include MacroFileNameコマンドを使って、クラスに含めることができます。この場合、Include App.LogMacroとなります。
 
初めに、ユーザーがアプリケーションコードに追加するメインのマクロを定義しましょう。

#define LogEvent(%type, %message) Do ##class(App.Log).AddRecord($$$CurrentClass, $$$CurrentMethod, $$$StackPlace, %type, $$$MethodArguments, %message)

このマクロは、イベントタイプとメッセージの2つの入力引数を受け入れます。 メッセージ引数はユーザーが定義しますが、イベントタイプパラメーターには、イベントタイプを自動的に識別する、別の名前による追加のマクロが必要となります。

#define LogNone(%message)         $$$LogEvent("NONE", %message)
#define LogError(%message)        $$$LogEvent("ERROR", %message)
#define LogFatal(%message)        $$$LogEvent("FATAL", %message)
#define LogWarn(%message)         $$$LogEvent("WARN", %message)
#define LogInfo(%message)         $$$LogEvent("INFO", %message)
#define LogStat(%message)         $$$LogEvent("STAT", %message)
#define LogDebug(%message)        $$$LogEvent("DEBUG", %message)
#define LogRaw(%message)          $$$LogEvent("RAW", %message)

したがって、ロギングを実行するには、ユーザーはアプリケーションコードに$$$LogError("Additional message")のみを配置するだけで済みます。
後は、$$$CurrentClass$$$CurrentMethod$$$StackPlace$$$MethodArgumentsマクロを定義するのみです。 では、最初の3つから始めましょう。

#define CurrentClass     ##Expression($$$quote(%classname))
#define CurrentMethod    ##Expression($$$quote(%methodname))
#define StackPlace       $st($st(-1),"PLACE")

%classname%methodname変数は、ドキュメントに記載されています。 $stack関数はINTコードの行番号を返します。 これをCLS行番号に変換するには、このコードを使用できます。

%Dictionaryパッケージを使用して、メソッド引数とその値のリストを取得しましょう。 これにはメソッドの説明を含む。クラスに関するすべての情報が含まれています。 特に関心があるのは%Dictionary.CompiledMethodクラスとFormalSpecParsedプロパティで、これはリストです。

$lb($lb("Name","Classs","Type(Output/ByRef)","Default value "),...)

これはメソッドのシグネチャに対応しています。 たとえば次のコードがあるとします。

ClassMethod Test(a As %Integer = 1, ByRef b = 2, Output c)

このコードには、次のFormalSpecParsed値があります。

$lb(
$lb("a","%Library.Integer","","1"),
$lb("b","%Library.String","&","2"),
$lb("c","%Library.String","*",""))

$$$MethodArgumentsマクロを次のコードに展開する必要があります(Testメソッド)。

"a="_$g(a,"Null")_"; b="_$g(b,"Null")_"; c="_$g(c,"Null")_";"

これを行うには、コンパイル中に次のことを行う必要があります。

  • クラス名とメソッド名を取得する
  • %Dictionary.CompiledMethodクラスの対応するインスタンスを開いて、そのFormalSpecプロパティを取得する
  • それをソースコード行に変換する
  • 対応するメソッドをApp.Logクラスに追加しましょう。

    ClassMethod GetMethodArguments(ClassName As %String, MethodName As %String) As %String
    {
        Set list = ..GetMethodArgumentsList(ClassName,MethodName)
        Set string = ..ArgumentsListToString(list)
        Return string
    }
    
    ClassMethod GetMethodArgumentsList(ClassName As %String, MethodName As %String) As %List
    {
        Set result = ""
        Set def = ##class(%Dictionary.CompiledMethod).%OpenId(ClassName _ "||" _ MethodName)
        If ($IsObject(def)) {
            Set result = def.FormalSpecParsed
        }
        Return result
    }
    
    ClassMethod ArgumentsListToString(List As %List) As %String
    {
        Set result = ""
        For i=1:1:$ll(List) {
            Set result = result _ $$$quote($s(i>1=0:"",1:"; ") _ $lg($lg(List,i))_"=")
            _ "_$g(" _ $lg($lg(List,i)) _ ","_$$$quote(..#Null)_")_"
            _$s(i=$ll(List)=0:"",1:$$$quote(";"))
        }
        Return result
    }

    次に$$$MethodArgumentsマクロを以下のように定義しましょう。

    #define MethodArguments ##Expression(##class(App.Log).GetMethodArguments(%classname,%methodname))

    ユースケース

    それでは、ロギングシステムの機能を示すために、Testメソッドを使ってApp.Useクラスを作成しましょう。

    Include App.LogMacro
    Class App.Use [ CompileAfter = App.Log ]
    {
    /// Do ##class(App.Use).Test()
    ClassMethod Test(a As %Integer = 1, ByRef b = 2)
    {
        $$$LogWarn("Text")
    }
    }

    上記の結果、intコードの$$LogWarn("Text")マクロは次の行に変換されます。

    Do ##class(App.Log).AddRecord("App.Use","Test",$st($st(-1),"PLACE"),"WARN","a="_$g(a,"Null")_"; b="_$g(b,"Null")_";", "Text")

    このコードを実行すると、新しいApp.Logレコードが作成されます。

    改善点

    ロギングシステムを作成したところで、次のような改善のアイデアがあります。

  • まず、オブジェクト型引数を処理するようにすることができます。現在の実装では、オブジェクトorefしか記録しないためです。
  • 次に、呼び出しで、保存された引数値からメソッドのコンテキストを復元するようにできます。
  • オブジェクト型引数の処理

    引数の値をログに入れる行は、ArgumentsListToStringメソッドに生成され、次のようになります。

    "_$g(" _ $lg($lg(List,i)) _ ","_$$$quote(..#Null)_")_"

    リファクタリングを行って、それを、変数名とクラス(FormalSpecParsedから知ることができます)を受け入れる別のGetArgumentValueメソッドに移動し、変数を行に変換するコードを出力してみましょう。 データ型には既存のコードを使用し、オブジェクトは、SerializeObject(ユーザーコードから呼び出すため)とWriteJSONFromObject(オブジェクトをJSONに変換するため)メソッドを使ってJSONに変換されます。

    ClassMethod GetArgumentValue(Name As %String, ClassName As %Dictionary.CacheClassname) As %String
    {
        If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
            // オブジェクトです
            Return "_##class(App.Log).SerializeObject("_Name _ ")_"
        } Else {
            // データ型です
            Return "_$g(" _ Name _ ","_$$$quote(..#Null)_")_"
        }
    }
    
    ClassMethod SerializeObject(Object) As %String
    {
        Return:'$IsObject(Object) Object
        Return ..WriteJSONFromObject(Object)
    }
    
    ClassMethod WriteJSONFromObject(Object) As %String [ ProcedureBlock = 0 ]
    {
        Set OldIORedirected = ##class(%Device).ReDirectIO()
        Set OldMnemonic = ##class(%Device).GetMnemonicRoutine()
        Set OldIO = $io
        Try {
            Set Str=""
    
            //IOを現在のルーチンにリダイレクト。以下に定義するラベルを利用。
            Use $io::("^"_$ZNAME)
    
            //リダイレクトを有効にします
            Do ##class(%Device).ReDirectIO(1)
    
            Do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(Object)
        } Catch Ex {
            Set Str = ""
        }
    
        //元のリダイレクト/ニューモニックルーチンの設定に戻ります
        If (OldMnemonic '= "") {
            Use OldIO::("^"_OldMnemonic)
        } Else {
            Use OldIO
        }
        Do ##class(%Device).ReDirectIO(OldIORedirected)
    
        Quit Str
     
        // IOリダイレクトが可能なラベル
        // 文字の読み取り。読み取りは重要ではありません
    rchr(c)      Quit
        // 文字列の読み取り。読み取りは重要ではありません
    rstr(sz,to)  Quit
        // 文字の書き込み。出力ラベルを呼び出します
    wchr(s)      Do output($char(s))  Quit
        // フォームフィードの書き込み。出力ラベルを呼び出します
    wff()        Do output($char(12))  Quit
        // 改行の書き込み。出力ラベルを呼び出します
    wnl()        Do output($char(13,10))  Quit
        // 文字列の書き込み。出力ラベルを呼び出します
    wstr(s)      Do output(s)  Quit
        // タブの書き込み。出力ラベルを呼び出します
    wtab(s)      Do output($char(9))  Quit
        // 出力ラベル。ここで、実際に行いたいことを処理します。
        // ここではStrに書き込みます
    output(s)    Set Str = Str_s Quit
    }

    オブジェクト型引数のログエントリは次のようになります。

    コンテキストの復元

    このメソッドの主旨は、すべての引数を現在のコンテキストで(主にデバッグ用のターミナルで)使用できるようにすることです。 これを行うために、ProcedureBlockメソッドパラメーターを使用することができます。 0に設定すると、そのようなメソッドに宣言されたすべての変数はメソッドを終了してもそのまま使用することができます。 ここでのメソッドは、App.Logクラスのオブジェクトを開いて、Argumentsプロパティを逆シリアル化します。

    ClassMethod LoadContext(Id) As %Status [ ProcedureBlock = 0 ]
    {
        Return:'..%ExistsId(Id) $$$OK
        Set Obj = ..%OpenId(Id)
        Set Arguments = Obj.Arguments
        Set List = ..GetMethodArgumentsList(Obj.ClassName,Obj.MethodName)
        For i=1:1:$Length(Arguments,";")-1 {
            Set Argument = $Piece(Arguments,";",i)
            Set @$lg($lg(List,i)) = ..DeserializeObject($Piece(Argument,"=",2),$lg($lg(List,i),2))
        }
        Kill Obj,Arguments,Argument,i,Id,List
    }
    
    ClassMethod DeserializeObject(String, ClassName) As %String
    {
        If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
            // オブジェクトです
            Set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(String,,.obj)
            Return:$$$ISOK(st) obj
        }
        Return String
    }

    ターミナルでは次のように表示されます。

    > zw
    > do ##class(App.Log).LoadContext(2) 
    > zw
    
    a=1 b=<OBJECT REFERENCE> [2@%ZEN.proxyObject]
    > zw b 
    b=<OBJECT REFERENCE> [2@%ZEN.proxyObject]
    +----------------- general information --------------- 
    |      oref value: 2 
    |      class name: %ZEN.proxyObject 
    | reference count: 2 
    +----------------- attribute values ------------------ 
    |           %changed = 1 
    |     %data("prop1") = 123 
    |     %data("prop2") = "abc" 
    |             %index = ""
    

    この続きは?

    鍵となる潜在的な改善点は、メソッド内に作成された任意の変数リストを使用して、ログクラスに別の引数を追加することです。

    まとめ

    マクロはアプリケーション開発に非常に役立ちます。

    質問

    コンパイル中に行番号を取得する方法はありますか?

    リンク

    0
    0 398
    記事 Toshihiko Minamoto · 9月 23, 2021 10m read

    この記事は、視覚化ツールと時系列データの分析を説明する連載の最初の記事です。 当然ながら、Caché製品ファミリーから収集できるパフォーマンス関連のデータを見ることに焦点を当てますが、 説明の途中で、他の内容についても解説していきます。 まずは、Pythonとそのエコシステムで提供されているライブラリ/ツールを探りましょう。

    この連載は、Murrayが投稿したCachéのパフォーマンスと監視に関する優れた連載(こちらから参照)、より具体的にはこちらの記事と密接に関係しています。

    免責事項1: 確認しているデータの解釈について話すつもりですが、それを詳しく話すと実際の目標から外れてしまう可能性があります。 そのため、Murrayの連載を先に読んで、主題の基本的な理解を得ておくことを強くお勧めします。

    免責事項2: 収集したデータを視覚化するために使用できるツールは山ほどあります。 その多くは、mgstatなどから得たデータを直接処理するが、必要最低限の調整だけで処理することができます。 この記事は「このソリューションがベストですよ」という投稿ではまったくなく、 あくまでも、「データを操作する上で便利で効果的な方法を見つけたよ」という記事です。

    免責事項3: データの視覚化と分析は、詳しく見るほど非常にやみつきになる、刺激的で楽しい分野です。 これに没頭して自由な時間を失ってしまう可能性があります。 でも、警告しましたからね!

    では、前置きはこれくらいにして、早速内容を見ていきましょう。

    前提条件

    始める前に、次のツールとライブラリを用意してください。

    • Jupyterノートブック
    • Python (3)
    • 作業中に使用するさまざまなPythonライブラリ

    Python(3): マシンに Python が必要です。 アーキテクチャごとにインストールの方法が様々です。 私はmacでhomebrewを使用しており、これを使って簡単にインストールできました。

    brew install python3
    

    Googleであなたのお気に入りのプラットフォームに使用できる手順を検索してください。

    Jupyterノートブック: 厳密には必要ではありませんが、Jupyterノートブックがあれば、Pythonスクリプトでの作業が楽になります。 ブラウザウィンドウからインタラクティブにPythonスクリプトを実行して表示することができます。 また、スクリプトでの共同作業も可能です。 コードの実験と操作を非常に簡単に行えるようになるため、強くお勧めします。

    pip3 install jupyter
    

    (繰り返しになりますが、$search(検索)エンジンに尋ねてください)

    Pythonライブラリ Pythonライブラリについては使用しながら説明します。 importステートメントでエラーが発生している場合は、まずは、ライブラリがインストールされていることを確認してください。

    pip3 install matplotlib
    

    はじめに

    マシンにすべてがインストールされていれば、ライブラリから

    jupyter notebook
    

    を実行できるはずです。 このコードを実行すると自動的にブラウザウィンドウが開き、単純なUIが表示されます。 空のノートブック

    早速メニューから新しいノートブックを作成し、最初のコードセルにインポートステートメントをいくつか追加します(新規 -> ノートブック -> Python3)。

    ノートブックのインポート

    import math
    import pandas as pd
    import mpl_toolkits.axisartist as AA
    from mpl_toolkits.axes_grid1 import host_subplot
    import matplotlib.pyplot as plt
    from datetime import datetime
    from matplotlib.dates import DateFormatter
    

    インポートしているライブラリについて、いくつかのコメントがあります。

    • Pandas これは、Pythonプログラミング言語向けの高性能で使いやすいデータ構造とデータ解析ツールを提供するオープンソースのBSDラインセンスライブラリです。 これにより、ビッグデータセットを効率的に処理することができます。 pButtonsから取得するデータセットは、決して「ビッグデータ」ではありませんが、 一度にたくさんのデータを確認できるので安心です。 過去20年に渡って、24時間/2秒サンプリングでpButtonsを収集していたとしても、 それをグラフ化できるということです。
    • Matplotlib mataplotlibは、さまざまなハードコピー形式やプラットフォーム間でのインタラクティブ環境で出版グレードの図を生成するPythonの2Dグラフ作成ライブラリです。 この記事のメインのグラフ作成エンジンとしてとりあえず使用するのがこのライブラリです。

    現在のコードセルの実行(ショートカット: Ctrl+Enter)(ショートカット一覧)でエラーが発生している場合は、上記のライブラリがインストールされていることを確認してください。

    また、Untitledノートブックの名前も変更していることに気づくでしょう。名前を変更するには、タイトルをクリックしてください。

    データの読み込み

    基礎工事が終了したので、データをいくらか読み込むことにしましょう。 幸い、PandasにはCSVデータを簡単に読み込む方法があります。 都合よくcsv形式のmgstatデータのセットが手元にあるため、それを使うことにします。

    mgstatfile = '/Users/kazamatzuri/work/proj/vis-articles/part1/mgstat.txt'
    data = pd.read_csv(
        mgstatfile, 
        header=1,
        parse_dates=[[0,1]]
       )
    

    read_csvコマンドを使用して、mgstatデータを直接DataFrameに読み取っています。 オプションに関する総合的な概要は、ドキュメント全文をご覧ください。 つまり、読み取るファイルを渡して、2行目(1行目は0です!)にヘッダー名が含まれていることを伝えているだけです。 mgstatは日付と時刻のフィールドを2つのフィールドに分割するため、parse_datesパラメーターでそれらのフィールドを組み合わせることも必要です。

    data.info()
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 25635 entries, 0 to 25634
    Data columns (total 37 columns):
    Date_       Time        25635 non-null datetime64[ns]
      Glorefs               25635 non-null int64
     RemGrefs               25635 non-null int64
     GRratio                25635 non-null int64
      PhyRds                25635 non-null int64
     Rdratio                25635 non-null float64
     Gloupds                25635 non-null int64
     RemGupds               25635 non-null int64
     Rourefs                25635 non-null int64
     RemRrefs               25635 non-null int64
      RouLaS                25635 non-null int64
     RemRLaS                25635 non-null int64
      PhyWrs                25635 non-null int64
       WDQsz                25635 non-null int64
      WDtmpq                25635 non-null int64
     WDphase                25635 non-null int64
      WIJwri                25635 non-null int64
      RouCMs                25635 non-null int64
     Jrnwrts                25635 non-null int64
       GblSz                25635 non-null int64
     pGblNsz                25635 non-null int64
     pGblAsz                25635 non-null float64
       ObjSz                25635 non-null int64
     pObjNsz                25635 non-null int64
     pObjAsz                25635 non-null int64
       BDBSz                25635 non-null int64
     pBDBNsz                25635 non-null int64
     pBDBAsz                25635 non-null float64
      ActECP                25635 non-null int64
      Addblk                25635 non-null int64
     PrgBufL                25635 non-null int64
     PrgSrvR                25635 non-null int64
      BytSnt                25635 non-null int64
      BytRcd                25635 non-null int64
      WDpass                25635 non-null int64
      IJUcnt                25635 non-null int64
     IJULock                25635 non-null int64
    dtypes: datetime64[ns](1), float64(3), int64(33)
    memory usage: 7.2 MB
    

    上記は、収集されたDataFrameの概要を示します。

    データの操作

    一部のフィールド名にはスペースが含まれており、「Date_ Time」は扱いにくいため、文字列を削除して最初の列の名前を変更します。

    data.columns=data.columns.str.strip()
    data=data.rename(columns={'Date_       Time':'DateTime'})
    

    DataFrameはデフォルトでRangeIndexになります。 これはこのデータを確認するにはあまり役に立ちません。 より実用的なDateTime列を使用できるため、それをインデックスとして設定することにします。

    data.index=data.DateTime
    

    これで、最初のバージョンのプロットを作成できるようになりました。 最初に確認するものの1つは必ずGlorefsであるため、Glorefsを使用してみましょう。

    plt.figure(num=None, figsize=(16,5), dpi=80, facecolor='w', edgecolor='k')
    plt.xticks(rotation=70)
    plt.plot(data.DateTime,data.Glorefs)
    plt.show()
    

    まず、ライブラリにグラフのサイズを指示しています。 また、x軸のラベルを少し回転させて、重なり合わないようにしています。 最後に、DateTimeとGlorefsのプロットを作成し、グラフを表示しています。 これで、次のようなグラフが出来上がります。

    Glorefs

    Glorefsをほかの列に置き換えるだけで、何が起きているのかを大まかに捉えることができます。

    グラフの合成

    複数のグラフをまとめて表示すると非常に便利な場合があります。 そのため、当然、複数のプロットを1つのグラフに描こうと考えるでしょう。 matplotlib だけでこれを行うのは非常に簡単です。

    plt.plot(data.DateTime,data.Glorefs)
    plt.plot(data.DateTime,data.PhyRds)
    plt.show()
    

    ところが、これでは前とほぼ同じグラフが作成されてしまいます。 問題はy軸です。 Glorefsは数百万に達するのに対し、PhyRdsはたいてい数百(から数千)であるため、PhyRdsはほぼ読み取れません。

    これを解決するには、前にインポートしたaxisartistツールキットを使用する必要があります。

    plt.gcf()
    plt.figure(num=None, figsize=(16,5), dpi=80, facecolor='w', edgecolor='k')
    host = host_subplot(111, axes_class=AA.Axes)
    plt.subplots_adjust(right=0.75)
    
    par1 = host.twinx()
    par2 = host.twinx()
    offset = 60
    new_fixed_axis = par2.get_grid_helper().new_fixed_axis
    par2.axis["right"] = new_fixed_axis(loc="right",axes=par2,offset=(offset, 0))
    par2.axis["right"].toggle(all=True)
    
    host.set_xlabel("time")
    host.set_ylabel("Glorefs")
    par1.set_ylabel("Rdratio")
    par2.set_ylabel("PhyRds")
    
    p1,=host.plot(data.Glorefs,label="Glorefs")
    p2,=par1.plot(data.Rdratio,label="Rdratio")
    p3,=par2.plot(data.PhyRds,label="PhyRds")
    
    host.legend()
    
    host.axis["left"].label.set_color(p1.get_color())
    par1.axis["right"].label.set_color(p2.get_color())
    par2.axis["right"].label.set_color(p3.get_color())
    
    plt.draw()
    plt.show()
    

    簡単に要約すると、2つのy軸をプロットに追加し、それぞれに異なる尺度を設定するということになります。 最初の例では暗黙的にサブプロットを使用しましたが、この場合は、直接それにアクセスして、軸とラベルを追加する必要があるため、 2つのラベルと色を設定しています。 凡例を追加して、様々なプロットに色を繋げたら、次のような画像になります。

    合成

    最後に

    これでデータをプロットするための非常に強力なツールを手に入れました。 mgstatデータを読み込んでいくつかの基本的なプロットを作成する方法を探りました。 次のパートでは、ほかのグラフの出力形式を試し、さらに多くのデータを取得することにします。

    コメントやご質問をお受け付けしております! あなたの体験をシェアしてください!

    -Fab

    追伸: これに使用するノートブックはこちらにあります。

    0
    0 155
    記事 Toshihiko Minamoto · 7月 1, 2021 5m read

    クラスのコンパイル時に、定義済みのプロパティ、クエリ、またはインデックスごとに対応する複数のメソッドが自動的に生成されます。 これらのメソッドは非常に便利です。 この記事では、その一部について説明します。

    プロパティ

    「Property」というプロパティを定義したとしましょう。 次のメソッドが自動的に利用できるようになります(太字のPropertyは変化する部分でプロパティ名になります)。

    ClassMethod PropertyGetStored(id)

    このメソッドは、データ型プロパティの場合は論理値を返し、オブジェクトプロパティの場合はIDを返します。 これはクラスのデータグローバルへの、ラップされたグローバル参照であり、特異なプロパティ値を取得する上でもっとも手早い方法です。 このメソッドは、保存されたプロパティでのみ利用できます。

    Method PropertyGet()

    プロパティgetterである。 再定義可能です。

    Method PropertySet(val) As %Status

    プロパティsetterである。 再定義可能です。


    オブジェクトプロパティ

    オブジェクトプロパティの場合、IDとOIDアクセスに関連するいくつかの追加メソッドを利用できるようになります。

    Method PropertySetObjectId(id)

    このメソッドはIDでプロパティ値を設定するため、オブジェクトを開いてプロパティ値として設定する必要はありません。

    Method PropertyGetObjectId()

    このメソッドは、プロパティ値のIDを返します。

    Method PropertySetObject(oid)

    このメソッドは、OIDでプロパティ値を設定します。

    Method PropertyGetObject()

    このメソッドは、プロパティ値のOIDを返します。


    データ型プロパティ

    データ型プロパティの場合、異なる形式間で変換するためのほかのメソッドがいくつか利用できるようになります。

    ClassMethod PropertyDisplayToLogical(val)
    ClassMethod PropertyLogicalToDisplay(val)
    ClassMethod PropertyOdbcToLogical(val)
    ClassMethod PropertyLogicalToOdbc(val)
    ClassMethod PropertyXSDToLogical(val)
    ClassMethod PropertyLogicalToXSD(val)
    ClassMethod PropertyIsValid(val) As %Status

    valが有効なプロパティ値であるかどうかをチェックします。

    ClassMethod PropertyNormalize(val)

    正規化された論理値を返します。

    注意事項

    • 関係はプロパティであり、これらのメソッドで取得/設定できます。
  • 入力値(val)は、形式変換メソッドを除き、必ず論理値です。

  • インデックス

    「Index」というインデックスの場合、次のメソッドを自動的に利用できるようになります。

    ClassMethod IndexExists(val) As %Boolean

    このvalを持つオブジェクトが存在するかどうかに応じて、1または0を返します。valはインデックス付きのプロパティの論理値です。


    一意のインデックス

    一意のインデックスの場合、追加メソッドを利用できるようになります。

    ClassMethod IndexExists(val, Output id) As %Boolean

    このvalを持つオブジェクトが存在するかどうかに応じて、1または0を返します。valはインデックス付きのプロパティの論理値です。 また、オブジェクトIDがある場合は、第2引数として返します。

    ClassMethod IndexDelete(val, concurrency = -1) As %Status

    インデックスの値がvalのエントリを削除します。

    ClassMethod IndexOpen(val, concurrency, sc As %Status)  

    インデックスの値がvalの既存のオブジェクトを返します。

    注意事項:

    a)インデックスは複数のプロパティに基づいている可能性があるため、メソッドシグネチャは、入力として複数の値を持つように変更されます。例として、次のインデックスを見てみましょう。

    Index MyIndex On (Prop1, Prop2);

    この場合、IndexExistsメソッドには次のシグネチャがあります。

    ClassMethod IndexExists(val1, val2) As %Boolean

    val1はProp1値に対応し、val2はProp2値に対応します。 その他のメソッドも同じロジックに従います。

    b)Cachéは、IDフィールド(RowID)にインデックスを作成するIDKEYインデックスを生成します。 ユーザーが再定義することが可能で、複数のプロパティを含むこともできます。 たとえば、クラスにプロパティが定義されているかをチェックするには、次を実行します。

    Write ##class(%Dictionary.PropertyDefinition).IDKEYExists(class, property)

    c)すべてのインデックスメソッドは、論理値をチェックします。

    d) ドキュメント

    クエリ

    「Query」というクエリの場合(単純なSQLクエリまたはカスタムクラスクエリのどちらでも構いません。これに関する記事をご覧ください)、Funcメソッドが生成されます。

    ClassMethod QueryFunc(Arg1, Arg2) As %SQL.StatementResult

    このメソッドは、クエリの反復処理に使用される%SQL.StatementResultを返します。 たとえば、SamplesネームスペースのSample.Personクラスには、1つのパラメーターを受け入れるByNameクエリがあり、 次のコードを使って、オブジェクトコンテキストから呼び出すことができます。

    Set ResultSet=##class(Sample.Person).ByNameFunc("A")
    While ResultSet.%Next() { Write ResultSet.Name,! }

    また、GitHubには、これらのメソッドを実演するデモクラスがあります。

    0
    1 201
    記事 Mihoko Iijima · 6月 28, 2021 2m read

    これは InterSystems FAQ サイトの記事です。

    クラス定義の Property 定義文の名称を直接変更した場合、内部的には 元のプロパティを削除し、新しいプロパティを追加 したことになります。

    このため、すでにデータを格納している永続クラスのプロパティ名をエディタで直接変更した場合、元のプロパティで設定されていた値にはアクセスができなくなります(新しいデータの格納位置が割り当てられます)。  

    また、変更したプロパティが必須(非ヌル)に指定されていた場合、データが存在しないために参照時にエラーが発生します。

    以下の図は、左画面が変更前の状態、右画面がプロパティ名を変更してコンパイルを行った状態です。

    基本的に、このような操作を行う場合は、あらかじめデータをエクスポートし、データの削除を行い、クラス定義の変更を行ってから、データを再インポートする方法が安全です。

    または、スタジオのメニューの クラス > リファクタ > 名前変更 を利用してプロパティ名を変更します。
    (リファクタの名前変更メニューにより、クラス定義内のストレージやメソッド定義に対して、一括でプロパティ名を変更できます。)

    変更対象のクラス定義を開き、修正したいプロパティ名を選択し、スタジオのメニューの クラス > リファクタ > 名前変更 を選択します。

    0
    0 421
    記事 Toshihiko Minamoto · 6月 15, 2021 11m read

    MonCaché — InterSystems Caché での MongoDB API 実装

    免責事項:この記事は筆者の私見を反映したものであり、InterSystemsの公式見解とは関係ありません。

    構想

    プロジェクトの構想は、クライアント側のコードを変更せずに MongoDB の代わりに InterSystems Caché を使用できるように、ドキュメントを検索、保存、更新、および削除するための基本的な MongoDB(v2.4.9) API 機能を実装するところにあります。

    動機

    おそらく、MongoDB に基づくインターフェースを使って、データストレージに InterSystems Caché を使用すると、パフォーマンスが向上するのではないか。 このプロジェクトは、学士号を取得するための研究プロジェクトとして始まりました。

    ダメなわけないよね?! ¯\(ツ)

    制限

    この研究プロジェクトを進める過程で、いくつかの簡略化が行われました。

    • プリミティブ型のデータのみを使用する: nullbooleannumberstringarrayobjectObjectId
    • クライアント側のコードは MongoDB ドライバーを使って MongoDB と連携する。
    • クライアント側のコードでは、MongoDB Node.js ドライバーを使用する。
    • クライアント側のコードでは、MongoDB API の基本的な機能のみを使用する:
      • findfindOne — ドキュメントの検索
      • saveinsert — ドキュメントの保存
      • update — ドキュメントの更新
      • remove — ドキュメントの削除
      • count — ドキュメント数のカウント

    実装

    タスクは最終的に次のサブタスクに分割されました。

    • 選択した基本機能ように、MongoDB Node.js ドライバーのインターフェースを再作成する。
    • このインターフェースを InterSystems Caché を使用してデータストレージ用に実装する。
      • Caché でデータベースの表現スキームを設計する。
      • Caché でコレクションの表現スキームを設計する。
      • Caché でドキュメントの表現スキームを設計する。
      • Node.js を使って Caché と対話するためのスキームを設計する。
      • 設計したスキームを実装して、少しテストする。 :)

    実装の詳細

    最初のサブタスクは問題ではなかったので、中間の説明を省略して、インターフェースの実装部分を説明します。

    MongoDB は、データベースをコレクションの物理的なコンテナとして定義します。 コレクションはドキュメントのセットで、ドキュメントはデータのセットです。 ドキュメントは JSON ドキュメントのようですが、大量の許容型を持っています。つまり BSON です。

    InterSystems Caché では、すべてのデータはグローバルに保存されます。 簡単に言えば、階層データ構造として考えることができます。

    このプロジェクトでは、すべてのデータは単一のグローバル(^MonCache )に保存されます。

    そのため、階層データ構造を使用して、データベース、コレクション、およびドキュメントの表現スキームを設計する必要があります。

    Caché におけるデータベースの表現スキーム

    実装したグローバルレイアウトは、数多くの潜在的なレイアウトの 1 つに過ぎず、これらのレイアウトには、それぞれにメリットと制限があります。

    MongoDB では、1 つのインスタンスに複数のデータベースが存在する可能性があるため、複数の分離されたデータベースを格納できるようにする表現スキームを設計する必要があります。 MongoDB はコレクションをまったく含まないデータベースをサポートしていることに注意しておきましょう(これらを「空」のデータベースと呼ぶことにします)。

    私はこの問題に対し、一番単純で一番明白なソリューションを選択しました。 データベースは、^MonCache グローバルの第 1 レベルノードとして表されます。 さらに、そのようなノードは、「空」のデータベースのサポートを有効にするために ”” 値を取得します。 問題は、これを行わずに子ノードを追加するだけの場合、それらを削除すると親ノードも削除されるということです(これがグローバルの動作です)。

    したがって、各データベースは Caché 内で次のように表されます。

    ^MonCache(<db>) = ""

    たとえば、「my_database」データベースの表現は次のようになります。

    ^MonCache("my_database") = ""

    Caché におけるコレクションの表現スキーム

    MongoDB は、コレクションをデータベースの要素として定義します。 単一のデータベース内にあるすべてのコレクションには、正確なコレクションの識別に使用できる一意の名前があります。 このことが、グローバルでコレクションを表現する単純な方法を見つけ、第 2 レベルのノードを使用する上で役立ちました。 次に、2 つの小さな問題を解決する必要があります。 1 つ目の問題は、コレクションがデータベースと同じように空であることができるということです。 2 つ目は、コレクションが一連のドキュメントであるということです。 そして、これらのドキュメントはすべて、互いに分離している必要があります。 正直なところ、コレクションノードの値として自動的に増分する値のようなカウンターを使う以外に良いアイデアが浮かびませんでした。 すべてのドキュメントには一意の番号があります。 新しいドキュメントが挿入されると、現在のカウンターの値と同じ名前が付いた新しいノードが作成され、カウンターの値が 1 つ増加するというアイデアです。

    したがって、各 Caché コレクションは、次のように表されます。

    ^MonCache(<db>) = ""
    ^MonCache(<db>, <collection>) = 0

    たとえば、「 my_database」データベース内の「my_collection」コレクションは次のように表されます。

    ^MonCache("my_database") = ""
    ^MonCache("my_database", "my_collection") = 0

    Caché におけるドキュメントの表現スキーム

    このプロジェクトでは、ドキュメントは追加の ObjectID という型で拡張された JSON ドキュメントです。 ドキュメントの表現スキームは、階層データ構造で設計する必要がありました。 いくつかの驚きに遭遇したのはここです。 まず、Caché では「ネイティブ」の null がサポートされていなかったために、使用できなかったことです。 もう 1 つは、ブール値が 0 と 1 の定数で実装されているということです。 つまり、1 が true で 0 が false ということなのです。 一番予想していた問題は、ObjectId を格納する方法を考え出す必要があるということでした。 結局、これらの問題はすべて最も簡単な方法でうまく解決されたのです。というか、解決されたと思いました。 以下では、各データ型とその表現について説明します。

    Caché のインタラクションスキーム

    Node.js ドライバーの選択は、InterSystems Caché と連携する上で論理的かつ単純な決定であるように思われました(ドキュメントサイトには、Caché と対話するためのドライバーがほかにも掲載されています)。 ただし、ドライバーの機能では不十分です。 私が行いたかったのは、1 回のトランザクションで複数の挿入を実行することだったので、 Caché 側で MongoDB API をエミュレートするために使用される一連の Caché ObjectScript クラスを開発することにしました。

    Caché Node.js ドライバーは、Caché クラスにアクセスできませんでしたが、Caché からプロフラム呼び出しを行うことができました。 このことから、小さなツールが作成されました。ドライバーと Caché クラスを繋ぐ、一種のブリッジです。

    結局、スキームは次のようになりました。

    プロジェクトの作業を進めながら、NSNJSON(Not So Normal JSON: あんまり普通じゃない JSON)と名付けた、ドライバーを介して ObjectId、null、true、および false を Caché に「密輸」する特別な形式を作成しました。 この形式の詳細については、GitHub の対応するページ(NSNJSON)をご覧ください。

    MONCACHÉ の機能

    ドキュメント検索には、次の基準を使用できます。

    • $eq — 等号
    • $ne — 不等号
    • $not — 否定
    • $lt — より小さい(未満)
    • $gt — より大きい
    • $exists — 有無、存在

    ドキュメントの更新操作には、次の演算子を使用できます。

    • $set — 値の設定
    • $inc — 指定された数による増分
    • $mul — 指定された数による乗算
    • $unset — 値の削除
    • $rename — 値の名前変更

    以下のコードは、私がドライバーの公式ページから取得して少し修正したコードです。

    var insertDocuments = function(db, callback) {
     var collection = db.collection('documents');
     collection.insertOne({ site: 'Habrahabr.ru', topic: 276391 }, function(err, result) {
         assert.equal(err, null);
         console.log("Inserted 1 document into the document collection");
         callback(result);
     });
    }
    var MongoClient = require('mongodb').MongoClient
      , assert = require('assert');
    var url = 'mongodb://localhost:27017/myproject';
    MongoClient.connect(url, function(err, db) {
        assert.equal(null, err);
        console.log("Successfully connected to the server");
        insertDocument(db, function() {
             db.close();
         });
    });

    このコードは、MonCaché と互換性を持つように簡単に変更することができます。

    ドライバーを変更するだけです!

    // var MongoClient = require('mongodb').MongoClient
    var MongoClient = require('moncache-driver').MongoClient

    このコードが実行されると、^MonCache グローバルは次のようになります。

    ^MonCache("myproject","documents")=1
    ^MonCache("myproject","documents",1,"_id","t")="objectid"
    ^MonCache("myproject","documents",1,"_id","v")="b18cd934860c8b26be50ba34"
    ^MonCache("myproject","documents",1,"site","t")="string"
    ^MonCache("myproject","documents",1,"site","v")="Habrahabr.ru"
    ^MonCache("myproject","documents",1,"topic","t")="number"
    ^MonCache("myproject","documents",1,"topic","v")=267391

    デモ

    ほかのすべてとは別に、小型のデモアプリケーションソースコード)を公開しました。また、サーバーの再起動とソースコードの変更を行わないで、MongoDB Node.js から MonCaché Node.js へのドライバーの変更を実演するために、Node.js を使って実装しました。 アプリケーションは、CRUD 演算を製品やオフィスで実行するための小さなツールであり、構成を変更(ドライバーを変更)するためのインア―フェースでもあります。

    サーバーでは、構成で選択されたストレージ(Caché または MongoDB)に保存される製品オフィスを作成できます。

    Orders]タブには、注文のリストが含まれています。 レコードは作成されていますが、フォームは未完成です。 プロジェクトの援護を歓迎しています(ソースコード)。

    構成は、Configuration ページで変更できます。 このページには、MongoDB と MonCache の 2 つのボタンがあります。 対応するボタンをクリックすると、希望する構成を選択できます。 構成が変更されると、クライアントアプリケーションはデータソースに再接続します(実際に使用されているドライバーからアプリケーションを分離する概念)。

    まとめ

    結論を出すために、根本的な質問に答えさせてください。 そうです! 基本的な操作におけるパフォーマンスを向上させることができました。

    MonCaché プロジェクトは GitHub に公開されており、MIT ラインセンスの下に提供されています。

    簡易マニュアル

    • Caché のインストール
    • 必要な MonCaché コンポーネントを Caché に読み込む
    • Caché で MONCACHE 領域を作成する
    • Caché で、ユーザー名が「moncache」でパスワードが「ehcacnom」(moncache」の逆)のユーザーを作成する
    • 環境変数 MONCACHE_USERNAME = moncache を作成する
    • 環境変数 MONCACHE_PASSWORD = ehcacnom を作成する
    • 環境変数 MONCACHE_NAMESPACE = MONCACHE を作成する
    • プロジェクトで、依存関係を 'mongodb' から 'moncache-driver' に変更する
    • プロジェクトを始動! :-)

    InterSystems 教育プログラム

    InterSystems テクノロジーに基づく独自の研究プロジェクトを始めたい方は、InterSystems 教育プログラム専用の特設サイトをご覧ください。

    0
    0 147
    記事 Mihoko Iijima · 6月 4, 2021 3m read

    これは InterSystems FAQ サイトの記事です。

    ユーザーが作成したクラス定義は、クラス定義クラスの中に格納されます。

    クラス定義一覧をプログラムから取得する方法として、「クラス定義クラス」を利用することができます。

    メモ:クラス定義クラスとは、%Dictionary パッケージに含まれるクラス全般のことをさします。

    以下サンプルコードでは、%Dictionary.ClassDefinitionQuery クラスSummary クエリを利用してクラス定義一覧を取得しています。

    0
    0 273
    記事 Mihoko Iijima · 5月 7, 2021 10m read

    開発者の皆さん、こんにちは!

    この記事では、Java から IRIS へ接続する方法の中から XEP(ゼップ)を利用して、GPS (GPX)データを高速に取り込むサンプルをご紹介します。

    Java の実行環境や IRIS がお手元になくても大丈夫です!コンテナ 🐳 のビルド+開始で体験できる「実行環境テンプレート」をご用意しました。

    ソースコードは コミュニティの Git で公開 していますので、docker、docker-compose 、git がインストールされた環境であれば、すぐにお試しいただけます(Javaの実行環境はコンテナでご提供するので準備不要です)。

    操作方法は、Gitの README に記載しています。ぜひお試しください。

    この記事の中では、コード解説を追加しています。ぜひ、最後までお付き合いください!

     

    1)処理概要

    GPX データを IRIS へ渡す迄の流れにリアクティブプログラミングが行える RxJava2 のライブラリを使用しています。

    メモ:サンプルコードの中では、GPS データを直接受信するのではなく、Google マイマップやサンプル GPS データから GPX ファイルに変換したデータを入力に使用しています。

    0
    0 260
    記事 Mihoko Iijima · 4月 15, 2021 2m read

    これは InterSystems FAQ サイトの記事です。

    クラスに定義されたプロパティの情報については、以下システムクラスを利用して情報を取得できます。

    %Dictionary.ClassDefintion

    %Dictionary.PropertyDefinition

    コード記述例は以下の通りです。

    0
    0 391
    記事 Toshihiko Minamoto · 4月 12, 2021 24m read

    (1NF/2NF/3NF) からの引用

    行と列で特定される位置には、それぞれアプリケーションドメインの値が 1 つだけあります (それ以外は何もない)。 その目的によって、同じ値がアトミックであったり、なかったりします。 例えば、「4286」という値は、
    • 「クレジットカードの PIN コード」を意味するのであれば、アトミックとなります (破損している場合や並び替えられている場合は、使用できません)。
    • 単に「連続する番号」であれば、非アトミックとなります (いくつかに分割されていたり、並び替えられていても、値は意味を成します)。

    この記事では、文字列や日付、($LB 形式の) 単純なリスト、「list of <...>」、「array of <...>」といったフィールドの型を伴う SQL クエリのパフォーマンスを向上させる標準的な方法にして検証します。


    はじめに


    それでは、お馴染みの「電話番号の一覧」から見てみましょう。 以下のように、テストデータを作成します。

    create table cl_phones(tname varchar2(100), phone varchar2(30));
    insert into cl_phones(tname,phonevalues ('Andrew','867-843-25');
    insert into cl_phones(tname,phonevalues ('Andrew','830-044-35');
    insert into cl_phones(tname,phonevalues ('Andrew','530-055-35');
    insert into cl_phones(tname,phonevalues ('Max','530-055-35');
    insert into cl_phones(tname,phonevalues ('Max','555-011-35');
    insert into cl_phones(tname,phonevalues ('Josh','530-055-31');
    insert into cl_phones(tname,phonevalues ('Josh','531-051-32');
    insert into cl_phones(tname,phonevalues ('Josh','532-052-33');
    insert into cl_phones(tname,phonevalues ('Josh','533-053-35');

    では、各人物が持つ電話番号をコンマで区切ってまとめたリストを表示します。

    SELECT
       %exact(tnametname,
       LIST(phonephonestr
    FROM cl_phones
    GROUP BY tname

    または以下のコードを使います。

    SELECT
       distinct %exact(tnametname,
       LIST(phone %foreach(tname)) phonestr
    FROM cl_phones
    tnamephonestr
    結果
    Andrew 867-843-25,830-044-35,530-055-35
    Josh 530-055-31,531-051-32,532-052-33,533-053-35
    Max 530-055-35,555-011-35

    インデックスを電話番号別に作成すれば、特定の番号を使って素早く検索することができます。 このソリューションの唯一の欠点は、名前が重複する可能性があるということです。リストの要素が多くなれば、その分データベースも大きくなります。

    従い、同じフィールドに複数の値 (電話番号の一覧やその一部だけを集めた一覧、パスワードなど) をコンマで区切った文字列として格納しておくと、値別に素早く検索できるので、重宝することがあります。 もちろん、そのようなフィールドに対し普通のインデックスを作成し、長い文字列の中の部分文字列を検索することは可能です。しかし、第 1 にそのような要素が数多く含まれる可能性があり、またインデックスも非常に長くなり得るということ、そして第 2 に、そのようなインデックスを使用しても検索処理をまったく加速化できないという点を考慮する必要があります。

    そうでは、どうすれば良いのでしょう。

    このように、コレクションを持つフィールドを扱う状況に対処するために、インデックスの特殊な型が導入されています。 コレクションは「実在するもの」(組み込みの list of <...> および array of <...>) でも「仮想のもの」でも構いません。

    組み込みコレクションの場合、それ専用にこのようなインデックスを作成するのはシステムの仕事であり、開発者はそのプロセスに干渉できません。 但し、仮想コレクションであれば、インデックスの作成は開発者が担います。

    そのような「仮想」コレクションには、シンプルなコンマ区切りの文字列、日付、単純なリストなどがあります。

    コレクションのインデックスには、以下の構文を用います。

    INDEX idx1 ON (MyField(ELEMENTS));
    もしくは
    INDEX idx1 ON (MyField(KEYS));

    インデックスは、propertynameBuildValueArray メソッドを使って作成しますが、これは開発者が自分で実装しなければいけません。

    通常、このメソッドのシグネチャは以下のようになります。

    ClassMethod propertynameBuildValueArray(valueByRef valueArrayAs %Status

    引数の意味

    • value – 複数の要素に分割されるフィールド値
    • valueArray – 個別の要素を含む結果の配列。 配列は、キー / 値の組み合わせで、以下の形式が使われます。 array(key1)=value1 array(key2)=value2 などなど。

    先ほども触れましたが、このメソッドはシステムにより組み込みコレクション用に自動的に生成されるものであり、属性が [Final] であるため、開発者は再定義できません。

    それでは、これらのインデックスを作成し、SQL での活用方法を見てみましょう。

    注意:
     前の例で作られた構造が残らないよう、新しいインデックスを作成する前にグローバルやクラスのストレージスキーマを空にしておくことをおすすめします。

    コンマ区切りの文字列


    では、次のクラスを作成しましょう。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iPhones On Phones(ELEMENTS);

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">Phones </FONT><FONT COLOR="#000080">As %String</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">PhonesBuildValueArray(   </FONT><FONT COLOR="#ff00ff">value</FONT><FONT COLOR="#000000">,   </FONT><FONT COLOR="#000080">ByRef </FONT><FONT COLOR="#ff00ff">array</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">As %Status </FONT><FONT COLOR="#000000">{   </FONT><FONT COLOR="#0000ff">i </FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008000">"" </FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(0)=</FONT><FONT COLOR="#800000">value   </FONT><FONT COLOR="#800080">}</FONT><FONT COLOR="#0000ff">else</FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">list</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#0000ff">$lfs</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">","</FONT><FONT COLOR="#000000">),</FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">=0     </FONT><FONT COLOR="#0000ff">while $listnext</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">list</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#800000">item</FONT><FONT COLOR="#000000">)</FONT><FONT COLOR="#800080">{       </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#800000">item     </FONT><FONT COLOR="#800080">}   }   </FONT><FONT COLOR="#0000ff">q $$$OK </FONT><FONT COLOR="#000000">}

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT>   <FONT COLOR="#800080">&sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">phones</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000080">null union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'a' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'b,a' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'b,b' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'a,c,b' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">',,'   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    ターミナルで Fill() メソッドを呼び出します。

    USER><FONT COLOR="#0000ff">d </FONT><FONT COLOR="#000080">##class</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">demo.test</FONT><FONT COLOR="#000000">).</FONT><FONT COLOR="#0000ff">Fill</FONT><FONT COLOR="#000000">()</FONT>
    <FONT COLOR="#000000">^demo.testD=6
    ^demo.testD(1)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">)
    ^demo.testD(2)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"a"</FONT><FONT COLOR="#000000">)
    ^demo.testD(3)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"b,a"</FONT><FONT COLOR="#000000">)
    ^demo.testD(4)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"b,b"</FONT><FONT COLOR="#000000">)
    ^demo.testD(5)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"a,c,b"</FONT><FONT COLOR="#000000">)
    ^demo.testD(6)=</FONT><FONT COLOR="#0000ff">$lb</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">",,"</FONT><FONT COLOR="#000000">)
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" "</FONT><FONT COLOR="#000000">,1)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" "</FONT><FONT COLOR="#000000">,6)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" A"</FONT><FONT COLOR="#000000">,2)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" A"</FONT><FONT COLOR="#000000">,3)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" A"</FONT><FONT COLOR="#000000">,5)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" B"</FONT><FONT COLOR="#000000">,3)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" B"</FONT><FONT COLOR="#000000">,4)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" B"</FONT><FONT COLOR="#000000">,5)=</FONT><FONT COLOR="#008000">""</FONT><FONT COLOR="#000000">
    ^demo.testI(</FONT><FONT COLOR="#008000">"iPhones"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">" C"</FONT><FONT COLOR="#000000">,5)=</FONT><FONT COLOR="#008000">""</FONT>

    ご覧のとおり、インデックスには文字列全体ではなく、その特定の部分だけが含まれています。 従って開発者は好きなように長い文字列を複数の部分文字列に分割できます。 コンマ区切りの文字列以外にも、XML や JSON ファイルなども使えます。

    ID電話番号
    私たちのテーブルには以下が含まれます。
    1 (null)
    2 a
    3 b,a
    4 b,b
    5 a,c,b
    6 ,,

    それでは、「a」を含む部分文字列をすべて見つけましょう。 このためには、'%xxx%'['xxx' といった述語を使うことがルールになっています。

    select * from demo.test where Phones 'a'select * from demo.test where Phones like '%a%'

    しかし、この場合、iPhones インデックスは使用されません。 使用するには、特殊な述語を使う必要があります。

    FOR SOME %ELEMENT(ourfield) (%VALUE elementvalue)

    これを考慮して、以下のようなクエリを使います。

    select * from demo.test where for some %element(Phones) (%value 'a')

    特殊なインデックスを使ったおかげで、このクエリの処理速度は最初のバージョンを使った場合よりも大幅に速くなります。

    もちろん、以下のようにもっと複雑な条件を使うこともできます。(%Value %STARTSWITH 'а')(%Value 'a' and %Value 'b')(%Value in ('c','d'))(%Value is null)

    ここでさっと魔法をかけます。。。


    機密情報を隠す


    通常、BuildValueArray メソッドでは、value を使って array 配列を作成します。

    でも、このルールに従わなかったらどうなるのでしょう?

    次の例を試してみましょう。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iLogin On Login(ELEMENTS);

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">Login </FONT><FONT COLOR="#000080">As %String</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">LoginBuildValueArray(   </FONT><FONT COLOR="#ff00ff">value</FONT><FONT COLOR="#000000">,   </FONT><FONT COLOR="#000080">ByRef </FONT><FONT COLOR="#ff00ff">array</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">As %Status </FONT><FONT COLOR="#000000">{   </FONT><FONT COLOR="#0000ff">i </FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008000">"Jack" </FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(0)=</FONT><FONT COLOR="#008000">"test1"     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(1)=</FONT><FONT COLOR="#008000">"test2"     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(2)=</FONT><FONT COLOR="#008000">"test3"   </FONT><FONT COLOR="#800080">}</FONT><FONT COLOR="#0000ff">elseif </FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008000">"Pete" </FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"-"</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#008000">"111"     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"5.4"</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#008000">"222"     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"fg"</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#008000">"333"   </FONT><FONT COLOR="#800080">}</FONT><FONT COLOR="#0000ff">else</FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"key"</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#008000">"value"   </FONT><FONT COLOR="#800080">}   </FONT><FONT COLOR="#0000ff">q $$$OK </FONT><FONT COLOR="#000000">}

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT><FONT COLOR="#800080">   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">login</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'Jack' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'Jack' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'Pete' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'Pete' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'John' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'John'   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    IDLogin
    配列を作成した後、テーブルの中身は以下のようになります。
    1Jack
    2Jack
    3Pete
    4Pete
    5John
    6John

    そして、肝心なのはここです! 次のクエリを実行してみましょう。

    select * from demo.test where for some %element(Login) (%value '111')
    IDLogin
    実行結果は以下のようになります。
    3Pete
    4Pete

    結果、一部のデータだけがテーブルに表示されています。また、インデックスでは非表示になったデータがいくつかありますが、それでも検索はできます。 これはどのように使えばいいのでしょう?

    例えば、特定のユーザーがアクセスできる一連のパスワードを、インデックスから (1 つだけではなく) すべて非表示にすることができます (通常 1 つだけ非表示にすることはできません)。 また、SQL を使って開かれては困るという機密情報があれば、それをすべて非表示にすることもできます。 もちろん、これは、GRANT column-privilege を実行するなど、他の方法でも行えます。 しかし、その場合は、protected フィールドにアクセスするためのストアドプロシージャを使う必要があります。


    機密情報を隠す(続き)


    私たちのテーブルのデータとインデックスを含むグローバルを見れば、「5.4」や「fg」といったキーの値が表示されていないのが分かります。

    ^demo.testD=6
    ^demo.testD(1)=$lb("","Jack")
    ^demo.testD(2)=$lb("","Jack")
    ^demo.testD(3)=$lb("","Pete")
    ^demo.testD(4)=$lb("","Pete")
    ^demo.testD(5)=$lb("","John")
    ^demo.testD(6)=$lb("","John")
    ^demo.testI("iLogin"," 111",3)=""
    ^demo.testI("iLogin"," 111",4)=""
    ^demo.testI("iLogin"," 222",3)=""
    ^demo.testI("iLogin"," 222",4)=""
    ^demo.testI("iLogin"," 333",3)=""
    ^demo.testI("iLogin"," 333",4)=""
    ^demo.testI("iLogin"," TEST1",1)=""
    ^demo.testI("iLogin"," TEST1",2)=""
    ^demo.testI("iLogin"," TEST2",1)=""
    ^demo.testI("iLogin"," TEST2",2)=""
    ^demo.testI("iLogin"," TEST3",1)=""
    ^demo.testI("iLogin"," TEST3",2)=""
    ^demo.testI("iLogin"," VALUE",5)=""
    ^demo.testI("iLogin"," VALUE",6)=""

    そもそも、なぜこれらを定義しているのか?

    その疑問に答えるには、インデックスを少し変更して、テーブルを作成しなおします。

    Index iLogin On (Login(KEYS), Login(ELEMENTS));

    これで、グローバルは見た目が変わります (インデックス付きのグローバルのみ表示しています)。

    ^demo.testI("iLogin"," -"," 111",3)=""
    ^demo.testI("iLogin"," -"," 111",4)=""
    ^demo.testI("iLogin"," 0"," TEST1",1)=""
    ^demo.testI("iLogin"," 0"," TEST1",2)=""
    ^demo.testI("iLogin"," 1"," TEST2",1)=""
    ^demo.testI("iLogin"," 1"," TEST2",2)=""
    ^demo.testI("iLogin"," 2"," TEST3",1)=""
    ^demo.testI("iLogin"," 2"," TEST3",2)=""
    ^demo.testI("iLogin"," 5.4"," 222",3)=""
    ^demo.testI("iLogin"," 5.4"," 222",4)=""
    ^demo.testI("iLogin"," FG"," 333",3)=""
    ^demo.testI("iLogin"," FG"," 333",4)=""
    ^demo.testI("iLogin"," KEY"," VALUE",5)=""
    ^demo.testI("iLogin"," KEY"," VALUE",6)=""

    これで、見事にキーの値と要素の値の両方が格納されています。 これは、今後どのように活用できるのでしょう?

    例えば、先ほどのパスワードを使った例では、パスワードをその有効期限や他の情報と一緒に保存することができます。 クエリでは、次のようにして行います。

    select * from demo.test where for some %element(Login) (%key='-' and %value '111')

    データをどこに保存するかは、あなた次第です。但し、キーは一意ですが、値はそうでないことを覚えておきましょう。

    また、「コレクション」インデックスは、普通のインデックスと同様、追加データを保存するのに使用できます。

    Index iLogin On (Login(KEYS), Login(ELEMENTS)) [ Data = (Login, Login(ELEMENTS)) ];

    この場合、上のクエリはデータにアクセスせずに、インデックスからすべてのデータを取得してくれるので、時間を節約できます。


    日付 (時刻など)


    日付はコレクションにどう関連しているのか、と疑問に思う方がいると思います。 答えはズバリ「ダイレクトに関連している」です。それは、日付や月、年だけによって検索する必要のあることが頻繁にあるためです。 通常の検索では効果はありません。ここで必要なのは、まさに「コレクションベース」の検索なのです。

    ここで、次の例を見てみましょう。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iBirthDay On (BirthDay(KEYS), BirthDay(ELEMENTS));

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">BirthDay </FONT><FONT COLOR="#000080">As %Date</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">BirthDayBuildValueArray(   </FONT><FONT COLOR="#ff00ff">value</FONT><FONT COLOR="#000000">,   </FONT><FONT COLOR="#000080">ByRef </FONT><FONT COLOR="#ff00ff">array</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">As %Status </FONT><FONT COLOR="#000000">{   </FONT><FONT COLOR="#0000ff">i </FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008000">"" </FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(0)=</FONT><FONT COLOR="#800000">value   </FONT><FONT COLOR="#800080">}</FONT><FONT COLOR="#0000ff">else</FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">d</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#0000ff">$zd</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">,3)     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"yy"</FONT><FONT COLOR="#000000">)=+</FONT><FONT COLOR="#0000ff">$p</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">d</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"-"</FONT><FONT COLOR="#000000">,1)     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"mm"</FONT><FONT COLOR="#000000">)=+</FONT><FONT COLOR="#0000ff">$p</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">d</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"-"</FONT><FONT COLOR="#000000">,2)     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"dd"</FONT><FONT COLOR="#000000">)=+</FONT><FONT COLOR="#0000ff">$p</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">d</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"-"</FONT><FONT COLOR="#000000">,3)   </FONT><FONT COLOR="#800080">}   </FONT><FONT COLOR="#0000ff">q $$$OK </FONT><FONT COLOR="#000000">}

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT><FONT COLOR="#800080">   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">birthday</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2000-01-01'</FONT><FONT COLOR="#000000">} </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2000-01-02'</FONT><FONT COLOR="#000000">} </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2000-02-01'</FONT><FONT COLOR="#000000">} </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2001-01-01'</FONT><FONT COLOR="#000000">} </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2001-01-02'</FONT><FONT COLOR="#000000">} </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">{</FONT><FONT COLOR="#000080">d </FONT><FONT COLOR="#008080">'2001-02-01'</FONT><FONT COLOR="#000000">}   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    IDBirthDay
    配列を作成した後、テーブルの中身は以下のようになります。
    101.01.2000
    202.01.2000
    301.02.2000
    401.01.2001
    502.01.2001
    601.02.2001

    これで、簡単に、とてもスピーディに 日付の特定の部分を検索できます。 例えば、以下のようにすれば 2 月生まれの人を全員選択できます。

    select * from demo.test where for some %element(BirthDay) (%key='mm' and %value = 2)
    IDBirthDay
    結果
    301.02.2000
    601.02.2001

    単純なリスト


    Caché DBMS では、単純なリストを対象とした特殊なデータ型 (%List) が備えられています。どの区切り記号を使うかを決められなくて困っている開発者は、文字列の代わりに使えるので便利です。

    このフィールドの使い方は、文字列の使い方にとても似ています。

    それでは、簡単な例を見てみましょう。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iList On List(ELEMENTS);

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">List </FONT><FONT COLOR="#000080">As %List</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">ListBuildValueArray(   </FONT><FONT COLOR="#ff00ff">value</FONT><FONT COLOR="#000000">,   </FONT><FONT COLOR="#000080">ByRef </FONT><FONT COLOR="#ff00ff">array</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">As %Status </FONT><FONT COLOR="#000000">{   </FONT><FONT COLOR="#0000ff">i </FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008000">"" </FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(0)=</FONT><FONT COLOR="#800000">value   </FONT><FONT COLOR="#800080">}</FONT><FONT COLOR="#0000ff">else</FONT><FONT COLOR="#800080">{     </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">=0     </FONT><FONT COLOR="#0000ff">while $listnext</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#800000">item</FONT><FONT COLOR="#000000">)</FONT><FONT COLOR="#800080">{       </FONT><FONT COLOR="#0000ff">s </FONT><FONT COLOR="#800000">array</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">ptr</FONT><FONT COLOR="#000000">)=</FONT><FONT COLOR="#800000">item     </FONT><FONT COLOR="#800080">}   }   </FONT><FONT COLOR="#0000ff">q $$$OK </FONT><FONT COLOR="#000000">}

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT><FONT COLOR="#800080">   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">list</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000080">null union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'c'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a,,'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#000080">null</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#000080">null</FONT><FONT COLOR="#000000">)   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    IDList
    テーブルの中身は、ODBC のディスプレイモードで表示すると以下のようになります。
    1 (null)
    2 a
    3 b,a
    4 b,b
    5 a,c,b
    6 "a,,",,
    注意
     Caché では、論理、ODBC、ディスプレイモード (まとめてデータディスプレイオプション) の 3 種類のデータプレゼンテーションモードがサポートされています。

    ここでは、要素の区切り記号が使われていないため、要素の中では好きな文字を使うことができます。

    ODBC モードで %List 型のフィールドを表示するときは、ODBCDELIMITER パラメーターが区切り記号として使用されます (デフォルトで「,」と同じ)。

    例えば、そのようなフィールドを表示する場合

    Property List As %List(ODBCDELIMITER "^");
    IDList
    を実行すると、テーブルは以下のようになります。
    1 (null)
    2 a
    3 b^a
    4 b^b
    5 a^c^b
    6 a,,^^

    要素の検索は、コンマ区切りの文字列を検索する場合と同じです。

    select * from demo.test where for some %element(List) (%value 'a,,')
    IDList
    6 a,,^^

    %INLIST のオプションは、未だ「コレクション」インデックスを使用できないため、上に紹介した例よりも処理速度が遅くなります。

    select * from demo.test where 'a,,' %inlist List

    コレクション


    それでは、上の例を書き直しましょう。単純なリストの代わりにコレクションのリストを使います。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iListStr On ListStr(ELEMENTS);

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">ListStr </FONT><FONT COLOR="#000080">As list Of %String</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT><FONT COLOR="#800080">   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">liststr</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000080">null union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'c'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'b'</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#808000">$LISTBUILD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008080">'a,,'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#000080">null</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#000080">null</FONT><FONT COLOR="#000000">)   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    この例は、中身はほぼ同じですが、微妙に違う点がいくつかあります。 以下にご注意ください。

    • フィールドの COLLATION 値や array のキーおよびインデックス値は、グローバルに保存される前に適切な形に変換されます。 両方の例のグローバルインデックスの値、特に、NULL 値の表記を見比べてください。
    • BuildValueArray メソッドがないため、要素の値しか使えません (キーは使用できない)。
    • フィールドの型に特殊なコレクションクラス (%ListOfDataTypes) が使われている。

    配列コレクション


    上述のとおり、リストではキーを使用できませんが、 配列を使えば、この欠点を解決できます。

    では、次のクラスを作成しましょう。

    Class demo.test Extends %Persistent
    {
    

    </FONT><FONT COLOR="#000080">Index </FONT><FONT COLOR="#000000">iArrayStr On (ArrayStr(KEYS), ArrayStr(ELEMENTS));

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">str </FONT><FONT COLOR="#000080">As %String</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">Property </FONT><FONT COLOR="#000000">ArrayStr </FONT><FONT COLOR="#000080">As array Of %String</FONT><FONT COLOR="#000000">;

    </FONT><FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">Fill() {   </FONT><FONT COLOR="#0000ff">k </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI</FONT><FONT COLOR="#800080">   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">str</FONT><FONT COLOR="#000000">)   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000080">null union all   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'aaa' </FONT><FONT COLOR="#000080">union all   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'bbb' </FONT><FONT COLOR="#000080">union all   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'bbb' </FONT><FONT COLOR="#000080">union all   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008080">'ccc' </FONT><FONT COLOR="#000080">union all   </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000080">null   </FONT><FONT COLOR="#800080">)   &sql(</FONT><FONT COLOR="#0000ff">insert </FONT><FONT COLOR="#000080">into </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test_ArrayStr</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">element_key</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">arraystr</FONT><FONT COLOR="#000000">)     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">1,</FONT><FONT COLOR="#008080">'0'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test1' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">1,</FONT><FONT COLOR="#008080">'1'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test2' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">1,</FONT><FONT COLOR="#008080">'2'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test3' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">2,</FONT><FONT COLOR="#008080">'0'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test1' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">2,</FONT><FONT COLOR="#008080">'1'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test2' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">2,</FONT><FONT COLOR="#008080">'2'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'test3' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">3,</FONT><FONT COLOR="#008080">'-'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'111' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">3,</FONT><FONT COLOR="#008080">'5.4'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'222' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">3,</FONT><FONT COLOR="#008080">'fg'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'333' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">4,</FONT><FONT COLOR="#008080">'-'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'111' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">4,</FONT><FONT COLOR="#008080">'5.4'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'222' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">4,</FONT><FONT COLOR="#008080">'fg'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'333' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">5,</FONT><FONT COLOR="#008080">'key'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'value' </FONT><FONT COLOR="#000080">union all     </FONT><FONT COLOR="#0000ff">select </FONT><FONT COLOR="#000000">6,</FONT><FONT COLOR="#008080">'key'</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008080">'value'   </FONT><FONT COLOR="#800080">)   </FONT><FONT COLOR="#0000ff">zw </FONT><FONT COLOR="#000000">^demo.testD,^demo.testI } }</FONT>

    説明が必要と思われる点を列挙しておきます。

    • データは、以前と同じように classD の ^name (データ) と classI の ^name (インデックス) という 2 つのグローバルに格納されています。
    • クラスは 1 つですが、テーブルは既に 2 つあります (お馴染みの demo.test と追加でもう 1 つ demo.test_ArrayStr)。
    • demo.test_ArrayStr では、SQL を使って簡単に配列データにアクセスできるほか、以下のフィールドが設けられています。それぞれの名前は一部事前定義されています。 element_key – キーの値 (事前定義されているフィールド名)。 ArrayStr – 要素の値。 test – 親テーブル demo.test へのリンク。 IDtest||element_key 形式のサービスプライマリキー (事前定義されているフィールド名)。
    • フィールドの型には特殊なコレクションクラス (%ArrayOfDataTypes) が使われています。

    以上を踏まえ、Fill() メソッドを実行すると、テーブルの中身は以下のとおりになります。

    IDstr
    demo.test テーブル
    1 (null)
    2 aaa
    3 bbb
    4 bbb
    5 ccc
    6 (null)
    IDtestelement_keyArrayStr
    demo.test_ArrayStr テーブル
    1||010test1
    1||111test2
    1||212test3
    2||020test1
    2||121test2
    2||222test3
    3||5.435.4222
    3||-3-111
    3||fg3fg333
    4||5.445.4222
    4||-4-111
    4||fg4fg333
    5||key5keyvalue
    6||key6keyvalue

    テーブルが 1 つから 2 になった結果、テーブル間で JOIN を使うことを余儀なくされているように思えますが、そうではありません。

    Caché DBMS が提供する SQL のオブジェクト拡張機能について考慮すると、

    demo.test テーブルの str フィールドにある文字列のうち、キーが「-」で要素の値が「111」であるものを表すテストクエリは、以下のようになります。

    <FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008000">test ID</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">test</FONT><FONT COLOR="#000000">-></FONT><FONT COLOR="#008000">str </FONT><FONT COLOR="#000080">from </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test_ArrayStr </FONT><FONT COLOR="#000080">where </FONT><FONT COLOR="#008000">element_key</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008080">'-' </FONT><FONT COLOR="#000000">and </FONT><FONT COLOR="#008000">arraystr</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008080">'111'</FONT>
    もしくはこちら
    <FONT COLOR="#0000ff">select </FONT><FONT COLOR="#008000">%ID</FONT><FONT COLOR="#000000">, </FONT><FONT COLOR="#008000">str </FONT><FONT COLOR="#000080">from </FONT><FONT COLOR="#008000">demo</FONT><FONT COLOR="#000000">.</FONT><FONT COLOR="#008000">test </FONT><FONT COLOR="#000080">where </FONT><FONT COLOR="#008000">test_ArrayStr</FONT><FONT COLOR="#000000">-></FONT><FONT COLOR="#008000">element_key</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008080">'-' </FONT><FONT COLOR="#000000">and </FONT><FONT COLOR="#008000">test_ArrayStr</FONT><FONT COLOR="#000000">-></FONT><FONT COLOR="#008000">arraystr</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#008080">'111'</FONT>
    IDstr
    結果
    3bbb
    4bbb

    ご覧のとおり、ここは至って単純で、JOIN も使われていません。これは、すべてのデータが 1 つのグローバルに格納され、Caché がこれらのテーブルの「関係」を把握しているためです。

    これらのフィールドを両方のテーブルから参照できるのは、まさにこれが理由です。 実際、demo.test テーブルに test_ArrayStr フィールドはありませんが、それを使って関連するテーブルにアクセスすることができます。


    結論


    この記事で説明したインデックス作成メカニズムは、一部のシステムクラスで広範に使用されています。それには、テキストストリームのインデックスを作成し、SQL による検索を可能にする %Stream.GlobalCharacterSearchable クラスなどが含まれます。 クラスコレクションのインデックス作成に関するトピックは、非常に多岐にわたるため、意図的に割愛しています (組み込み、格納、ストリーム、ユーザー定義、コレクションのコレクションなど)。 また、SQL による操作が不便な場合もあります。これを踏まえ、著者はそのようなコレクションが必要になることは、一部の極めて稀な状況を除き、ほぼないであろうと判断しました。 フルテキスト検索もまた別のトピックであり、SQL を使う場合には独自のインデックスと操作法を伴うものであるため、本記事ではカバーしておりません。 最後に、著者は、SqlListTypeSqlListDelimiter といったプロパティパラメーターの使用例も省いておりますが、好奇心旺盛の方は実際に試す方法を見い出していただけると思います。

    役に立つリンク


    これは、こちらの記事を英訳したものです。 [@Evgeny Shvarov]、翻訳作業にご協力いただきありがとうございました。

    この記事は、Habrahabr でもお読みいただけます。

    1
    0 170
    記事 Toshihiko Minamoto · 3月 31, 2021 13m read

    デベロッパーの方なら、反復的なコードを書いた経験があると思います。 プログラムを使ってコードを生成できたら楽なのに、と考えたことがあるかもしれません。 まさに自分のことだと思った方、ぜひこの記事をお読みください!

    まずは例をお見せします。 注意: 次の例で使用する %DynamicObject インターフェースは Caché 2016.2 以上のバージョンが必要です。 このクラスに馴染みのない方は、Using JSON in Caché と題したドキュメンテーションをお読みください。 とても重宝すると思います!

    データを保管するために使う %Persistent というクラスがあります。 %DynamicObject インターフェースを使い、データを JSON 形式で取り込むとしましょう。 どうすれば %DynamicObject 構造をクラスにマッピングできると思いますか? ソリューションの 1 つに、値を直接コピーするコードを書くという方法があります。

    Class Test.Generator Extends %Persistent 
    {
    Property SomeProperty As %String;
    
    Property OtherProperty As %String;
    
    ClassMethod FromDynamicObject(dynobj As %DynamicObject) As Test.Generator
    {
        set obj = ..%New()
        set obj.SomeProperty = dynobj.SomeProperty
        set obj.OtherProperty = dynobj.OtherProperty
        quit obj
    }
    }
    

    しかし、プロパティの数が多かったり、このパターンを複数のクラスに使ったりすると、少し面倒なことになります (もちろん管理も大変です)。 それを解決するのがメソッドジェネレータです! 簡単に言うと、メソッドジェネレータを使うときは、特定のメソッドのコードを書く代わりに、クラスのコンパイラが実行するコードを書き、それによりメソッドのコードを生成します。 少しややこしいでしょうか? いたって単純なんですよ。 では、例を一つお見せしましょう。

    Class Test.Generator Extends %Persistent
    {
    ClassMethod Test() As %String [ CodeMode = objectgenerator ]
    {
        do %code.WriteLine(" write ""This is a method Generator!"",!")
        do %code.WriteLine(" quit ""Done!""")
    
        quit $$$OK
    }
    }
    

    CodeMode = objectgenerator というパラメーターを使い、現在のメソッドはメソッドジェネレータであり、普通のメソッドではないことを示しています。 このメソッドの働きですが、 メソッドジェネレータをデバッグするには、クラスの生成されたコードを見ると便利です。 今回の例で言うと、Test.Generator.1.INT と名付けた INT ルーチンがそれに当たります。 これを開くには、Studio で「Ctrl+Shift+V」と入力してもいいですし、Studio の「Open」ダイアログまたは Atelier から開くこともできます。

    INT コードを見ると、このメソッドが実装されているのが分かります。

    zTest() public {
     write "This is a method Generator!",!
     quit "Done!" }
    

    見てお分かりの通り、この実装は %code オブジェクトに書き込まれるテキストを含む単純なものです。 %code は、特殊なタイプのストリームオブジェクトです(%Stream.MethodGenerator)。 このストリームに書き込まれるコードには、マクロやプリプロセッサディレクティブ、埋め込まれた SQL など、MAC ルーチンで有効なコードであれば、何でも含めることができます。 メソッドジェネレータを使用するにあたり、いくつか頭に入れておきたいことがあります。

    • メソッドシグネチャは、生成するターゲットメソッドに適用される。 ジェネレータのコードは、常に成功またはエラー状況を示すステータスコードを返すものである。

    • %code に書き込まれるコードは有効な ObjectScript でなければいけない (他の言語モードを持つメソッドジェネレータは本記事の範囲外です)。 つまり、特に重要なこととして、コマンドを含む行はスペースから始めなければいけません。 例にある WriteLine() の呼び出しは、2 つともスペースで始まっています。

    %code の変数 (生成されたメソッド) 以外にも、コンパイラは現在のクラスのメタデータを以下の変数が使用できます。

    • %class
    • %method
    • %compiledclass
    • %compiledmethod
    • %parameter

    最初の 4 つは、それぞれ %Dictionary.ClassDefinition%Dictionary.MethodDefinition%Dictionary.CompiledClass%Dictionary.CompiledMethod のインスタンスです。 %parameter は、クラスで定義されたパラメータ名とキーで構成される添え字付き配列です。

    (今回の目的において) %class%compiledclass の主な違いは、%class には現在のクラスで定義されているクラスメンバー (プロパティやメソッドなど) のメタデータだけが含まれている点です。 一方の %compiledclass には、これらのメンバー以外にも、継承されたすべてのメンバーのメタデータが含まれます。 また、%class から参照される型の情報は、クラスコードで指定されている通りに表示される一方で、%compiledclass (および %compiledmethod) の型は完全なクラス名に展開されます。 例えば、%String%Library.String に展開され、パッケージが指定されていないクラス名は Package.Class のように完全なクラス名に展開されます。 詳細は、これらのクラスのクラスリファレンスをご覧ください。

    この情報を使えば、%DynamicObject 用にメソッドジェネレータを構築することができます。

    ClassMethod FromDynamicObject(dynobj As %DynamicObject) As Test.Generator [ CodeMode = objectgenerator ]
    {
        do %code.WriteLine(" set obj = ..%New()")
        for i=1:1:%class.Properties.Count() {
            set prop = %class.Properties.GetAt(i)
            do %code.WriteLine(" if dynobj.%IsDefined("""_prop.Name_""") {")
            do %code.WriteLine("   set obj."_prop.Name_" = dynobj."_prop.Name)
            do %code.WriteLine(" }")
        }
    
        do %code.WriteLine(" quit obj")
        quit $$$OK
    }
    

    これにより、以下のコードが生成されます。

    zFromDynamicObject(dynobj) public {
     set obj = ..%New()
     if dynobj.%IsDefined("OtherProperty") {
       set obj.OtherProperty = dynobj.OtherProperty
     }
     if dynobj.%IsDefined("SomeProperty") {
       set obj.SomeProperty = dynobj.SomeProperty
     }
     quit obj }
    

    ご覧のとおり、このクラスで定義されている各プロパティを set するコードが生成されます。 この実装では、継承されたプロパティを除外していますが、%class.Properties の代わりに %compiledclass.Properties を使えば簡単に含めることができます。 また、プロパティを set しようと試みる前に、%DynamicObject にプロパティが存在するかどうかをチェックするコードも追加しました。 存在しないプロパティを %DynamicObject から参照してもエラーは出ないので絶対に必要な訳ではありませんが、クラス内のプロパティのいずれかがデフォルト値を定義している場合は便利です。 このチェックを行わなければ、デフォルト値はいつもこのメソッドによって上書きされます。

    メソッドジェネレータは継承と組み合わせて使うと大きな威力を発揮します。 FromDynamicObject() メソッドジェネレータは、抽象クラスに置くことができます。 なお、%DynamicObject から逆シリアル化できる新しいクラスを作成するのであれば、このクラスを拡張してこの機能を有効化するだけで OK です。 クラスのコンパイラは、各サブクラスをコンパイルするときに、メソッドジェネレータのコードを実行し、そのクラスの実装をカスタマイズします。

    メソッドジェネレータのデバッグ

    基本的なデバッグ作業

    メソッドジェネレータを使用すると、プログラミングの間接参照のレベルが増えてしまいます。 これにより、ジェネレータのコードをデバッグする際に問題が起こる場合があります。 それでは、1 つ例を見てみましょう。 次のメソッドをご覧ください。

    Method PrintObject() As %Status [ CodeMode = objectgenerator ]
    {
        if (%class.Properties.Count()=0)&&($get(%parameter("DISPLAYEMPTY"),0)) {
            do %code.WriteLine(" write ""{}"",!")
        } elseif %class.Properties.Count()=1 {
            set pname = %class.Properties.GetAt(1).Name
            do %code.WriteLine(" write ""{ "_pname_": ""_.."_pname_"_""}"",!")
        } elseif %class.Properties.Count()>1 {
            do %code.WriteLine(" write ""{"",!")
            for i=1:1:%class.Properties.Count() {
                set pname = %class.Properties.GetAt(i).Name
                do %code.WriteLine(" write """_pname_": ""_.."_pname_",!")
            }
            do %code.WriteLine(" write ""}""")
        }
    
        do %code.WriteLine(" quit $$$OK")
        quit $$$OK
    }
    

    これは、オブジェクトの中身を出力するだけの単純なメソッドです。 オブジェクトは、プロパティの数によって異なる形式で出力されます。具体的には、複数のプロパティを持つオブジェクトは複数の行に渡って出力され、プロパティを持たない、または 1 つしか持たないオブジェクトは 1 つの行に出力されます。 また、オブジェクトは DISPLAYEMTPY というパラメーターを導入しています。これは、プロパティを持たないオブジェクトの出力を抑制するかしないかを制御するものです。 しかし、このコードには問題点があります。 プロパティを持たないクラスでは、オブジェクトが正しく出力されていません。

    TEST>set obj=##class(Test.Generator).%New()
    
    TEST>do obj.PrintObject()
    
    TEST>
    

    ここでは、何も出力されないのではなく、空のオブジェクト "{}" が出力されるはずなのです。 これをデバッグするに、INT コードの中身を確認します。 ところが、INT コードを開いてみると、なんと zPrintObject() の定義が見当たらないのです! 私の言うことを鵜呑みにせず、コードをコンパイルしてご自身の目でお確かめください。 どうぞ... 終わるまでお待ちします。

    はい、 終わりましたでしょうか? 何か分かりましたか? 鋭い方なら、1 つ目の問題の原因が分かったのではないでしょうか。そうです、IF 文の最初の節に入力ミスがあります。DISPLAYEMPTY パラメーターのデフォルト値は 0 ではなく、1 でなければいけません。 正しくは、$get(%parameter("DISPLAYEMPTY"),1)$get(%parameter("DISPLAYEMPTY"),0) は間違いです。 これで原因がはっきりしましたね。 でも、どうして INT コードにメソッドがなかったのでしょう? でも、実行はできましたよね。 <METHOD DOES NOT EXIST> エラーは出なかったし。メソッドは全く何もしなかったのです。 ミスが解明したところで、このメソッドが INT コードにあればどうようなコードに_なっていたか_を見てみましょう。 if ... else if ... コンストラクトの条件を1つも満たしていないので、コードは単純に以下のようなります。

    zPrintObject() public {
        quit 1 }
    

    このコードは、リテラル値を返す以外には、何もしないことに注目してください。 Caché のクラスのコンパイラは非常に賢いことが分かりました。 特定の状況では、メソッドのコードを実行する必要がないことに気付き、INT コードをメソッドに合わせて最適化できるのです。 これは紛れもなく素晴らしい最適化機能です。なぜなら、主にシンプルなメソッドの場合は、カーネルから INT コードにディスパッチすると膨大なオーバーヘッドが生じるからです。

    この動作は、メソッドジェネレータ固有のものではないことに注意してください。 次のメソッドをコンパイルしてから、INT コードの中で探してみてください。

    ClassMethod OptimizationTest() As %Integer
    {
        quit 10
    }
    

    メソッドジェネレータのコードをデバッグするときは、INT コードを確認すると非常に便利です。 ジェネレータによって実際に作成されたものを確認できます。 但し、生成されたコードが INT コードに表示されない場合があるので、注意が必要です。 そういった予想外の事象が発生する場合は、ジェネレータのコードにバグがあり、ジェネレータが有意義なコードを生成できない原因となっていることが考えられます。

    デバッガーの使用について

    先ほど説明しましたが、生成されたコードに問題がある場合は、INT コードを見れば確認できます。 また、ZBREAK や Studio のデバッガーを使って、メソッドをデバッグすることもできます。 メソッドジェネレータのコードそのものをデバッグする方法はないだろうか、と気になっている方もいるのではないでしょうか。 もちろん、いつでもメソッドジェネレータに「write」式を追加したり、caveman のようなデバッググローバルを設定したりできます。 でも、もっといい方法があるはずだと思いませんか?

    そうです、あるんです。しかし、その方法を理解するには、まずクラスのコンパイラーが機能する仕組みを理解する必要があります。 大まかに説明すると、クラスのコンパイラーは、クラスをコンパイルするとき、まず最初にクラスの定義を解析して、そのクラス用にメタデータを生成します。 基本的には、先ほど説明した %class 変数と %compiledclass 変数用にデータを生成していることになります。 次に、すべてのメソッドに対し INT コードを生成します。 この段階で、すべてのメソッドジェネレータの生成コードを格納する個別のルーチンを作成します。 このルーチンは、<classname>.G1.INT と呼ばれています。 そして、*.G1 ルーチンのコードを実行してメソッドのコードを生成し、そのコードをクラスの残りのメソッドと一緒に <classname>.1.INT ルーチンに保管します。 そして、このルーチンをコンパイルすると、 コンパイルされたクラスが作成されます! もちろん、これは非常に複雑なソフトウェアを極端に単純化したものですが、この記事の目的を果たすには十分です。

    この *.G1 ルーチンは面白そうですね。 ではその中身を見てみましょう!

        ;Test.Generator3.G1
        ;(C)InterSystems, method generator for class Test.Generator3.  Do NOT edit.
        Quit
        ;
    FromDynamicObject(%class,%code,%method,%compiledclass,%compiledmethod,%parameter) public {
        do %code.WriteLine(" set obj = ..%New()")
        for i=1:1:%class.Properties.Count() {
            set prop = %class.Properties.GetAt(i)
            do %code.WriteLine(" if dynobj.%IsDefined("""_prop.Name_""") {")
            do %code.WriteLine("   set obj."_prop.Name_" = dynobj."_prop.Name)
            do %code.WriteLine(" }")
        }
        do %code.WriteLine(" quit obj")
        quit 1
     Quit 1 }
    

    クラスの INT コードを編集して、デバッグコードを追加するということに慣れている方もいるのではないでしょうか。 少しやり方が粗いですが、通常ならそれでも構いません。 しかし、この場合はそれだとうまく行きません。 このコードを実行するには、クラスをコンパイルし直す必要があります。 (結局はクラスのコンパイラに呼び出されます。) しかし、クラスをまたコンパイルすると、このルーチンが再生成されるので、加えた変更がすべて消去されてしまいます。 幸い、ZBreak か Studio のデバッガーを使えば、このコードを細かく確認できます。 ルーチン名が分かっているので、ZBreak の使い方はいたって単純です。

    TEST>zbreak FromDynamicObject^Test.Generator.G1
    
    TEST>do $system.OBJ.Compile("Test.Generator","ck")
    
    Compilation started on 11/14/2016 17:13:59 with qualifiers 'ck'
    Compiling class Test.Generator
    FromDynamicObject(%class,%code,%method,%compiledclass,%compiledmethod,%parameter) publ
                ^
    ic {
    <BREAK>FromDynamicObject^Test.Generator.G1
    TEST 21e1>write %class.Name
    Test.Generator
    TEST 21e1>
    

    Studio のデバッガーの使い方も簡単です。 *.G1.MAC ルーチンにブレークポイントを設定し、$System.OBJ.Compile() をクラスに対して呼び出すようにデバッグターゲットを設定できます。

    $System.OBJ.Compile("Test.Generator","ck")
    

    これでデバッグ作業が開始されます。

    結論

    この記事では、メソッドジェネレータについて簡単にまとめました。 詳細にご興味のある方は、以下のドキュメンテーションをお読みください。

    0
    0 267
    記事 Mihoko Iijima · 3月 9, 2021 1m read

    これは InterSystems FAQ サイトの記事です。

    ダイナミックオブジェクトから JSON 文字列を生成するときに使用する %ToJSON() の引数にストリームオブジェクトを指定することでエラーを回避できます。

    コード例は以下の通りです。

    USER>set temp=##class(%Stream.TmpCharacter).%New()
    
    USER>set jsonobj={}
    
    USER>set jsonobj.pro1=["a","b","c","d"]
    
    USER>set jsonobj.pro2=["あ","い","う","え"]
    
    USER>do jsonobj.%ToJSON(temp)
    
    USER>write temp.Size
    51
    USER>write temp.Read()
    {"pro1":["a","b","c","d"],"pro2":["あ","い","う","え"]}

     

    詳細はドキュメントもご参照下さい。

    【IRIS】大きいダイナミック・エンティティからストリームへのシリアル化

    大きいダイナミック・エンティティからストリームへのシリアル化

    3
    0 544
    記事 Mihoko Iijima · 3月 5, 2021 1m read

    これは InterSystems FAQ サイトの記事です。

    永続クラス(=テーブル)定義に提供される %BuildIndices() メソッドの引数に、インデックスを再構築したい ID の開始値と終了値を指定することにより、その範囲内のインデックスのみが再構築できます。
     

    例えば、Sample.Person クラスにある NameIDX インデックスと ZipCode インデックスを ID=10~20 のみ再構築する場合は、以下のように実行します(ID の範囲は、第5引数、第6引数に指定してます)。

     set status = ##class(Sample.Person).%BuildIndices($LB("NameIDX","ZipCode"),1,,1,10,20) 

     

    $LB() は $ListBuild() 関数で、%BuildIndices() メソッドでは、インデックス名を指定するために使用しています。

    インデックスの再構築方法については、ドキュメントもご参照ください。

    2018.1 以下はこちらのドキュメントをご参照ください。

    0
    0 454
    記事 Toshihiko Minamoto · 3月 4, 2021 10m read

    マッピングの例

    三連載で 4 記事目を書いてしまったら、これまでのハイライトとして 5 記事目を書かないわけにはいかないでしょう!

    注意:  何年か前に Dan Shusman 氏が私に「グローパルのマッピングは芸術だ」と言いました。  そのやり方に正解も不正解もありません。  どのようなマッピングを行うかは、データをどう解釈するかで決まります。  例のごとく、最終的な結論を出す方法は 1 つに限られません。  ここでご紹介する例の中には、同じ型のデータを異なる方法でマッピングする例がいくつかあります。

    この記事の最後には、私が長年お客様のために書いてきたマッピングの例をまとめた zip ファイルをご用意しています。  過去 4 つの記事で触れた内容をまとめたハイライトとして、いくつか例を挙げていきたいと思います。  この記事は単なるハイライトですので、過去 4 記事ほどの詳細はカバーいたしません。  不明な点があれば、遠慮なくご連絡ください。もっと詳しく説明させていただきます。

    Row ID Spec: クラス例:  Mapping.RowIdSpec.xml

    これについては、過去の記事で何度か断言していますが、 これを定義する必要があるのは、添え字の式が単純なフィールドではない場合に限ります。  ご紹介した例では、添え字に格納された値を 100 で乗算しましたので、グローバルを見ると必要なのは 1.01 ですが、論理値として欲しいのは 101 ということになります。  したがい、Subscript 2 には {ClassNumber}/100 という式があり、RowIdSpec は {L2}*100 となっています。  最初はいつも逆にやってしまいます。  Subscript Expression は論理値を受け取り、それをグローバルの $ORDER() で使うので、100 で除算します。  一方の RowId Spec は、グローバルから値を受け取り、論理値を作成しているので、100 で乗算します。 

    これは、Next Code に加え、乗算と除算を処理する Invalid Condition を書いて行うこともできます。

    Subscript Expression Type: Other / Next Code / Invalid Condition / Data Access: クラス例:  Mapping.TwoNamespacesOneGlobal.xml

    このクラスはまさに便利な機能満載です!  ご紹介したい機能の半分がこのクラスで使用されます。  このクラスは、同じグローバルを 2 つの異なるネームスペースでループします。  これらのネームスペースでは、添え字の同じ値が使用されるので、添え字レベルのマッピングは使用できません。  代わりに、Global Reference の拡張構文 ^|namespace|GlobalName を使用し、最初に USER ネームスペースでグローバルをイテレーションしてから、今度は SAMPLES ネームスペースで同じグローバルをイテレーションします。  このテーブルの IDKey は、Namespace と Sub1 の 2 つの要素で構成されます。

                    Subscript Expression Other: Subscript Level 1 では、$ORDER() や $PIECE() を使わずに、ハードコーディングされた 2 つの値 USER と SAMPLES のどちらかに {L1} を設定します。  ここでは、グローバルが一切使用されていないため、タイプは ‘Other’ になっています。

                    Next Code:  Subscript Expression のタイプ ‘Other’ を使用している場合は、Next Code を指定する必要があります (これを実行するために複雑なコードを書いても構いませんが、どちらにしろコードを提供する必要があります)。  Next Code が最初に呼び出されるとき、{L1} は ‘Start Value’ に設定されます。デフォルトは空の文字列です。  ループが終了したら、Next Code は {L1} を空の文字列に設定します。  この例では、{L1} は 3 回呼び出され、「USER」、「SAMPLES」、「“”」に設定されます。  Subscript Level 2 の Next Code は、Subscript Level 1 が有効な値を 2 つ返した後、「“”」にリセットされます。  この Next Code は、拡張参照付きのグローバルに対し実行されるシンプルな $$ORDER() です。

                    Invalid Condition:  Subscript Level 1 と 2 に Next Code があるということは、両方が Invalid Condition を必要とすることを意味します。   この Subscript Level の値を考慮すると、条件の評価結果として値が無効となれば、1 が返されます。  {L1} については、値が「USER」でも「SAMPLES」でもなければ、1 が返されます。    例えば、 

    SELECT * FROM Mapping.RowIdSpec WHERE NS = “%SYS”

    を実行しても、{L1} の Invalid Condition が評価され、行は 1 つも返されません。

                    Data Access:  グローバルの 2 つのプロパティ (例えば ^glo({sub1},{Sub2})) に基づいた IdKey を持つグローバルがあるとします。{Sub2} に対しループを開始する前に {Sub1} の値を指定している場合は、$DATA(^glo({Sub1}) を実行して前のレベルにデータがないかどうかをチェックします。  この例では、Subscript Level 1 にグローバルがないため、何をテストするのかという指示が必要になります。  Data Access 式は、^|{L1}|Facility となります。  次の例でも Data Access 式が必要になりますが、そちらの方が理解しやすいかもしれません。  これが分かりにくいと思う方は、次の例をご覧ください。

    データアクセス / 行全体の参照: クラス例:  Mapping.TwoNamespacesOneGlobal2.xml

    このクラスは、前のクラスと同じデータをマッピングしています。  違うのは Subscript Level 1 です。  このクラスでは、ネームスペースの値をハードコードする代わりに、2 つ目のグローバルを設け、その中にループする必要があるネームスペース  ^NS(“Claims”,{NS}) を含めています。  これにより、マッピングが簡素化されるほか、クラスのマッピングを変更する代わりにグローバルを設定するだけで、新しいネームスペースを追加できるので柔軟性も増します。

                    データアクセス:   マッピング内の ‘Global’ は ^NS として定義されていますが、それは、Subscript Level 1 と 2 でそのグローバルに対しループを実行するためです。  Subscript Level 3 では、^|{NS}|Facility({Sub1}) に切り替えます。  Next Code で別のグローバルを使用するには、それを ‘Data Access’ で定義する必要があります。  {L1} は ^NS グローバルで制約となっていただけで、^Facility グローバルで使用されてもいないので、単純に整列させています。  {L2} は、グローバルの拡張構文  ^|{L2}|Facility 内でネームスペース参照として使用されています。 このクラスでは、‘Next Code’ は定義されていません (前の例で必要なかったのもこのためです)。  クラスのコンパイラーが ‘Data Access’ を受け取り、それを使ってこのレベルで必要な $ORDER() を生成します。

                   Full Row Reference:  これは、‘Invalid Condition” と似ていて、^|{L2}|Facility({L3}) のように IdKey のすべての部分が使用されている場合に、行を参照する目的で使用されます。  このクラスでは、‘Full Row Reference’ を定義しなくても大丈夫だと思いますが、定義しても損はありません。  グローバルをイテレートする前に添え字の論理値を変更する ‘Next Code’ がある場合は、‘Full Row Reference’ を定義することが必要になります。  例えば、先ほども触れましたが、RowIdSpec クラスには ‘Next Code’ を使うこともできたと思います。  そのアプローチをとっていたら、‘Full Row Reference’ は ^Mapping({L1},{L2}/100) となっていたでしょう。

    Subscript の Access Type 'Global': クラス例:  Mapping.TwoGlobals.xml

    このクラスは、^Member と ^Provider という 2 つの別のグローバルからデータを表示しています。  前の例では、行にアクセスするのに、1 つのグローバルからはじめ、その後に別のグローバルに切り替えています。  今回は、行を含むグローバルが 2 つあり、 1 つ目のグローバルのすべての行をループしてから、もう 1 つのグローバルの行をループします。

                   Subscript Expression Type 'Global': 1 つ目の Subscript Level を 'Global' として定義した場合は、マップの Global Property を「*」に設定する必要があります。  このスタイルのマッピングは、Mapping.TwoNamespacesOneGlobal でも使用できたのではないかと思います。  このようなマッピングを作成するあなたは、まさにアーティストです!

    {L1} の Access Type は ‘Global’、そして ‘Next Code’ は「Membe」、「Provider」、「“”」を返します。  Access Type を ‘Global’ として定義すると、コンパイラーは {L1} をグローバルの名前として使う必要があると分かるため、{L2} と {L3} は単なる添え字ということになります。   {L4} も添え字ではありますが、2 つのグローバルがプロパティを別の場所に格納するというやっかいな事実に対処するコードを持っています。

    このクラスでは、マッピングの Data セクションでもう 1 つ興味深いことが見られます。  ^Member グローバルだけをマッピングしていたなら、Subscript Level は 3 つだけ定義していたでしょう。また、Data セクションの Node 値 は 4、5、6、7、8、および 9 になっていたと思います。  Zip Code が Node 9 または 16 にあることに対処するために、ベースノードを IdKey 4 もしくは 10 に追加し、Zip Code に対するオフセットを取得するために Node 内で +6 を使用します。

    アクセス変数: クラス例: Mapping.SpecialAccessVariable.xml

                   特殊アクセス変数 は、どの Subscript Level でも定義できるほか、 1 つのレベルに複数個設けることができます。  この例では、{3D1} という変数が 1 つ定義されています。  「3」は Subscript Level 3 を意味し、「1」はこのレベルで定義された最初の変数であることを意味ます。  コンパイラーはこのために一意の変数名を生成し、その定義と削除を行います。  変数は、‘Next Code’ を実行した後に定義されます。  この例では、Level 4 の ‘Next Code’ にある変数を使いたいと思ったので、Level 3 で既に定義しておきました。  この例では、無効な日付の値があればそれを「*」に変更し、かつループの同じ場所に戻れるよう、ループのどの要素まで行ったのかを「覚えておくため」に {3D1} を使用しています。

    Bitmaps: クラス例:  Mapping.BitMapExample.xml

    Bitmap インデックスは比較的新しいものですが、だからといって Cache SQL Storage を使うアプリケーションには追加しない方がいいというわけでもありません。  Bitmap インデックスは、単純な正の %Integer IdKey を持つクラスなら、どのクラスにでも追加できます。

    ‘Type’ を「bitmap」として定義し、IdKey は添え字として含めません。  Bitmap を定義するときは、Bitmap Extent が必要なことも覚えておきましょう。  何ら特別なものではありません。Extent は IdKey の値のインデックスなので、添え字は必要ありません。また ‘Type” は「bitmapextent」です。

    このクラスには、Sets や Kills を使ってデータを変更する際に、Bitmap インデックスを管理するために呼び出せるメソッドがあります。  SQL もしくは Objects を使って変更を加えることができるなら、インデックスは自動的に管理されます。

    確かに少しややこしい例も含まれています!  これらのクラスを見て、内容が理解できるでしょうか。  よく解らないという方は、Brendan@interSystems.com までご連絡ください。頑張るアーティストの皆さんのためなら、いつでも喜んでサポートさせていただきます。  スマイル

    クラスの例はこちら

    マッピングに関する他の記事の内容をおさらいしたいという方は、以下のリンクをご利用ください。

    グローバルをクラスにマッピングする技術 (1/3)

    グローバルをクラスにマッピングする技術 (2/3)

    グローバルをクラスにマッピングする技術 (3/3)

    グローバルをクラスにマッピングする技術 (4/3)

    0
    0 201
    記事 Toshihiko Minamoto · 3月 1, 2021 11m read

    グローバルをクラスにマッピングする技術 (4/3)

    三連載のはずが 4 記事目に突入してしまいました。『銀河ヒッチハイク・ガイド』のファンという方はいませんか?

    古くなった MUMPS アプリケーションに新たな生命を吹き込みたいとお考えですか? 以下にご紹介するステップを実行すれば、グローバルをクラスにマッピングし、美しいデータを Object や SQL に公開できます。

    上の内容に馴染みが無い方は、以下の記事を初めからお読みください。

    グローバルをクラスにマッピングする技術 (1/3)

    グローバルをクラスにマッピングする技術 (2/3)

    グローバルをクラスにマッピングする技術 (3/3)

    この記事は Joel、あなたのために書きます!  前回の例で定義した親子関係を土台に、今度は孫クラスを作成し、^ParentChild グローバルに追加された季節情報を処理したいと思います。

    前回と同じ免責事項:  これらの記事を読んでもグローバルがよく理解できないという方は、WRC (Support@InterSystems.com) までメールでお問い合わせください。喜んでサポートさせていただきます。

    グローバルをクラスにマッピングするステップ。

    1. グローバルデータが繰り返し使用されるパターンを特定する。
    2. 固有キーの構成を特定する。
    3. プロパティとそれぞれの型を特定する。
    4. クラス内のプロパティを定義する (変数の添え字をお忘れなく)。
    5. IdKey のインデックスを定義する。
    6. Storage Definition を以下の手順で定義する。
      1. 添え字を IdKey まで (IdKey を含む) 定義する。
      2. Data セクションを定義する。
      3. Row ID セクションには触れない。  デフォルトが 99% の割合で適切なので、これはシステムに任せます。
    </ol>  7. クラス / テーブルをコンパイルし、テストします。
    

    ^ParentChild(1)="Brendan^45956"

    ^ParentChild(1,"Hobbies",1)="Pit Crew"

    ^ParentChild(1,"Hobbies",1,"Seasons")="Fall*Winter"

    ^ParentChild(1,"Hobbies",2)="Kayaking"

    ^ParentChild(1,"Hobbies",2,"Seasons")="Spring*Summer*Fall"

    ^ParentChild(1,"Hobbies",3)="Skiing"

    ^ParentChild(1,"Hobbies",3,"Seasons")="Summer*Winter"

    ^ParentChild(2)="Sharon^46647"

    ^ParentChild(2,"Hobbies",1)="Yoga"

    ^ParentChild(2,"Hobbies",1,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(2,"Hobbies",2)="Scrap booking"

    ^ParentChild(2,"Hobbies",2,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(3)="Kaitlin^56009"

    ^ParentChild(3,"Hobbies",1)="Lighting Design"

    ^ParentChild(3,"Hobbies",1,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(3,"Hobbies",2)="pets"

    ^ParentChild(3,"Hobbies",2,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(4)="Melissa^56894"

    ^ParentChild(4,"Hobbies",1)="Marching Band"

    ^ParentChild(4,"Hobbies",1,"Seasons")="Fall"

    ^ParentChild(4,"Hobbies",2)="Pep Band"

    ^ParentChild(4,"Hobbies",2,"Seasons")="Winter"

    ^ParentChild(4,"Hobbies",3)="Concert Band"

    ^ParentChild(4,"Hobbies",3,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(5)="Robin^57079"

    ^ParentChild(5,"Hobbies",1)="Baking"

    ^ParentChild(5,"Hobbies",1,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(5,"Hobbies",2)="Reading"

    ^ParentChild(5,"Hobbies",2,"Seasons")="Spring*Summer*Fall*Winter"

    ^ParentChild(6)="Kieran^58210"

    ^ParentChild(6,"Hobbies",1)="SUBA"

    ^ParentChild(6,"Hobbies",1,"Seasons")="Summer"

    ^ParentChild(6,"Hobbies",2)="Marching Band"

    ^ParentChild(6,"Hobbies",2,"Seasons")="Fall"

    ^ParentChild(6,"Hobbies",3)="Rock Climbing"

    ^ParentChild(6,"Hobbies",3,"Seasons")="Spring*Summer*Fall"

    ^ParentChild(6,"Hobbies",4)="Ice Climbing"

    ^ParentChild(6,"Hobbies",4,"Seasons")="Winter"

    ステップ 1:

    この新しいクラスも繰り返し使用されるデータを見つけるのは簡単ですね、そう Season サブノードです。  “Spring*Summer*Fall” をすべて同じ行に並べる代わりに、“Spring”、“Summer”、“Fall” という個別の行を 3 つ作成する、という場合に少しややこしくなります。 

    ^ParentChild(1)="Brendan^45956"

    ^ParentChild(1,"Hobbies",1)="Pit Crew"

    ^ParentChild(1,"Hobbies",1,"Seasons")="Fall*Winter"

    ^ParentChild(1,"Hobbies",2)="Kayaking"

    ^ParentChild(1,"Hobbies",2,"Seasons")="Spring*Summer*Fall"

    ^ParentChild(1,"Hobbies",3)="Skiing"

    ^ParentChild(1,"Hobbies",3,"Seasons")="Summer*Winter"

    各趣味 (Hobby) には、季節を最大 4 つ割り当てることができます。  Example3Child クラスにプロパティをあと 4 つ作成できないことはないですが、誰かが新しい季節を勝手に作ってしまったらどうなるでしょう。 そこで、もっと柔軟な解決策として、孫テーブルを作り、季節の数を動的に割り当てられるようにします。

    ステップ 2:

    添え字に注目すれば、IdKey に含まれる部分が分かるので、添え字 1 と 3 は IdKey に含まれるということが分かります。ところが、それぞれの季節を一意に識別するにはもう 1 つ添え字が必要なのですが、使える添え字が残っていません!

    マッピングに入力している情報は、クエリを実行するためのコードを生成するために使用されます。  どのような COS コマンドを使えばこの情報を取得できるのかと考えることで、マッピングを定義しやすくなるかもしれません。  ここで、実行する必要のある重要なコマンドが 3 つあります。

                    SET sub1=$ORDER(^Parentchild(sub1))

                    SET sub2=$ORDER(^Parentchild(sub1,”Hobbies”,sub2))

                    SET season=$PIECE(^ParentChild(sub1,”Hobbies”,sub2”,”Seasons”),”*”,PC)

    マッピングの Subscripts セクションでも同じことを行えます。  Caché SQL Storage は、4 種類の添え字をサポートしています (Piece、Global、Sub、Other)。  ここまでは、デフォルトの Sub を使っています。必需品的な存在として活躍する $ORDER() ループを使えるのもそのおかげです。 

    この例では、Piece オプションをご紹介します。  このレベルで使用するプロパティは、Piece Counter (上の例で PC として表示) として使用されます。  デフォルトの動作として、文字列の最後に達するまでこれを 1 ずつ増加させます。

    ステップ 3:

    3 つのプロパティ:  Data は単純な Season、そして Relationship プロパティ HobbyRef があり、最後に childsub として PieceCount が必要になります。

    ステップ 4:

    Property Season As %String;
    Property PieceCounter As %Integer;
    Relationship HobbyRef As Mapping.Example3Child [ Cardinality = parent, Inverse = Seasons ];

    ステップ 5:

    Subscripts のマッピングを見ると、変数のレベルが 3 つありますが、IdKey のインデックスでは、2 つのプロパティ HobbyRef と PieceCounter だけを参照しています。

    Index Master On (HobbyRef, PieceCounter) [ IdKey ];

    ステップ 6:

    これまで使用してきたお馴染みの 3 つのセクションです。  Row ID は引き続きそのままにしておきます。  このステップでは、Subscripts についてもう少し詳しく説明して、Access Type を定義する必要があります。  このクラスでは、‘Sub’ と ‘Piece’ を使います。  ‘Global’ と ‘Other’ を使った例をご覧になりたい方は、私が例をまとめた zip ファイルをダウンロードしてください。

    ステップ 6a:

    Subscripts のメインページはいつもと同じですが、レベルが 2 つ追加されています。  Parent 参照の 2 つの部分については、先ほど参照した 2 つのクラス {Mapping.Example3Parent} と {Mapping.Example3Child} を参照し直す必要があることを覚えておきましょう。  ウィンドウの左側にある Subscripts Levels の 1 つをクリックすると、違いが表示されます。

    下の画像では、Subscripts Level で行える様々なアクションをご覧いただけます。  ‘Sub’ の Access Type では、「“”」からはじめて、「“”」 に到達するまで $ORDER() を実行しようとお考えではないでしょうか。  そうでない場合は、Start Value または Stop Value またはその両方を指定できます。 

    ‘Data Access’ を使うと、確認する内容を前のレベルから変更できます (イテレーションするグローバルを変更するなど)。 

    ‘Next Code’ と ‘Invalid Conditions’ は一緒に使います。このウィンドウで一番頻繁に使うことになるでしょう。  シンプルな $ORDER() では有効な値を順に取得できないという場合は、代わりに独自のコードを書いてください。 

    ‘Next Code’ は、$ORDER() と同様、1 つの有効な値からその次の有効な値に移動するために使用されます。 

    ‘Invalid Condition’ は特定の値を評価するのに使用されます。  ‘subscriptX’ の値を指定した場合は、それを見つけるためにわざわざ ‘Next’ を呼び出す必要がなくなります。  必要なのは、その値が有効なのかどうかを見極めるコードです。 

    長い間にわたってお約束してきた zip ファイルには、Next Code’ と ‘Invalid Conditions’ を使うクラスがたくさん入っています。 

    ページの最後に登場する ‘Access Variables’ ですが、使うことは滅多にありません。  簡単に言うと、変数を設定し、1 つの Subscript Level で値を割り当て、それを上位の Subscript Level で使用するためのものです。  スコーピングは生成されるテーブルのコードが代わりにやってくれます。

    Subscript Level 5 では、‘Access Type’ は ‘Piece’、‘Delimiter’ は “*” となります。  生成されたコードは、Piece 1 からスタートし、$PIECE の値がなくなるまで 1 ずつ増加していきます。  ここでも、Start Value または Stop Value またはその両方を指定すれば、これを制御できます。

    ステップ 6b:

    Data セクションにはプロパティが 1 つしかないので、‘Piece’ も ‘Delimiter’ も必要ありません。  このテーブルにもっとフィールドがあれば、‘Piece’ と ‘Delimiter’ を指定することになると思いますが、それはそれで問題ありません。

    ステップ 6c:

    まだ空白のままにしておきます。

    ステップ 7:

    すべてが正常にコンパイルします。

    Compilation started on 11/30/2016 08:17:42 with qualifiers 'uk/importselectivity=1 /checkuptodate=expandedonly'
    Compiling 2 classes, using 2 worker jobs
    Compiling class Mapping.Example3Child
    Compiling class Mapping.Example3GrandChild
    Compiling table Mapping.Example3GrandChild
    Compiling table Mapping.Example3Child
    Compiling routine Mapping.Example3Child.1
    Compiling routine Mapping.Example3GrandChild.1
    Compilation finished successfully in 1.021s.

    3 つのテーブルの結合

    SELECT P.ID, P.Name, P.DateOfBirth,

    C.ID, C.Hobby, G.ID, G.Season

    FROM Mapping.Example3Parent P

    JOIN Mapping.Example3Child C ON P.ID = C.ParentRef

    JOIN Mapping.Example3Grandchild G ON C.ID = G.HobbyRef

    WHERE P.Name = 'Kieran'

    この結果、以下が出力されます。

    <td><p  style-"margin-left: 9pt; text-align: center;">
      <strong>Name</strong></p>
    </td>
    
    <td><p  style-"margin-left: 9pt; text-align: center;">
      <strong>DateOfBirth</strong></p>
    </td>
    
    <td style="margin-left: 9pt; text-align: center;">
      <strong>ID</strong></p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      <strong>Hobby</strong></p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      <strong>ID</strong></p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      <strong>Season</strong></p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||1</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      SUBA</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||1||1</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Summer</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||2</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Marching Band</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||2||1</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Fall</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Rock Climbing</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3||1</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Spring</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Rock Climbing</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3||2</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Summer</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Rock Climbing</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||3||3</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Fall</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Kieran</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      05/16/2000</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||4</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Ice Climbing</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      6||4||1</p>
    </td>
    
    <td><p style="margin-left: 9pt; text-align: center;">
      Winter</p>
    </td>
    

    ID

    6

    6

    6

    6

    6

    6

     

    子テーブルの IdKey は、常に Parent 参照と Childsub の 2 つで構成されると説明したことを覚えておいてください。 

    最初の行は、Example3Child ID が 6||1、 Parent 参照が 6、Childsub が 1 となっています。 

    Example3GrandChild の IdKey は、3 つの部分 (6||1||1) で構成されていますが、それでも Parent 参照と Childsub を表しています。  Parent 参照は少し複雑な感じに 6||1 となって 、Childsub は 1 となっています。 

    Example3Child では、Subscripts のプロパティの数が IdKey のプロパティの数に一致しています。 親子構造の入れ子状態が深まると、IdKey は複合化され、添え字の数は増えていきます。

    今回の例で使用した 3 つのクラスはこちらにエクスポートしておきました: MappingExample4.zip。

    0
    0 212
    記事 Mihoko Iijima · 2月 12, 2021 5m read

    これは InterSystems FAQ サイトの記事です。

    XMLファイルの内容を格納する永続クラス定義を作成し、%XML.Adaptor を追加で継承します。

    例は以下の通りです(右端の %XML.Adaptorクラスを追加で継承します)。

    Class ISJ.Class1 Extends (%Persistent, %Populate, %XML.Adaptor)

     

    次に、%XML.Reader クラスを使用して格納先のインスタンスへ、タグとクラスの関連付け(Correlate())を行い、reader.Next() でXMLを取り込みます。

    set sc=reader.OpenFile(filename)
    do reader.Correlate(tag,class)
    while reader.Next(.x,.sc) { do x.%Save() } 

     

    サンプルコードは以下の通りです。

    0
    0 2179
    記事 Toshihiko Minamoto · 1月 11, 2021 4m read

    前回の記事では、データの変更を簡単に記録できる方法をお見せしました。 今回は、監査ログが記録されるデータ構造と監査データを記録する「Audit Abstract クラス」を変更しました。

    また、データ構造は親構造と子構造に変更し、それぞれに「トランザクション」とそのトランザクションで「その値によって変更されたフィールド」を記録するテーブルが 2 つ設けられます。

    新しいデータモデルをご覧ください。

    「監査クラス」から変更したコードをご覧ください。

    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"" { ")
              Do %code.WriteLine($Char(9,9,9)_"Set tAudit = ##class(Sample.Audit).%New()")
              Do %code.WriteLine($Char(9,9,9)_"Set tAudit.Date = +$Horolog")
              Do %code.WriteLine($Char(9,9,9)_"Set tAudit.UserName = tUsername")
              Do %code.WriteLine($Char(9,9,9)_"Set tAudit.ClassName = """_tClassName_"""")
              Do %code.WriteLine($Char(9,9,9)_"Set tAudit.Id = {id}")
              Do %code.WriteLine($Char(9,9,9)_"Set tSC = tAudit.%Save()")
              do %code.WriteLine($Char(9,9,9)_"If $$$ISERR(tSC) $$$ThrowStatus(tSC)")
              Do %code.WriteLine($Char(9,9,9)_"Set tAuditId = tAudit.%Id()")
              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 tAuditField = ##class(Sample.AuditField).%New()")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAuditField.Field = """_tColumnName_"""")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAuditField.OldValue = {"_tProperty.SqlFieldName_"*O}")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tAuditField.NewValue = {"_tProperty.SqlFieldName_"*N}")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Do tAuditField.AuditSetObjectId(tAuditId)")
                                  Do %code.WriteLine($Char(9,9,9,9)_"Set tSC = tAuditField.%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
    }
    }

    Test() クラスメソッドを使ってデータを変更することで、Audit クラス (Sample.Audit) から「parent record」が見えるようになり、「children fields」も「Audit Field」クラスから変更されていることが分かります。 (Sample.AuditField)  

    d ##class(Sample.Person).Test(1)
    INSERT INTO Sample.Person (Name, Age) VALUES ('TEST PARENT-CHILD', '01')
    SQLCODE: 0
    ID Age Name
    1 01 TEST PARENT-CHILD
    1 Rows(s) Affected
    UPDATE Sample.Person SET Name = 'INTERSYSTEMS DEVELOPER COMMUNITY', Age = '100' WHERE Name = 'TEST PARENT-CHILD'
    SQLCODE:0
    ID Age Name
    1 100 INTERSYSTEMS DEVELOPER COMMUNITY
    1 Rows(s) Affected
    <p>
      監査クラス: 
    </p>
    
    <p style="margin-left: 40px;">
      <img alt="" src="/sites/default/files/inline/images/image9.png" />
    </p>
    
    <p style="margin-left: 40px;">
      <img alt="" src="/sites/default/files/inline/images/image10.png" />
    </p>
    
    <p>
      Sample.AuditField の記録には、監査フィールド = 1 を使った Sample.Audit クラスへの参照があります。 以下に示すように、両クラスの関係を使えば、データに対してクエリを実行できます。
    </p>
    
    <p>
      <img alt="" src="/sites/default/files/inline/images/image11.png" />
    </p>
    

     これで完了です。 結果として、最初とは異なるログデータ構造ができました。

    0
    0 158
    お知らせ Mihoko Iijima · 1月 11, 2021

    開発者の皆さん、こんにちは!

    第9回のマルチモデルコンテストの 続報 📣 の「テクノロジーボーナス」についてご紹介します。

    • InterSystems Globals (key-value)
    • InterSystems SQL
    • InterSystems Objects 
    • Your data model
    • ZPM Package deployment
    • Docker container usage

    詳細は以下ご参照ください。<--break->

    InterSystems Globals (key-value) - 2 points

    InterSystems グローバルは、InterSystems IRIS に任意のデータを格納するために使用される多次元スパース配列です。
    各グローバル・ノードはキーとみなされ、値(バリュー)を設定することができます。InterSystems IRIS は、グローバルを管理するための ObjectScript のコマンドや Native API を含む一連の API を提供しています。

    ObjectScript または Native API を介してグローバルを使用すると、2 ポイント獲得できます。

    ツール:

    ドキュメント:

    記事:

    0
    0 125
    記事 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 · 11月 18, 2020 5m read

    クラス、テーブル、グローバルとその仕組み

    InterSystems IRIS を技術的知識を持つ人々に説明する際、私はいつもコアとしてマルチモデル DBMSであることから始めます。

    個人的には、それが(DBMSとして)メインの長所であると考えています。 また、データが格納されるのは一度だけです。 ユーザーは単に使用するアクセス API を選択するだけです。

    • データのサマリをソートしたいですか?SQL を使用してください!
    • 1 つのレコードを手広く操作したいですか?オブジェクトを使用してください!
    • あなたが知っているキーに対して、1 つの値にアクセスしたりセットしたいですか? グローバルを使用してください!

    これは短く簡潔なメッセージで、一見すると素晴らしく聞こえます。しかし、実際には intersystems IRIS を使い始めるたユーザーには クラス、テーブル、グローバルはそれぞれどのように関連しているのだろうか? 互いにどのような存在なのだろうか? データは実際にどのように格納されているのだろうか?といった疑問が生じます。

    この記事では、これらの疑問に答えながら実際の動きを説明するつもりです。

    パート 1. モデルに対する偏見。

    データを処理するユーザーは多くの場合、処理対象のモデルに偏見を持っています。

    開発者はオブジェクトで考えます。 このようなユーザーにとって、データベースとテーブルは CRUD(Create-Read-Update-Delete、ORM の使用が望ましい)を介して操作する箱のようなものですが、その基礎となる概念モデルはオブジェクトです(これは主に私たちのような多くのオブジェクト指向言語の開発者に当てはまります)。

    一方、リレーショナル DBMS に多くの時間を費やしているデータベース管理者は往々にしてデータをテーブルと見なしています。 この場合、オブジェクトはレコードの単なるラッパー扱いです。

    また、InterSystems IRIS では永続クラスはデータをグローバルに格納するテーブルでもあるため、いくつかの説明が必要になります。

    パート 2. 具体例

    次のような Point クラスを作成したとします。

    Class try.Point Extends %Persistent [DDLAllowed]
    {
        Property X;
        Property Y;
    }
    

    次のように DDL/SQL を使用して同じクラスを作成することもできます。

    CREATE Table try.Point (
        X VARCHAR(50),
        Y VARCHAR(50))
    

    コンパイル後、新しいクラスがグローバルにネイティブに格納されているデータをカラム(またはオブジェクト指向のユーザーの場合はプロパティ)にマッピングするストレージ構造を自動生成します。

    Storage Default
    {
    <Data name="PointDefaultData">
        <Value name="1">
            <Value>%%CLASSNAME</Value>
        </Value>
        <Value name="2">
            <Value>X</Value>
        </Value>
        <Value name="3">
            <Value>Y</Value>
        </Value>
    </Data>
    <DataLocation>^try.PointD</DataLocation>
    <DefaultData>PointDefaultData</DefaultData>
    <IdLocation>^try.PointD</IdLocation>
    <IndexLocation>^try.PointI</IndexLocation>
    <StreamLocation>^try.PointS</StreamLocation>
    <Type>%Library.CacheStorage</Type>
    }
    

    ここでは何が起きているのでしょうか?

    下から順番に説明します(太字の単語が重要です。残りは無視してください)。

    • Type - 生成されたストレージタイプ。この場合は永続オブジェクトのデフォルトストレージです。
    • StreamLocation - ストリームを格納するグローバルです。
    • IndexLocation - インデックス用のグローバルです。
    • IdLocation - ID の自動インクリメントカウンターを格納するグローバルです。
    • DefaultData - グローバルの値をカラム/プロパティにマッピングするストレージの XML 要素です。
    • DataLocation - データを格納するグローバルです。

    ここでは「DefaultData」が PointDefaultData となっていますので、その構造をもう少し詳しく見てみましょう。 基本的に、グローバルノードは次の構造を持っていると言われています。

    • 1 - %%CLASSNAME
    • 2 - X
    • 3 - Y

    したがって、グローバルは次のようになると予想されます。

    ^try.PointD(id) = %%CLASSNAME, X, Y
    

    しかし、グローバルを出力すると空になります。ここではデータを追加していなかったためです。

    zw ^try.PointD
    

    オブジェクトを 1 つ追加しましょう。

    set p = ##class(try.Point).%New()
    set p.X = 1
    set p.Y = 2
    write p.%Save()
    

    すると、グローバルはこのようになります。

    zw ^try.PointD
    ^try.PointD=1
    ^try.PointD(1)=$lb("",1,2)
    

    ご覧のように、期待する構造 %%CLASSNAME, X, Y はオブジェクトの X プロパティと Y プロパティに対応する $lb("",1,2) とセットになっています(%%CLASSNAME はシステムプロパティですので無視してください)。

    次のように SQL を使用してレコードを追加することもできます。

    INSERT INTO try.Point (X, Y) VALUES (3,4)
    

    すると、グローバルの内容は次のようになります。

    zw ^try.PointD
    ^try.PointD=2
    ^try.PointD(1)=$lb("",1,2)
    ^try.PointD(2)=$lb("",3,4)
    

    つまり、オブジェクトまたは SQL を介して追加するデータは、ストレージ定義に従ってグローバルに格納されます(補足:PointDefaultData の X と Y を置き換えることでストレージ定義を手動で変更できます。その場合に新しいデータがどうなるかを確認してください!)。

    では、SQL クエリを実行したい場合はどうなるのでしょうか?

    SELECT * FROM try.Point
    

    これは ^try.PointD グローバルを反復処理し、ストレージ定義(正確にはその PointDefaultData 部分)に基づいてカラムにデータを入力する ObjectScript コードに変換されます。

    今度は変更を行います。 テーブルからすべてのデータを削除しましょう。

    DELETE FROM try.Point
    

    すると、この時点でグローバルの内容は次のようになります。

    zw ^try.PointD
    ^try.PointD=2
    

    ここでは ID カウンターのみが残っているため、新しいオブジェクト/レコードの ID は 3 になることに注意してください。 また、クラスとテーブルは引き続き存在します。

    しかし、次を実行するとどうなるでしょうか。

    DROP TABLE try.Point
    

    これはテーブルとクラスを破棄し、グローバルを削除します。

    zw ^try.PointD
    

    皆さんがこの具体例に従い、グローバル、クラス、テーブルがどのように統合され、相互に補完しているかをより深く理解できたことを願っています。 手元の仕事に適切な API を使用すれば、開発がより高速かつアジャイルになり、バグが少なくなります。

    0
    0 557