クリアフィルター
記事
Mihoko Iijima · 2023年6月6日
これは InterSystems FAQ サイトの記事です。
ターミナルでルーチンやクラスのコンパイルを行う際、コンパイル結果が画面に表示されるのでエラーが発生した場合でも確認しやすいですが、一括コンパイルの場合は、大量のコンパイル結果の中にエラー情報が含まれてしまうためエラー情報だけを取得したい場合には少し工夫が必要です。
以下、ルーチン/クラスの一括コンパイル時の結果からエラー情報を取得する方法をご紹介します。
ルーチンの場合
ネームスペースにあるルーチンをターミナルで一括コンパイルするには、%Library.Routine クラスの CompileAll() メソッドを使用します。
以下実行例は、USERネームスペースにあるルーチンを一括コンパイルした結果です。TestRoutine1でコンパイルエラーが発生しています。
USER>do ##class(%Routine).CompileAll()
Compiling in namespace USER at 11:50:47
Routine1.MAC TestRoutine1.MAC
TestRoutine1.MAC - Errors compiling routine
TestRoutine1.INT(3) : Error 8: <NOLINE> : ' do sub3()'
2 routines compiled with 1 errors at 11:50:47 in 00:00:00.030
USER>
大量にルーチンがある場合、出力結果が流れて画面から消えてしまうため、カレントデバイスに出力されている内容をファイル保存し、保存したファイルの中からエラー情報を取得するようにします。
1) コンパイル結果をファイルに保存する
CompileAll() メソッドの第2引数にファイル名をフルパスで指定します。
このメソッドは、第2引数に指定したデバイスがオープンされている場合、そのデバイスにログを書き込みます。
そのため、一旦ファイルを新規書き込みモードでオープンします(OPENコマンドを使用します)。
// ログファイルのフルパスを変数に設定します
set log="C:\temp\result.log"
//ファイルを新規書き込みモードでオープンします
open log:"NWS"
//第2引数にログ出力するファイル名を指定し、一括コンパイルを実行します。
do ##class(%Routine).CompileAll(,log)
//ファイルをクローズします。
close log
2) 1)で作成したファイルからエラー情報を取得する。
ファイルをオープンし、エラー発生時の文字列「Errors compiling routine」が含まれている場合にルーチン名を取り出して変数に設定する例をご紹介します。
//文字列ファイル操作用のインスタンスを生成
set file=##class(%Stream.FileCharacter).%New()
//ファイルとのリンク付け
do file.LinkToFile("c:\temp\result.log")
//ファイルの終わりを検出するまで読み取りながら情報抽出
//ファイルの終わりが検出されるとAtEndプロパティに1が設定される
while file.AtEnd=0 {
set reco=file.ReadLine()
//読み取った行にエラー字の文字列が含まれる場合
if reco["Errors compiling routine" {
//スペース区切りの1番目にルーチン名が含まれているので取得
set rtn=$piece(reco," ",1)
}
//ルーチン名が空だったら次のループへ移動
if $get(rtn)="" {
continue
}
//ローカル変数の添え字にルーチン名をセット
set val(rtn)=""
}
zwrite val
ターミナルで実行する場合
set file=##class(%Stream.FileCharacter).%New()
do file.LinkToFile("c:\temp\result.log")
while file.AtEnd=0 { set reco=file.ReadLine() if reco["Errors compiling routine" { set rtn=$piece(reco," ",1)} if $get(rtn)="" { continue } set val(rtn)=""}
zwrite val
クラスの場合
ネームスペースにあるクラスをターミナルで一括コンパイルするには、%SYSTEM.OBJクラスのCompileAll()メソッドを使用します。
以下実行例は、USERネームスペースにあるクラスを一括コンパイルした結果で、Dummy.ErrorClass1でコンパイルエラーが発生しています。
USER>do $system.OBJ.CompileAll("ck")
04/20/2023 12:17:49 に修飾子 'ck' でコンパイルを開始しました。
エラー #5373: クラス 'Dummy.ErrorClass1:property:XYZ' が使用するクラス '%Library.Strig' は、存在しません
Skip class Dummy.ErrorClass1
, 72 クラスをコンパイル中
クラスのコンパイル中 CookBook.Class1
クラスのコンパイル中 A.b3
クラスのコンパイル中 A.B1
《省略》
ルーチンのコンパイル中 F4.GoldMember.1
クラスのコンパイル中 MyApp.MyService.Test
ルーチンのコンパイル中 MyApp.MyService.Test.1
1.091s のコンパイル中に 1 エラーを検出しました。
第2引数を参照渡しで指定するとエラー情報が配列変数として設定されます。
USER>do $system.OBJ.CompileAll("ck",.log)
USER>zwrite log
log=1
log(1)="エラー #5373: クラス 'Dummy.ErrorClass1:property:XYZ' が使用するクラス '%Library.Strin' は、存在しません"
log(1,"caller")="findalldependencyclasses+149^%occDepend"
log(1,"code")=5373
log(1,"dcode")=5373
log(1,"domain")="%ObjectErrors"
log(1,"namespace")="USER"
log(1,"param")=2
log(1,"param",1)="%Library.Strin"
log(1,"param",2)="Dummy.ErrorClass1:property:XYZ"
log(1,"stack")=$lb("e^findalldependencyclasses+149^%occDepend^2","e^findalldependencyclasses+58^%occDepend^1","e^findalldependencyclasses+8^%occDepend^1","e^IncludeClasses+44^%occCompile^1","e^CompileList+59^%occCompile^1","e^CompileList+23^%apiOBJ^1","e^CompileAll+15^%apiOBJ^1","e^zCompileAll+1^%SYSTEM.OBJ.1^1","d^^^0")
複数エラーが発生した場合は、ログ用に指定した変数直下にエラー個数が設定されます。
エラーメッセージだけを取り出す方法は以下の通りです。
for i=1:1:log { write log(i),! }
マッピングされているクラス・ルーチンのコンパイル方法については、以下開発者コミュニティの記事をご参照ください。
マッピングされたクラス・ルーチンをコンパイルする方法
記事
Hiroshi Sato · 2020年8月18日
IRISでは.Net Bindingは非推奨機能となりました。
.Net Bindingを使ったアプリケーションは、IRISで提供されている.Net Native APIを利用して書き換えることができます。
ここでは、実際に書き換えをおこなったサンプルコードを示しながら、具体的な方法を説明していきます。
CacheDirect(VisM)エミュレーター
OpenExchangeに登録しているVisMエミュレーターは、元々Cachéの.Net Bindingを使用して作成されました。
それをIRISの標準機能で動作可能にするために、.Net Native APIを使用して書き換えをおこないました。
以下にどのように書き換えを行ったかを順を追って説明します。
参照の変更
まず以前の参照を削除します。
Visual Studioのソリューションエクスプローラーの所で参照をクリックします。
表示されるInterSystems.Data.CacheClientを削除します。(右クリックして削除を選ぶ)
次にプロジェクトメニューから参照の追加をクリックして、以下の2つのファイルを選択します。(プロジェクトの.Net Frameworkバージョンに合わせて、それに対応するファイルを選択する以下の例は、v4.5を選択)
c:\InterSystems\IRIS\dev\dotnet\bin\v4.5InterSystems.Data.IRISClient.dll
using句の変更
先頭のusing句の変更が必要になります。
using InterSystems.Data.CacheClient;using InterSystems.Data.CacheTypes;
上記を以下の様に書き換えます。
using InterSystems.Data.IRISClient;using InterSystems.Data.IRISClient.ADO;
connection情報
connectionオブジェクトをCachéからIRISに変更する必要があります。
CacheConnection conn;
public IRISConnection conn = new IRISConnection();
Proxyクラスの削除
.Net Native APIではプロキシークラスは必要なくなるので、その参照を削除します。
(プロジェクトからもUser.CacheDirect.csを削除します。)
public User.CacheDirect cd;
代わりにIRISオブジェクトを宣言します。
public IRISObject cd;
続いてプロキシークラスが保持していたプロパティ機能を実装するために、以下の宣言を追加します。(すべてのプロパティに対してプライベート変数とそれにパブリックアクセスするためのアクセッサメソッド)
private string p0; private string p1; private string p2; private string p3; private string p4; private string p5; private string p6; private string p7; private string p8; private string p9; private string plist; private string pdelim; private string value; private string code; private long execflag; private string errorname; private long error; public string P0 { set { this.p0 = value; } get { return this.p0; } } public string P1 { set { this.p1 = value; } get { return this.p1; } } public string P2 { set { this.p2 = value; } get { return this.p2; } } public string P3 { set { this.p3 = value; } get { return this.p3; } } public string P4 { set { this.p4 = value; } get { return this.p4; } } public string P5 { set { this.p5 = value; } get { return this.p5; } } public string P6 { set { this.p6 = value; } get { return this.p6; } } public string P7 { set { this.p7 = value; } get { return this.p7; } } public string P8 { set { this.p8 = value; } get { return this.p8; } } public string P9 { set { this.p9 = value; } get { return this.p9; } } public string PLIST { set { this.plist = value; } get { return this.plist; } } public string PDELIM { set { this.pdelim = value; } get { return this.pdelim; } } public string Value { set { this.value = value; } get { return this.value; } } public string Code { set { this.code = value; } get { return this.code; } } public long ExecFlag { set { this.execflag = value; if (value == 1) { this.Execute(this.code); } } get { return this.execflag; } } public string ErrorName { get { return this.errorname; } } public string Error { get { return this.error.ToString(); } }
サーバー接続処理
コンストラクターの処理の所で、サーバー接続処理をIRIS用に変更します。
conn = new CacheConnection(); conn.ConnectionString = constr; conn.Open(); cd = new User.CacheDirect(conn);
IRISでは、コネクションオブジェクトを作成した後、プロキシークラスのインスタンスを生成する代わりにIRISクラスのインスタンスを生成し、サーバーのCacheDirect.Emulatorクラスの%Newクラスメソッドを呼び出して、IRISObjectクラスのインスタンスを生成しています。
(.Net Binding版のクラスUser.CacheDirectから名前も変更)このインスタンスが従来のプロキシークラスのインスタンスと同様の機能を提供します。
conn.ConnectionString = constr; conn.Open(); IRIS iris = IRIS.CreateIRIS(conn); cd = (IRISObject)iris.ClassMethodObject("CacheDirect.Emulator", "%New");
プロキシークラスでの実装と異なり、.Net Native APIではプロパティに値を設定するにはIRISObjectクラスのSetメソッドを使って、明示的に値を設定する必要があります。
public long Execute(string command) { long status; cd.Set("P0", p0); cd.Set("P1", p1); cd.Set("P2", p2); cd.Set("P3", p3); cd.Set("P4", p4); cd.Set("P5", p5); cd.Set("P6", p6); cd.Set("P7", p7); cd.Set("P8", p8); cd.Set("P9", p9); cd.Set("PLIST", plist); cd.Set("PDELIM", pdelim)
サーバーのインスタンスメソッド(Execute)を呼び出すためには、IRISObjectクラスのInvokeメソッドを呼び出します。
status = (long)cd.Invoke("Execute", command);
サーバー側のExecuteメソッド実行後に変更された可能性のあるプロパティの値(P0-P9,PLIST,Valueなど)をクライアントのプロパティに反映させるためにIRISOBjectクラスのGetメソッドを呼び出します。
ここでは、サーバー側のプロパティのタイプに関わらず、戻り値によってタイプが動的に変化する可能性があるために戻り値の型をチェックして適切に処理する必要があります。
if (cd.Get("P0") is string) { p0 = (string)cd.Get("P0"); } else { if (cd.Get("P0") is null) { } else { p0 = cd.Get("P0").ToString(); } }
ErrorNameとErrorもサーバー側のプロパティからGetメソッドを使用して取得します。
errorname =(string) cd.Get("ErrorName"); error = (long)cd.Get("Error");
PLISTの処理用に追加したメソッドも同様にサーバー側のPLISTプロパティをGetメソッドで取得します。PLISTプロパティに値を設定するためにSetメソッドを使用します。
string[] PLISTArray = cd.Get("PLIST").ToString().Split(cd.Get("PDELIM").ToString().ToCharArray()); cd.Set("PLIST", string.Join(cd.Get("PDELIM").ToString(), PLISTArray));
記事
Toshihiko Minamoto · 2021年4月22日
## はじめに (および本記事を書いた動機) {#RobustErrorHandlingandCleanupinObjectScript-IntroductionandMotivation}
ObjectScript コードのユニット (ClassMethod など) を実行する場合、そのスコープ外にあるシステムの諸部分と対話するときに適切なクリーンアップを行えないことが原因で、様々な予期せぬ副作用が発生することがあります。 以下にその一部を紹介します。
* トランザクション
* ロック
* I/O デバイス
* SQL のカーソル
* システムフラグと設定
* $Namespace
* 一時ファイル
ObjectScript のこういった重要な機能を、クリーンアップのコーディングや防御的なコーディングを適切に行わずに使用すると、普段は正常に動作しても、予期せぬかたちで、またデバッグが困難なかたちで失敗し得るアプリケーションができてしまう可能性があります。 想定できるすべてのエラーケースにおいてクリーンアップコードが正常に動作することは、極めて重要です。表面的なテストではエラーを見落とす可能性が高いことを考えるとなおさらです。 この記事では、既知の落とし穴をいくつかご紹介し、信頼性の高いエラー処理とクリーンアップを実現するための 2 種類の対処法について説明いたします。
_確実にすべてのエッジケースをテストしたい方は、 私が Open Exchange に掲載している [Test Coverage Tool](https://openexchange.intersystems.com/package/Test-Coverage-Tool) をご覧ください!_
_注記: この記事は私が元々 2018年 6月 に InterSystems 社内で掲載したものです。 開発者コミュニティに投稿しようと思って To-Do リストに加えてから、もう 1 年半になります。 [ありきたりな言い訳で恐縮です。。。](https://natethesnake.com/)_
## 避けたい落とし穴 {#RobustErrorHandlingandCleanupinObjectScript-PitfallstoAvoid}
### トランザクション {#RobustErrorHandlingandCleanupinObjectScript-Transactions}
トランザクションを自然にかつシンプルに処理する方法として、以下のように try/catch ブロックを使い、catch の中で TRollback を使います。
Try {
TSTART
// ... データを処理するコード ...
TCOMMIT
} Catch e {
TROLLBACK
// e.AsStatus(), e.Log(), etc.
}
ここだけを見た場合、エラーが発生したときに TStart と TCommit の間のコードが早々と Quit を出すのではなく、エラーを投げてくれることを考えると、このコードに問題はありません。 しかし、このコードにはリスクがあります。理由は以下の 2 つです。
* もし他のデベロッパーが try ブロックに「Quit」を追加した場合、このトランザクションは開いた状態で放置される。 そのような変更内容は、コードレビューの際につい見落としてしまうことがあるでしょう。現在のコンテキストにトランザクションが含まれていることが明らかでない場合は一層見落としやすくなります。
* このブロックのメソッドが外部のトランザクションの中から呼び出されることがあれば、トランザクションのすべてのレベルが TRollback によりロールバックされてしまう。
より好適なアプローチとしては、トランザクションのレベルをメソッドの初めから追跡し、最後にトランザクションのそのレベルにロールバックします。 下の例をご覧ください。
Set tInitTLevel = $TLevel
Try {
TSTART
// ... データを処理するコード...
// tStatus は例外として投げる必要がないので、次のコードはこれで問題ありません。
If $$$ISERR(tStatus) {
Quit
}
// ... データを処理する他のコード...
TCOMMIT
} Catch e {
// e.AsStatus(), e.Log(), etc.
}
While $TLevel > tInitTLevel {
// トランザクションのレベルを一度に一つずつロールバックします。
TROLLBACK 1
}
### ロック {#RobustErrorHandlingandCleanupinObjectScript-Locks}
インクリメントロックを使用するコードでは、ロックが必要でなくなったら、クリーンアップコードの実行時にロックをデクリメントする必要があります。これをしないと、そのようなロックはプロセスが終了するまで保持されることになります。 ロックがメソッドの外にリークすることはありませんが、そのようなロックを取得することがメソッドの副作用として確認されている場合は除きます。
### I/O デバイス {#RobustErrorHandlingandCleanupinObjectScript-I/ODevices}
同じように、現在の I/O デバイス (特殊変数 $io) の変更もメソッドの外にリークすることはありませんが、メソッドが現在のデバイスを変更することを目的としている場合は除きます (I/O リダイレクトを有効にするなど)。 ファイルを操作するときは、OPEN / USE / READ / CLOSE を使うシーケンシャルファイルのダイレクト I/O よりも、%Stream パッケージを使用することをおすすめします。 これ以外の場合で、I / O デバイスを使用する必要があるときは、メソッドの終わりにデバイスを元のデバイスに戻すよう注意が必要です。 例えば、次のコードにはリスクがあります。
Method ReadFromDevice(pSomeOtherDevice As %String)
{
Open pSomeOtherDevice:10
Use pSomeOtherDevice
Read x
// ... x を使って複雑なことを実行します...
Close pSomeOtherDevice
}
pSomeOtherDevice がクローズする前に例外が投げられることがあれば、$io は pSomeOtherDevice のままとなります。これにより、カスケードエラーが発生する可能性があります。 また、デバイスがクローズされると、$io はプロセスのデフォルトデバイスにリセットされますが、メソッドが呼び出された前の同じデバイスにはリセットされない場合があります。
### SQL のカーソル {#RobustErrorHandlingandCleanupinObjectScript-SQLCursors}
カーソルベースの SQL を使用する場合にエラーが発生したら、カーソルをクローズする必要があります。 カーソルをクローズしなければ、リソースがリークする可能性があります (ドキュメントにその旨の記載あり)。 また、コードを再度実行して、カーソルをオープンしようとすると、「already open」(既にオープン状態) エラー (SQLCODE -101) が発生する場合もあります。
### システムフラグと設定 {#RobustErrorHandlingandCleanupinObjectScript-SystemFlagsandSettings}
アプリケーションコードがプロセスレベルのフラグやシステムレベルのフラグを変更する必要があるということは滅多にありません。例えば、これらの多くは %SYSTEM.Process と %SYSTEM.SQL に定義されています。 その必要があるという場合は、初期値を保存してから、メソッドの終わりに再度保存しなおす必要があるので注意が必要です。
### $Namespace {#RobustErrorHandlingandCleanupinObjectScript-$Namespace}
ネームスペースの変更がメソッドのスコープ外にリークするのを防ぐために、ネームスペースを変更するコードは、常に新しい $Namespace を最初に記述する必要があります。
### 一時ファイル {#RobustErrorHandlingandCleanupinObjectScript-TemporaryFiles}
%Library.File:TempFilename などを使って一時ファイルを作成するアプリケーションコードでは、その一時ファイルが不要になれば、その時点で削除するよう心掛ける必要があります (ちなみに、%Library.File:TempFilename を使うと、主に InterSystems IRIS では、実際にファイルが作成されます)。
## お薦めの対処法: Try-Catch (-Finally) {#RobustErrorHandlingandCleanupinObjectScript-RecommendedPattern:Try-Catch(-Finally)}
多くのプログラミング言語には、try/catch 構造に「finally」ブロックを追加できるという機能が備わっています。「finally」ブロックは、try/catch 文が完了した後に、例外が発生したかどうかに関係なく実行されます。 ObjectScript にこの機能はありませんが、似たようなことができます。 その一般的なパターンは、以下のとおりです。上述した潜在的な多くの問題をご確認いただけます。
ClassMethod MyRobustMethod(pFile As %String = "C:\foo\bar.txt") As %Status
{
Set tSC = $$$OK
Set tInitialTLevel = $TLevel
Set tMyGlobalLocked = 0
Set tDevice = $io
Set tFileOpen = 0
Set tCursorOpen = 0
Set tOldSystemFlagValue = ""
Try {
// グローバルをロックする。但し、5 秒以内にロックを取得できることが条件。
Lock +^MyGlobal(42):5
If '$Test {
$$$ThrowStatus($$$ERROR($$$GeneralError,"Couldn't lock ^MyGlobal(42)."))
}
Set tMyGlobalLocked = 1
// ファイルを開く
Open pFile:"WNS":10
If '$Test {
$$$ThrowStatus($$$ERROR($$$GeneralError,"Couldn't open file "_pFile))
}
Set tFileOpen = 1
// [ カーソル MyCursor を宣言 ]
&;SQL(OPEN MyCursor)
Set tCursorOpen = 1
// このプロセスにシステムフラグを設定する。
Set tOldSystemFlagValue = $System.Process.SetZEOF(1)
// 重要なアクションを実行...
Use tFile
TSTART
// [ ... ここでデータを変更する重要で複雑なコードをたくさん実行... ]
// すべて完了!
TCOMMIT
} Catch e {
Set tSC = e.AsStatus()
}
// Finally {
// クリーンアップ: システムフラグ
If (tOldSystemFlagValue '= "") {
Do $System.Process.SetZEOF(tOldSystemFlagValue)
}
// クリーンアップ: デバイス
If tFileOpen {
Close pFile
// pFile が現在のデバイスなら、CLOSE コマンドは $io をプロセスのデフォルトのデバイスに戻す
// メソッドが呼び出されたときの $io の値とは違う可能性あり。
// 念のために:
Use tDevice
}
// クリーンアップ: ロック
If tMyGlobalLocked {
Lock -^MyGlobal(42)
}
// クリーンアップ: トランザクション
// トランザクションのレベルを最初のレベルまで一つずつロールバックする。
While $TLevel > tInitialTLevel {
TROLLBACK 1
}
// } // "finally" 終了
Quit tSC
}
注意: このアプローチでは、try ... ブロックで「Return」の変わりに「Quit」を使用することが極めて重要です。「Return」だとクリーンアップをバイパスしてしまいます。
## お薦めの対処法: Registered Objects および Destructors {#RobustErrorHandlingandCleanupinObjectScript-RecommendedPattern:RegisteredObjectsandDestructors}
クリーンアップコードは複雑になる場合があります。 そのような場合は、クリーンアップコードを Registered Object の中でカプセル化し、その再利用を促進することが理に適っているかもしれません。 システムの状態は、オブジェクトが初期化されたときや、システムの状態を変化させる (オブジェクトの) メソッドが呼び出された時点から追跡され、そのオブジェクトがスコープから外れたときに、元の値に戻されます。 では、以下のシンプルな例について考えます。ここでは、トランザクション、現在のネームスペース、$System.Process.SetZEOF の状態が管理されています。
/// このクラスのインスタンスがスコープから外れると、そのインスタンスが作成された時点で存在していたネームスペース、トランザクションのレベル、および $System.Process.SetZEOF() の値が復元されます。
Class DC.Demo.ScopeManager Extends %RegisteredObject
{
Property InitialNamespace As %String [ InitialExpression = {$Namespace} ];
Property InitialTransactionLevel As %String [ InitialExpression = {$TLevel} ];
Property ZEOFSetting As %Boolean [ InitialExpression = {$System.Process.SetZEOF()} ];
Method SetZEOF(pValue As %Boolean)
{
Set ..ZEOFSetting = $System.Process.SetZEOF(.pValue)
}
Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
{
Set tSC = $$$OK
Try {
Set $Namespace = ..InitialNamespace
} Catch e {
Set tSC = $$$ADDSC(tSC,e.AsStatus())
}
Try {
Do $System.Process.SetZEOF(..ZEOFSetting)
} Catch e {
Set tSC = $$$ADDSC(tSC,e.AsStatus())
}
Try {
While $TLevel > ..InitialTransactionLevel {
TROLLBACK 1
}
} Catch e {
Set tSC = $$$ADDSC(tSC,e.AsStatus())
}
Quit tSC
}
}
以下のクラスは、今お見せした登録クラスを使って、メソッドの最後で実行するクリーンアップを簡素化する方法を示すものです。
Class DC.Demo.Driver
{
ClassMethod Run()
{
For tArgument = "good","bad" {
Do ..LogState(tArgument,"before")
Do ..DemoRobustMethod(tArgument)
Do ..LogState(tArgument,"after")
}
}
ClassMethod LogState(pArgument As %String, pWhen As %String)
{
Write !,pWhen," calling DemoRobustMethod("_$$$QUOTE(pArgument)_"):"
Write !,$c(9),"$Namespace=",$Namespace
Write !,$c(9),"$TLevel=",$TLevel
Write !,$c(9),"$System.Process.SetZEOF()=",$System.Process.SetZEOF()
}
ClassMethod DemoRobustMethod(pArgument As %String)
{
Set tScopeManager = ##class(DC.Demo.ScopeManager).%New()
Set $Namespace = "%SYS"
TSTART
Do tScopeManager.SetZEOF(1)
If (pArgument = "bad") {
// 通常ならこれは大問題となるところですが、 tScopeManager があるので大丈夫です。
Quit
}
TCOMMIT
}
}
記事
Toshihiko Minamoto · 2021年9月14日
## より産業向けのグローバルストレージスキーム
この連載の第1回では、リレーショナルデータベースにおけるEAV(Entity-Attribute-Value)モデルを取り上げ、テーブルにエンティティ、属性、および値を保存することのメリットとデメリットについて確認しました。 このアプローチには柔軟性という点でメリットがあるにもかかわらず、特にデータの論理構造と物理ストレージの基本的な不一致などによりさまざまな問題が引き起こされるという深刻なデメリットがあります。
こういった問題を解決するために、階層情報の保存向けに最適化されたグローバル変数を、EAVアプローチが通常処理するタスクに使用できるかどうかを確認することにしました。
[パート1](https://jp.community.intersystems.com/node/501181)では、オンラインストア向けのカタログをテーブルを使って作成し、その後で1つのグローバル変数のみで作成しました。 それでは、複数のグローバル変数で同じ構造を実装してみることにしましょう。
最初のグローバル変数`^catalog`には、ディレクトリ構造を保存します。 2つ目のグローバル変数`^good`には、店の商品を保存します。 `^index`グローバルには、店のインデックスを保存します。 プロパティは階層的なカタログに関連付けられているため、プロパティ用の個別のグローバル変数は作成しません。
このアプローチでは、エンティティごとに(プロパティを除く)、個別のグローバル変数を使用しているため、論理の観点では優れています。 グローバルカタログ構造は次のようになります。
.png)
Set ^сatalog(root_id, "Properties", "capacity", "name") = "Capacity, GB"
Set ^сatalog(root_id, "Properties", "capacity", "sort") = 1
Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "name") = "Endurance, TBW"
Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "sort") = 2
Set ^сatalog(root_id, sub1_id, "goods", id_good1) = 1
Set ^сatalog(root_id, sub1_id, "goods", id_good2) = 1
Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "name") = "Rotate speed, ms"
Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "sort") = 3
Set ^сatalog(root_id, sub2_id, "goods", id_good3) = 1
Set ^сatalog(root_id, sub2_id, "goods", id_good4) = 1
商品のグローバル変数は、次のようになります。

Set ^good(id_good, property1) = value1
Set ^good(id_good, property2) = value2
Set ^good(id_good, property3) = value3
Set ^good(id_good, "catalog") = catalog_id
もちろん、商品のあるすべてのカタログセクションで、必要なプロパティで並べ替えを行えるようにインデックスが必要となります。 インデックスグローバルは、次のような構造になります。

Set ^index(id_catalog, property1, id_good) = 1
; To quickly get the full path to concrete sub-catalog
Set ^index("path", id_catalog) = "^catalog(root_id, sub1_id)"
したがって、カタログのすべてのセクションで、リストを並べ替えることができます。 インデックスグローバルはオプションです。 カタログのこのセクションの商品数が多い場合にのみ役立ちます。
## デモデータを操作するためのObjectScriptコード
では、データを操作するために、ObjectScriptを使用しましょう。 まず、特定の商品のプロパティを取得することから始めます。 特定の商品のIDがあり、そのプロパティを並べ替えの値で指定された順序で表示する必要があります。 そのためのコードは次のようになります。
get_sorted_properties(path, boolTable)
{
; remember all the properties in the temporary global
While $QLENGTH(@path) > 0 {
if ($DATA(@path("Properties"))) {
set ln=""
for {
Set ln = $order(@path("Properties", ln))
Quit: ln = ""
IF boolTable & @path("Properties", ln, "table_view") = 1 {
Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
}
ELSE {
Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
}
}
}
}
print_sorted_properties_of_good(id_good)
{
Set id_catalog = ^good(id_good, "catalog")
Set path = ^index("path", id_catalog)
Do get_sorted_properties(path, 0)
set ln =""
for {
Set ln = $order(^tmp(ln))
Quit: ln = ""
Set fn = ""
for {
Set fn = $order(^tmp(ln, fn))
Quit: fn = ""
Write ^tmp(ln, fn), " ", ^good(id_good, fn),!
}
}
}
次に、`id_catalog`に基づいて、カタログセクションの商品を表形式で取得します。
print_goods_table_of_catalog(id_catalog)
{
Set path = ^index("path", id_catalog)
Do get_sorted_properties(path, 1)
set id=""
for {
Set id = $order(@path("goods"), id)
Quit: id = ""
Write id," ", ^good(id, "price"), " "
set ln =""
for {
Set ln = $order(^tmp(ln))
Quit: ln = ""
Set fn = ""
for {
Set fn = $order(^tmp(ln, fn))
Quit: fn = ""
Write ^tmp(ln, fn), " ", ^good(id, fn)
}
Write !
}
}
}
## 可読性: EAV SQLとグローバル変数
では、EAVとSQLの使用をグローバル変数の使用と比較してみましょう。 コードの明確さについては、これが主観的なパラメーターであることは明らかです。 しかし、例として新しい商品の作成方法を見てみましょう。
SQLを使用したEAVアプローチから確認します。 まず、オブジェクトのプロパティリストを取得する必要があります。 これは別のタスクであり、非常に時間がかかります。 `capacity`、`weight`、および`endurance`という3つのプロパティのIDがすでに分かっているとします。
START TRANSACTION
INSERT INTO good (name, price, item_count, catalog_id) VALUES ('F320 3.2TB AIC SSD', 700, 10, 15);
SET @last_id = LAST_INSERT_ID ();
INSERT INTO NumberValues Values(@last_id, @id_capacity, 3200);
INSERT INTO NumberValues Values(@last_id, @id_weight, 0.4);
INSERT INTO NumberValues Values(@last_id, @id_endurance, 29000);
COMMIT
この例ではプロパティが3つしかないため、例にはそれほど圧倒されません。 一般的なケースでは、トランザクション内のテキストテーブルにいくつかの挿入があります。
INSERT INTO TextValues Values(@last_id, @ id_text_prop1, 'Text value of property 1');
INSERT INTO TextValues Values(@last_id, @ id_text_prop2, 'Text value of property 2');
...
INSERT INTO TextValues Values (@last_id, @id_text_propN, 'Text value of property N');
もちろん、数値の代わりに「capacity」を使うというように、IDプロパティの代わりにテキスト表記を使用すれば、SQLバージョンをもう少し簡略することも可能ですが、 SQLの世界では、これは受け入れられません。 エンティティのインスタンスを列挙するには、数値IDを使用するのが慣例です。 このため、インデックス処理が高速化し(インデックス処理のバイトが少なくなるため)、一意性を追跡しやすくなり、新しいIDを自動的に作成しやすくなります。 この場合、挿入フラグメントは次のようになります。
INSERT INTO NumberValues Values(@last_id, 'capacity', 3200);
INSERT INTO NumberValues Values(@last_id, 'weight', 0.4);
INSERT INTO NumberValues Values(@last_id, 'endurance', 29000);
次は、同じ例をグローバル変数を使用した場合のコードです。
TSTART
Set ^good(id, "name") = "F320 3.2TB AIC SSD"
Set ^("price") = 700, ^("item_count") = 10, ^("reserved_count") = 0, ^("catalog") = id_catalog
Set ^("capacity") = 3200, ^("weight") = 0.4, ^("endurance") = 29000
TCOMMIT
では、EAVアプローチで商品を削除してみましょう。
START TRANSACTION
DELETE FROM good WHERE id = @ good_id;
DELETE FROM NumberValues WHERE good_id = @ good_id;
DELETE FROM TextValues WHERE good_id = @ good_id;
COMMIT
そして、グローバル変数でも同じことを行います。
Kill ^good(id_good)
2つのアプローチをコードの長さの観点から比較することもできます。 上記の例からわかるように、グローバル変数を使用した方が、コードは短くなります。 これはメリットです。 コードが短くなるほど、エラーの数も減り、コードを理解して管理するのも容易になります。
一般に、コードが短いほど処理が高速化します。 そして、この場合には、グローバル変数はリレーショナルテーブルよりも低位データ構造であるため、確かにそのとおりです。
## EAVとグローバル変数におけるデータのスケーリング
次に、水平方向のスケーリングを見てみましょう。 EAVアプローチでは、少なくとも3つの最も大きなテーブル(Good、NumberValues、TextValues)を複数のサーバーに分散する必要があります。 エンティティと属性のあるテーブルにはほとんど情報がないため、これらのテーブルは単純にすべてのサーバーに丸ごとコピーすることができます。
各サーバーでは、水平方向のスケーリングにより、さまざまな商品がGood、NumberValues、およびTextValuesテーブルに保存されます。 異なる商品でIDが重複しないように、各サーバーの商品に対して特定のIDブロックを割り当てる必要があります。
グローバルを使って水平方向のスケーリングを行う場合、グローバルでID範囲を構成し、グローバル範囲を各サーバーに割り当てる必要があります。
複雑さは、EAVとグローバルであまり変わりませんが、EAVアプローチの場合は、3つのテーブルにID範囲を構成しなければなりません。 グローバルの場合は、1つのグローバル変数のみにIDを構成するだけで済みます。 つまり、水平方向のスケーリングを調整するには、グローバル変数の方が簡単と言えます。
## EAVとグローバル変数におけるデータ損失
最後に、データベースファイルの破損によるデータ損失のリスクを検討してみましょう。 5つのテーブルか3つのグローバル(インデックスグローバルを含む)のどちらにすべてのデータを保存する方が簡単でしょうか。
3つのグローバルの方が簡単だと思います。 EAVアプローチでは、さまざまな商品のデータがテーブルに混在しているのに対し、グローバルでは情報がより全体的に保存されています。 基盤のブランチは、保存されて順に並べ替えられています。 そのため、データがパスタが絡み合うように保存されるEAVアプローチに比べれば、グローバルの一部の破損によってダメージにつながる可能性は低くなります。
データ回復におけるもう1つの悩みの種は、情報の表示方法です。 EAVアプローチでは、情報は複数のテーブルに分割されているため、1つにまとめるには特別なスクリプトが必要です。 グローバルの場合は、ZWRITEコマンドを使用するだけで、ノードのすべての値と基盤のブランチを表示することができます。
## InterSystems IRISのグローバル: より優れたアプローチ?
EAVアプローチは、階層データを保存するためのトリックとして出現しました。 テーブルは元々、ネストされたデータを保存するようには設計されてはいなかったため、 テーブルでグローバルをエミュレーションするのがEAVの事実上のアプローチです。 テーブルがグローバルよりも高位で低速のデータストレージ構造であることを考えると、EAVアプローチは、グローバルと比較した場合に失敗となります。
個人的な意見を言えば、階層データ構造の場合、グローバルの方がプログラミングの点でより利便性が高く理解しやすいと思います。また、より高速でもあります。
プロジェクトでEAVアプローチを計画している場合は、InterSystems IRISのグローバルを使用して階層データを保存することを検討するようお勧めします。
記事
Mihoko Iijima · 2023年3月23日
開発者の皆さん、こんにちは。
AWSのEC2インスタンス(Ubuntu 20.04を選択)にIRISをインストールした環境を事前に用意した状態からの流れですが、AWS Lambda 関数からPyODBC経由でIRISに接続するまでの流れを試してみました。
Native APIを利用する流れについては、「AWS Lambda の IRIS Python Native API IRIS」をご参照ください。
参考にしたAWSドキュメント:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python.html
以下の流れでご紹介します。
1. レイヤーを作成する
2. Lambda関数を作成する。
3. 1,2の流れをCloudformationで行う例
例で使用しているコード一式はこちらにあります👉https://github.com/Intersystems-jp/iris-pyodbc-lambda
流れに入る前に事前準備についてご紹介します。
事前準備
1) IRISの準備
EC2インスタンスを用意し(例ではUbuntu20.04)IRISをインストールした環境を用意し、IRISのスーパーサーバーポート(1972番)にアクセスできるようにします。
IRISに以下テーブルとデータを用意しておきます(USERネームスペースで作成する例)。
IRISにログインします。
iris session iris
続いて、SQL用シェルに切り替え(:sql と入力するとSQLシェルに切り替えできます)、CREATE文とINSERT文を実行します。
:sql
CREATE TABLE Test.Person (Name VARCHAR(50),Email VARCHAR(50))
INSERT INTO Test.Person (Name,Email) VALUES('山田','taro@mai.com')
INSERT INTO Test.Person (Name,Email) VALUES('武田','takeda@mai.com')
SQLシェルを終了するには quit を入力します。
quit
IRISからログアウトするには、haltコマンドを使用します。
halt
2) Lambda関数の実行ロールの準備
必要であれば、Lambda関数からEC2にアクセスするときのロールを作成しておきます。
《参考にしたページ》https://dev.classmethod.jp/articles/tsnote-lambda-the-provided-execution-role-does-not-have-permissions-to-call-createnetworkinterface-on-ec2/
準備ができたら、さっそくLambda関数を作成してみましょう!
1. レイヤ―を作成する
《参考にしたページ》https://qiita.com/Todate/items/03c5d3911b52b93d39af
レイヤーは、Lambda関数で使用するライブラリとその他依存関係ファイルをZipファイルでアーカイブしたものでコードやデータも含めることができるそうですが、以下の流れでは、IRISの接続に必要なunixODBC用ファイル、pyodbcモジュール用ファイルを含めたレイヤ―作成の流れでご紹介します。
参考にしたAWSドキュメント:Lambda レイヤーの作成と共有
(1) unixODBC用soファイルの用意
(2) IRIS用ドライバのダウンロード
(3) pyodbcのインストール
(4) ODBCデータソース用ファイルの作成
(5) zip作成(レイヤーの作成)
(6) レイヤーの追加
(1) unixODBC用soファイルの用意
unixODBC-2.3.7 の soファイルを入手するため、以下ページを参考にしています。
https://qiita.com/Todate/items/03c5d3911b52b93d39af
以下任意ディレクトリ上で実行します。
curl ftp://ftp.unixodbc.org/pub/unixODBC/unixODBC-2.3.7.tar.gz -O
tar xvzf unixODBC-2.3.7.tar.gz
cd unixODBC-2.3.7
./configure --sysconfdir=/opt --disable-gui --disable-drivers --enable-iconv --with-iconv-char-enc=UTF8 --with-iconv-ucode-enc=UTF16LE --prefix=/opt
make
sudo make install
ここまでで /opt/lib/*.so ができるので、libディレクトリをレイヤー作成用ディレクトリに全コピーします。
以降、レイヤー作成用ディレクトリを ~/pyodbc_lambda として記述します。
sudo cp -r /opt/lib ~/pyodbc_lambda/
(2) IRIS用ドライバのダウンロード
IRIS用ドライバlibirisodbcur6435.soをレイヤー作成用ディレクトリ以下libディレクトリにダウンロードします。
cd ~/pyodbc_lambda/lib
wget https://github.com/Intersystems-jp/IRISModules/raw/master/python/wheel/linux/libirisodbcur6435.so
(3) pyodbcのインストール
lambda関数で使用するPythonモジュールは、レイヤー内pythonディレクトリ以下に配置します。
cd ~/pyodbc_lambda
mkdir python
cd python
pip3 install pyodbc -t .
(4) ODBCデータソース用ファイルの作成
odbc.iniとodbcinst.iniを、レイヤー作成用ディレクトリ以下pythonディレクトリに配置します。
レイヤー用ディレクトリ以下にあるファイルは以下の通りです。
$ tree
.
├── lib
│ ├── libirisodbcur6435.so
│ ├── libodbc.la
│ ├── libodbc.so -> libodbc.so.2.0.0
│ ├── libodbc.so.2 -> libodbc.so.2.0.0
│ ├── libodbc.so.2.0.0
│ ├── libodbccr.la
│ ├── libodbccr.so -> libodbccr.so.2.0.0
│ ├── libodbccr.so.2 -> libodbccr.so.2.0.0
│ ├── libodbccr.so.2.0.0
│ ├── libodbcinst.la
│ ├── libodbcinst.so -> libodbcinst.so.2.0.0
│ ├── libodbcinst.so.2 -> libodbcinst.so.2.0.0
│ ├── libodbcinst.so.2.0.0
│ └── pkgconfig
│ ├── odbc.pc
│ ├── odbccr.pc
│ └── odbcinst.pc
└── python
├── odbc.ini
├── odbcinst.ini
├── pyodbc-4.0.35.dist-info
│ ├── INSTALLER
│ ├── LICENSE.txt
│ ├── METADATA
│ ├── RECORD
│ ├── WHEEL
│ └── top_level.txt
├── pyodbc.cpython-38-x86_64-linux-gnu.so
└── pyodbc.pyi
4 directories, 26 files
(5) zip作成(レイヤーの作成)
レイヤー用ディレクトリで以下実行します。
cd ~/pyodbc_lambda
zip -r9 ../iris_pyodbc_lambda.zip *
ご参考:この手順で作ったZipの例 iris_pyodbc_lambda.zip
(6) レイヤーの追加
AWS Lambdaでレイヤーを追加します。
図例では、レイヤーの動作するアーキテクチャに x86_64 を指定し、ランタイムに Python3.8 を選択しています。
以上でレイヤーの作成が完了です。
次はいよいよ、lambda関数の作成です。
2. Lambda関数を追加する
以下の順序で追加します。
(1) 確認:IRISへの接続情報について
(2) 確認:Lambda関数ハンドラー名について
(3) Lambda関数の追加
(4) 環境変数の追加
(5) ランタイム設定の変更
(6) レイヤーの追加
(7) コード類のアップロード
(8) テスト実行
サンプルのpythonスクリプト:index.pyを使用して登録します。
(1) 確認:IRISへの接続情報について
サンプルのpythonスクリプト:index.pyでは、以下いずれかの方法でIRISに接続できるように記述しています。
環境変数を使用する index.pyには、lambda関数作成時に設定する環境変数を利用するように記述しています(18~22行目) 。なお、環境変数は、Lambda関数登録後、画面で追加/変更できます。
connection.config を使用する index.py の9行目と11~15行目のコメントを外し18~22行目をコメント化して利用します。 接続するIRISの情報に合わせてconnection.configを変更してください。
(2) 確認:Lambda関数ハンドラー名について
サンプルのpythonスクリプト:index.py 6行目に記載の関数 lambda_handler を今回登録するLambda関数ハンドラーとして設定します。
Lambda関数登録時のハンドラー名として、 "ファイルの名称"."Pythonの関数名称" とするルールがあるため、今回登録するハンドラー名は、index.lambda_hander となります。
(3) Lambda関数の追加
AWS Lambdaの関数メニューから登録します。
図例では、関数が動作するアーキテクチャに x86_64 を指定し、ランタイムに Python3.8 を選択しています。
必要であれば、Lambda関数がEC2にアクセスできるようにロールを作成します。
※ロール作成の参考ページ:https://dev.classmethod.jp/articles/tsnote-lambda-the-provided-execution-role-does-not-have-permissions-to-call-createnetworkinterface-on-ec2/
(4) 環境変数の追加
作成した関数の設定タブ:環境変数 で、ODBCSYSINI に ./ を設定します。
この他、IRISへの接続情報に環境変数を利用する場合は以下追加します。
例)
環境変数名
値
IRISHOST
13.231.153.242
IRISPORT
1972
NAMESPACE
USER
USERNAME
SuperUser
PASSWORD
SYS
(5) ランタイム設定の変更
サンプルのpythonスクリプト:index.py を実行時に使用したいので、ハンドラ名をデフォルト名称lambda_function.lambda_handlerから index.lambda_handler に変更します。
コードタブを選択し画面下のほうにある「ランタイム設定」の「編集」をクリックして変更保存します。
(6) レイヤーの追加
レイヤーを作成する の手順で作成したレイヤーをLambda関数に追加します。
コードタブを選択した状態で画面一番下の「レイヤー」から「レイヤーの追加」ボタンで追加します。
(7) コード類のアップロード
以下のファイルをZipファイルに含めてアップロードします。
connection.config (IRISへの接続情報を記載したファイル)
index.py
odbcinst.ini
※IRISへ接続情報を環境変数から取得する場合は、connection.configは不要です。
ご参考:iris_pyodbc_code.zip
コードタブの右端のボタン「アップロード元」をクリックし、Zipを選択してアップロードします。
(8) テスト実行
テストタブを使用します。
新しいイベントを作成します。(何度もテストする場合は保存しておくと便利です)
サンプルは特に引数入力がないので、指定する引数は{}と指定していますが、引数がある場合はJSONできるようです。
接続ができ、SELECT文が実行できると以下のような結果を表示します。
画面上には実行した関数の戻り値の表示(JSON配列)
画面下のほうにスクリプト内で記述したprint()の結果が表示されています。
最後にご紹介するのは、レイヤーとLambda関数追加の流れをCloudformationを利用して自動的に作成する方法です。
メモ:事前に準備したEC2の情報を指定する流れで試しています。
3. 1,2の流れをCloudformationで行う例
サンプル:cloudformation.xmlを使用して「1. レイヤーを作成する 」と「2. Lambda関数を作成する」の流れを自動化します。
「1. レイヤーを作成する」 の流れで作成したZip(例:iris_pyodbc_lambda.zip)と、「2. Lambda関数を作成する」 の流れで作成したZip(例:iris_pyodbc_code.zip)を cloudformation.xml の中で指定します。
S3にZipを配置する
S3のバケットを作成し、作成したZipファイル(例:iris_pyodbc_lambda2.zip)を配置します。
バケット名:iijimas3 にコピーしている例
aws s3 cp iris_pyodbc_lambda.zip s3://iijimas3
aws s3 cp iris_pyodbc_code.zip s3://iijimas3
※ AWS CLI https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html
cloudformationを使ってlambda関数作成
例)cloudformation.yml
ymlでは、lambda関数で使用するレイヤーの作成(リソース名:LambdaLayer)と、
LambdaLayer:
Type: AWS::Lambda::LayerVersion
Properties:
CompatibleArchitectures:
- x86_64
CompatibleRuntimes:
- python3.8
Content:
S3Bucket: iijimas3
S3Key: iris_pyodbc_lambda.zip
Description: "iris python layer"
LayerName: "IRISPyODBCLayer"
lambda関数を作成しています。
IRISPyODBCFunction:
Type: "AWS::Lambda::Function"
Properties:
Environment:
Variables:
IRISHOST: "13.231.153.242"
IRISPORT: "1972"
NAMESPACE: "USER"
USERNAME: "SuperUser"
PASSWORD: "SYS"
ODBCSYSINI: "./"
Code:
S3Bucket: iijimas3
S3Key: iris_pyodbc_code.zip
Description: "IRIS pyodbc Function"
FunctionName: iris-pyodbc
Handler: "index.lambda_handler"
Layers:
- !Ref LambdaLayer
MemorySize: 128
Role: "arn:aws:iam::109671571309:role/lambda_vpc_basic_execution_IRIS"
Runtime: "python3.8"
Timeout: 30
lambda関数の中で使用する環境変数の設定や、
Properties:
Environment:
Variables:
IRISHOST: "13.231.153.242"
IRISPORT: "1972"
NAMESPACE: "USER"
USERNAME: "SuperUser"
PASSWORD: "SYS"
ODBCSYSINI: "./"
関数が使用するレイヤーの指定
Layers:
- !Ref LambdaLayer
ハンドラー名の指定
Handler: "index.lambda_handler"
必要であれば、Lambda関数がEC2にアクセスするときに使用するロール名を指定します。
※ロール作成の参考ページ:https://dev.classmethod.jp/articles/tsnote-lambda-the-provided-execution-role-does-not-have-permissions-to-call-createnetworkinterface-on-ec2/
Role: "arn:aws:iam::109671571309:role/lambda_vpc_basic_execution_IRIS"
※各設定値を適宜変更してください。
後は、cloudformationの画面でスタックを作成し、ymlを実行するだけです。
ymlのアップロード手順は以下の通りです。
https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/
cloudformation.ymlをアップロードし、スタック名を任意に決定します。
この後、2画面表示されますが、すべてデフォルトで「次へ」と「送信」ボタンをクリックします。
正しく作成できるとこの表示になります。
作成したスタックの「リソース」タブをクリックすると、lambda関数へのリンクが表示されます。
テスト実行が成功すると以下のような出力が表示されます。
接続できない場合は、環境変数の値をご確認ください(「設定」タブで確認できます)。
Cloudformation便利ですね
もっと良い方法があるよ!や、こんな風にもできるよ!などありましたら、ぜひ返信欄で教えて下さいよろしくお願いします!
記事
Hiroshi Sato · 2020年7月27日
初めに
Caché ActiveX Bindingは、Visual Basicでクライアント・サーバー型のアプリケーション開発を支援するためにInterSystemsが提供してきたツールです。
CacheActiveX.dllとCacheObject.dllの2種類のバージョンが存在します。
IRISでは、CacheActiveX.dllは動作可能です。
CacheObject.dllはサポートしていません。
いずれにしろ誕生から既に20年以上が経過した非常に古いテクノロジーでマイクロソフト社も非推奨の古い規格ですので、今後も使い続けるのは得策ではありません。
Caché ActiveX Bindingの機能はIRISに用意されている.Net Native APIと.Net Managed Providerの機能を使って書き換え可能です。
ここでは、Caché ActiveX Bindingを使って書かれていたサンプルアプリケーションをIRISで動作するように移植した作業内容について解説します。
このサンプルは、以下のgithubサイトから入手可能です。
ADBKサンプル
ADBKアプリケーション
このサンプルアプリケーションは、20年以上も前にVB6サンプルとして作成されました。
VB6プロジェクトを.Netプロジェクトに変換
この作業は、VisM.OCXを利用したアプリケーションをIRISに移行する方法という記事にも同じ内容が記載されていますので、そちらをご参考ください。VisM.OCXを利用したアプリケーションをIRISに移行する方法
CacheObject.dllの参照
Visual Studioを起動すると右側に表示されるソリューションエクスプローラーから参照の部分を開きます。
CacheObjectというのが見えるので、それを選択して、右クリックのメニューから削除をクリックします。
アプリケーション修正
それでは、IRISで動作するようにサンプルアプリケーションを修正していきましょう。
参照の追加
IRIS Native APIと.Net Managed Providerを利用するためにIRISの.Netライブラリーの参照を追加します。
プロジェクトメニューから参照の追加をクリックして参照ボタンを押して以下のファイルを選択します。
c:\intersystems\IRIS\dev\dotnet\bin\v4.5
InterSystems.Data.Gateway64.exe
InterSystems.Data.IRISClient.dll
ADBKMain.vbの修正
それではADBKMain.vbの修正を行いましょう。
先ほど参照設定したライブラリーをImportします。
Option Explicit Onの後ろに以下の行を追加します。
Imports InterSystems.Data.IRISClientImports InterSystems.Data.IRISClient.ADO
class宣言の後ろの変数宣言の所を以下のように変更します。
Dim iris As IRIS Dim iris_object As IRISObject Dim iris_conn As IRISConnection = New IRISConnection 'Dim m_factory As CacheObject.Factory 'Dim m_object As CacheObject.ObjInstance 'Const m_classname As String = "User.ADBK" Const iris_classname As String = "User.ADBK"
CacheObjectの関連の変数をIRIS関連の変数に置き換えます。IRISの場合は、サーバーとの通信のコネクションが別オブジェクトになっているため追加の変数設定が必要です。
次にフォームのロード処理の修正を行います。
'm_factory = CreateObject("CacheObject.Factory") iris_conn.ConnectionString = "Server = localhost; Log File=cprovider.log;Port=1972; Namespace=USER; Password = SYS; User ID = _system;" iris_conn.Open() iris = IRIS.CreateIRIS(iris_conn)
IRISの場合は、まずコネクションオブジェクトを作成する必要があります。そして、そのConnectionStringプロパティにサーバー接続に必要な情報を設定します。次にOpen()メソッドで接続を確立します。接続が確立したら、IRISのインスタンスを生成します。CacheObjectではFactoryと呼んでいたものと同等のものとなります。
'sdir = m_factory.ConnectDlg 'If sdir <> "" Then 'm_factory.Connect(sdir)
CacheObjectの場合は、接続情報が指定されていない場合は、接続情報を尋ねるダイアログボックスが表示されていましたが、IRISのコネクションオブジェクトにはその機能がないので、すべてコメントアウトします。
次に新規ボタンが押された時の処理を変更しましょう。
CmdNew_Clickの処理になります。
以下のように書き換えます。
iris_object = iris.ClassMethodObject(iris_classname, "%New") If iris_object Is Nothing Then MsgBox("新しいオブジェクトを作成できません。") End If 'm_object = m_factory.New(m_classname) 'If m_object Is Nothing Then 'MsgBox("新しいオブジェクトを作成できません。") 'End If
CacheObjectの場合は、FactoryのNewメソッドで新しいオブジェクトの生成を行いましたが、IRISでは、ClassMethodObjectメソッドで第一パラメータで指定したクラスのクラスメソッド%Newメソッドを呼び出すように変更します。%Newメソッドは、OREFを返すので、ClassMethodObjectメソッドを使います。ClassMethodXXXは、戻り値のタイプ別に用意されています。
次に保存ボタンが押された時の処理を変更します。
CmdUpdate_Clickの処理になります。
'm_object.sys_Save() iris_object.Invoke("%Save")
インスタンスメソッドを呼び出す方法は、InVokeメソッドでパラメータに呼び出すメソッドの名前を指定します。次に削除ボタンが押された時の処理を変更します。
CmdDelete_Clickの処理になります。
'm_object.sys_DeleteId(id) iris_object.Invoke("%DeleteId", id)
同様にInVokeメソッドを呼び出すように変更します。
次に検索ボタンが押された時の処理を変更します。
CmdFind_Clickの処理になります。
'm_object = FindByName.ShowDialog_Renamed(m_factory) iris_object = FindByName.ShowDialog_Renamed(iris, iris_conn)
検索用のダイアログを表示する際に渡すパラメータを追加する必要がありました。コネクションオブジェクトを追加で渡す必要があります。また戻り値もIris_objectに変更します。次に CmdUpdate_Clickの処理を変更します。
'UPGRADE_WARNING: オブジェクト m_object.ANAME の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.ANAME = TxtNAME.Text iris_object.Set("ANAME", TxtNAME.Text) 'UPGRADE_WARNING: オブジェクト m_object.ASTREET の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.ASTREET = TxtADDRESS.Text iris_object.Set("ASTREET", TxtADDRESS.Text) 'UPGRADE_WARNING: オブジェクト m_object.AZIP の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.AZIP = TxtZIP.Text iris_object.Set("AZIP", TxtZIP.Text) 'UPGRADE_WARNING: オブジェクト m_object.ABTHDAY の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.ABTHDAY = TxtDOB.Text iris_object.Set("ABTHDAY", TxtDOB.Text) 'UPGRADE_WARNING: オブジェクト m_object.APHHOME の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.APHHOME = TxtTELH.Text iris_object.Set("APHHOME", TxtTELH.Text) 'UPGRADE_WARNING: オブジェクト m_object.APHOTH1 の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.APHOTH1 = TxtTELO.Text iris_object.Set("APHWORK", TxtTELO.Text) On Error GoTo actionSaveError 'UPGRADE_WARNING: オブジェクト m_object.sys_Save の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'm_object.sys_Save() iris_object.Invoke("%Save")
IRISObjectでは、プロパティに直接アクセスができず代わりにSetメソッドで設定する必要があります。また保存は、インスタンスメソッドの%Save()をInvokeメソッドで起動します。これでADBKMain.vbの修正は終了です。FindByNme.vbの修正続いてFindByNme.vbの内容を変更していきましょう。まずはIRISライブラリーのインポートが必要です。
Option Explicit Onの後ろに以下の行を追加します。
Imports InterSystems.Data.IRISClientImports InterSystems.Data.IRISClient.ADO
次に変数宣言の所を以下のように変更します。
'Dim RS As CacheObject.ResultSet 'Dim m_factory As CacheObject.Factory 'Dim m_object As CacheObject.ObjInstance Dim iris As IRIS Dim iris_object As IRISObject Dim iris_conn As IRISConnection 'Const m_classname As String = "User.ADBK" Const iris_classname As String = "User.ADBK"
IRISにはResultSetオブジェクトがありませんので、代替の方法で処理する必要があります。詳細は、後程説明します。
次にShowDialog_Renamed関数の処理を変更します。
これはADBKMainから検索ボタンを押したときに呼ばれる処理になります。
'Public Function ShowDialog_Renamed(ByRef factory As CacheObject.Factory) As CacheObject.ObjInstance Public Function ShowDialog_Renamed(ByRef iris_factory As IRIS, ByRef iris_connection As IRISConnection) As IRISObject
パラメータとしてiris_connectionを追加する必要があるので、追加します。
'm_factory = factory iris = iris_factory iris_conn = iris_connection
コネクションオブジェクトの設定を追加します。
'Set m_factory = Nothing 'ShowDialog_Renamed = m_object ShowDialog_Renamed = iris_object
戻り値をIRISObjectを返すように変更します。
次に検索ボタンが押された時の処理を変更します。
CmdFind_Clickの処理になります。
'RS = m_factory.ResultSet("User.ADBK", "ByName") 'RS.Execute(TxtSNAME.Text) ' ByName takes a single argument Dim SQLtext As String = "call sqluser.ADBK_byname(?)" Dim Command As IRISCommand = New IRISCommand(SQLtext, iris_conn) Dim Name_param As IRISParameter = New IRISParameter("Name_col", IRISDbType.NVarChar) Name_param.Value = TxtSNAME.Text Command.Parameters.Add(Name_param) Dim Reader As IRISDataReader = Command.ExecuteReader()
While Reader.Read() ListLookupName.Items.Add(Reader.Item(Reader.GetOrdinal("ANAME"))) End While Reader.Close() Command.Dispose()
' 取得した名前リストをListBoxに展開 'While RS.Next 'UPGRADE_WARNING: オブジェクト RS.GetDataByName() の既定プロパティを解決できませんでした。 詳細については、'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="6A50421D-15FE-4896-8A1B-2EC21E9037B2"' をクリックしてください。 'ListLookupName.Items.Add(RS.GetDataByName("ANAME")) 'End While
IRISではResultSetメソッドがありませんので、代替手段で書き換えます。クラスクエリーはSQLのcall文で置き換え可能です。但し、サーバー側のクエリー定義にsqlProc属性をつける必要があります。
Query ByName(Name As %String) As %SQLQuery(CONTAINID = 1) [ SqlProc ]
IRISCommandオブジェクトとIRISDataReaderオブジェクトを使用してクエリーを処理します。パラメータは、IRISParameterオブジェクトを使って定義します。
このあたりは、MicrosoftのADO.NETの仕様に基づき実装されているので、詳細はドキュメントを確認してください。
次にOKボタンが押された時の処理を変更します。
CmdOK_Clickの処理になります。
'name_Renamed = VB6.GetItemString(ListLookupName, ListLookupName.SelectedIndex) name_Renamed = ListLookupName.Items(ListLookupName.SelectedIndex).ToString()
これはIRIS対応とは関係ないのですが、動作しなかったので、変更しました。VB6との互換性がない部分だと想定しています。
'RS = m_factory.DynamicSQL("SELECT * FROM ADBK WHERE ANAME = ?") 'RS.Execute(name_Renamed) ' 値を'?'にバインド 'RS.Next() Dim SQLtext As String = "SELECT * FROM ADBK WHERE ANAME = ?" Dim Command As IRISCommand = New IRISCommand(SQLtext, iris_conn) Dim Name_param As IRISParameter = New IRISParameter("Name_col", IRISDbType.NVarChar) Name_param.Value = name_Renamed Command.Parameters.Add(Name_param) Dim Reader As IRISDataReader = Command.ExecuteReader() Reader.Read()
先ほどと同様にIRISCommandとIRISDataReaderを使って書き換えます。
'id = RS.GetDataByName("AID") id = Reader.Item(Reader.GetOrdinal("AID"))
フィールドの値の取得もさきほどと同様に書き換えます。
'm_object = m_factory.OpenId(m_classname, id) 'If m_object Is Nothing Then iris_object = iris.ClassMethodObject(iris_classname, "%OpenId", id) If iris_object Is Nothing Then
次は、検索の結果取得されたidを使って、オブジェクトインスタンスをオープンする処理もClassMethodObjectメソッドを使って実装します。
'CType(ADBKMain.Controls("TxtNAME"), Object).Text = m_object.ANAME CType(ADBKMain.Controls("TxtNAME"), Object).Text = iris_object.Get("ANAME") 'CType(ADBKMain.Controls("TxtZIP"), Object).Text = m_object.AZIP CType(ADBKMain.Controls("TxtZIP"), Object).Text = iris_object.Get("AZIP") 'CType(ADBKMain.Controls("TxtADDRESS"), Object).Text = m_object.ASTREET CType(ADBKMain.Controls("TxtADDRESS"), Object).Text = iris_object.Get("ASTREET") 'CType(ADBKMain.Controls("TxtTELH"), Object).Text = m_object.APHHOME CType(ADBKMain.Controls("TxtTELH"), Object).Text = iris_object.Get("APHHOME") 'CType(ADBKMain.Controls("TxtTELO"), Object).Text = m_object.APHOTH1 CType(ADBKMain.Controls("TxtTELO"), Object).Text =iris_object.Get("APHWORK") 'CType(ADBKMain.Controls("TxtAGE"), Object).Text = m_object.AAGE CType(ADBKMain.Controls("TxtAGE"), Object).Text = iris_object.Get("AAGE") 'CType(ADBKMain.Controls("TxtDOB"), Object).Text = m_object.ABTHDAY CType(ADBKMain.Controls("TxtDOB"), Object).Text = iris_object.InvokeString("ABTHDAYLogicalToOdbc", iris_object.Get("ABTHDAY"))
IRISObjectでは、プロパティに直接アクセスができず代わりにGetメソッドで取得する必要があります。
DOB(誕生日)は、内部値($Horolog)をODBC形式の日付に変換する処理を加えています。
以上で修正は終了です。
最後に
既存の資産を生かしつつ、ActiveX Bindingを使用したアプリケーションの移行が簡単にできるということをご理解いただき、アプリケーションの移行にチャレンジしていただきたいと思います。
もう一つ、サーバー側の処理は全く修正していない点も強調しておきたいと思います。
記事
Toshihiko Minamoto · 2021年3月22日
Caché 2013.1 より、InterSystems は特殊な値を持つフィールドが使われるクエリプランのセレクションを改善する目的で Outlier Selectivity (外れ値の選択性) を導入しました。
この記事では、「Project」テーブルを例に使い、Outlier Selectivity の概要やそれが SQL のパフォーマンスを向上させる仕組み、またクエリを書く際の注意点などについて解説したいと思います。
# Selectivity (選択性)
まずは、Selectivity についてさっと説明します。 Selectivity とは、テーブル内の 1 つの列の中にある値に関するメタ情報のことです。 データが典型的なかたちで分布されていると想定した場合、「このテーブル内のこの列に特定の値を持つすべての行を要求するとしたら、通常取得できるのはテーブル内のどの程度の割合であろうか?」という疑問の答えとなる情報です。
Owener と Status という 2 つのフィールドを持つ「Project」という架空のテーブルについて考えます。 Owner にはプロジェクトを担当する従業員が入り、Status には PREP、OPEN、REVIEW、COMPLETE という 4 つのオプションの 1 つが入ります。 [Tune Table](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_opttable#GSQLOPT_opttable_tunetable) を実行すると、クラスのストレージに Selectivity の値があるのが確認できます。
3.3726%
25.000%
では、次の 2 つのクエリについて考えます。
SELECT * FROM Projects WHERE Owner = ?
SELECT * FROM Projects WHERE Status = ?
1 つ目のクエリが返すプロジェクトの割合は、平均で「Project」テーブルにあるすべてのプロジェクトの 3% をわずかに超える程度です。 2 つ目のクエリの平均は 25% です。 こうしたテーブルが JOIN や複数の WHERE 条件を伴うクエリで使用されるとなれば、3% と 25% では実行時間に大きな差が生じるほか、Caché が実行するクエリプランも変更される可能性があります。
# Outlier Selectivity (外れ値の選択性)
Selectivity を見ればすべてが分かるという訳ではありません! フィールド内の潜在的な値は特殊なかたちで分布される場合があります。 Outlier Selectivity を使用することで、特殊な値、すなわち、外れ値を 1 つ持つフィールドを賢く取り扱うことができます。
「Project」テーブルでは、プロジェクトのステータスは先ほどふれた 4 つのうちの 1 つになりますが、数年ほど経てば COMPLETE のプロジェクトの数が他のステータスのプロジェクトよりも大分多くなります。
先ほども言いましたが、次のクエリは*平均で*「Project」テーブルの 25% を返します。
SELECT * FROM Projects WHERE Status = ?
ですが、もっと細かく推測できるはずです! もし、WHERE 節が「WHERE Status = 'COMPLETE'」であれば、テーブルのほぼすべてを取得できますが、 「WHERE Status = 'PREP'」だと、取得できる割合はごくわずかです。
保管する前の WHERE:
0.25
Outlier Selectivity の導入により、以下を格納できます。
0.9:"COMPLETE"
0.03333
これで、以下の 2 つのクエリを区別することができます。
SELECT * FROM Projects WHERE Status = 'COMPLETE'
SELECT * FROM Projects WHERE Status = 'PREP'
1 つ目のクエリはテーブル内にあるすべてのプロジェクトの 90% を返し、2 つ目はわずか 3% しか返さないと推測できます。
複数のテーブルや複数のインデックスから選べる選択肢があるクエリの場合、90% と 3% ではパフォーマンスに大きな差が生じるほか、この場合も SQL エンジンが選択するクエリプランが変更される可能性があります。
# Outlier Selectivity を使ったクエリ
Outlier Selectivity には間違いなくメリットがあり、アプリケーションに変更を加える必要もありません。しかし、フル活用するには注意すべき点がいくつかあります。 デフォルトで、Caché は同じ形式が使われたすべてのクエリに対し、クエリプランを 1 つだけ生成します。 (先ほどの WHERE Status = 'COMPLETE' や WHERE Status = 'PREP' など)
デフォルトで、Caché は、クエリのパラメーターの値は外れ値ではないと想定します。 クエリに強制的に外れ値を考慮させるには、丸かっこを二重にして、外れ値のリテラル置換を抑制します。
SELECT * FROM Projects WHERE Status = (('COMPLETE'))
SELECT * FROM Projects WHERE Status = 'PREP'
丸かっこを二重にすると、SQL エンジンがクエリ内のパラメーターの特定の値に対してプランを生成することを強制できます。 これで Caché は、プロジェクトの 90% が取得されるときと、3% が取得されるときが分かるため、このクエリに対して 2 種類のプランを使うことができます。
また、BiasQueriesAsOutlier の値を 1 か 0 に設定すれば、Caché がデフォルトで外れ値以外の値を想定するかどうかも制御できます。 以下を実行すると、Caché は、外れ値を使用するクエリは稀なクエリではないと想定します。
1
0.9:"COMPLETE"
0.03333
以上の例は、Outlier Selectivity の概要、およびそれがクエリのパフォーマンスを向上させる仕組みについて理解する手掛かりとしてお役に立ちましたでしょうか? この情報の別のプレゼン資料や SQL の他の統計に関する詳細は、[Selectivity と Outlier Selectivity](http://docs.intersystems.com/latestj/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_opttable#GSQLOPT_opttable_tunetable_selectivity) と題した DocBook 文書をご覧ください。
記事
Toshihiko Minamoto · 2020年12月8日
インスタンスのデータに基づくビジネスインテリジェンスを実装しようと計画中です。 DeepSee を使うには、データベースと環境をどのようにセットアップするのがベストですか?

このチュートリアルでは、DeepSee の 3 つのアーキテクチャ例を示しながら、上記の質問を解決します。 基本的なアーキテクチャモデルを、その制限を重点に説明するところから始めましょう。 以降のモデルは、複雑さが中程度のビジネスインテリジェンスアプリケーションに推奨されており、ほとんどのユースケースで十分なはずです。 チュートリアルの最後には、高度な実装を管理できるように、アーキテクチャの柔軟性を強化する方法を説明します。
このチュートリアルに含まれる例では、新しいデータベースとグローバルマッピングを紹介し、それらをセットアップする理由とタイミングについて説明します。 アーキテクチャを構築する際には、より柔軟な例から得られるメリットについて説明します。
始める前に
プライマリサーバーと分析サーバー
データの高可用性を実現する場合、InterSystems では一般的にミラーリングとシャドウイングを使用して、ミラー/シャドウサーバーに DeepSee を実装することをお勧めしています。 データの元のコピーをホストするマシンを「プライマリサーバー」と呼び、データとビジネスインテリジェンスアプリケーションのコピーをホストするマシンを「分析(またはレポーティング)サーバー」と呼びます。
プライマリサーバーと分析サーバーを用意しておくことは非常に重要です。これは主に、いずれのサーバーにおいてもパフォーマンスに関する問題を回避するためです。 推奨アーキテクチャに関するドキュメントをご覧ください。
データとアプリケーションコード
ソースデータとコードを同じデータベースに保存することは、通常、規模の小さなアプリケーションでのみうまく機能します。 より大規模なアプリケーションでは、ソースデータとコードをそれぞれの専用データベースに保存することが推奨されます。専用のデータベースを使用することで、データを分離しながらも、DeepSee が実行するすべてのネームスペースでコードを共有することができます。 ソースデータ用のデータベースは、本番サーバーからミラーリングできるようにしておく必要があります。 このデータベースは、読み取り専用または読み取り/書き込みのいずれかです。 このデータベースでは、ジャーナリングを有効にしておくことをお勧めします。
ソースクラスとカスタムアプリケーションは、本番サーバーと分析サーバーの両方にある専用データベースに保存します。 これら 2 つのソースコード用データベースは同期している必要がなく、同じバージョンの Caché を実行している必要もありません。 コードが別の場所で定期的にバックアップされているのであれば、ジャーナリングは通常必要ではありません。
このチュートリアルでは、次の構成を使用しています。 分析サーバーの APP ネームスペースには、デフォルトのデータベースとして APP-DATA と APP-CODE があります。 APP-DATA データベースは、プライマリサーバーにある
ソースデータ用データベースのデータ(ソーステーブルのクラスとファクト)にアクセスできます。 APP-CODE データベースは、Caché コード(.cls と .INT ファイル)とほかのカスタムコードを保存します。 このようにデータとコードを分離するのは典型的なアーキテクチャであり、ユーザーは、DeepSee コードとカスタムアプリケーションを効率的にデプロイすることができます。
異なるネームスペースでの DeepSee の実行
DeepSee を使用したビジネスインテリジェンス実装は、異なるネームスペースから実行されることがよくあります。 この記事では単一の APP ネームスペースのセットアップ方法を示しますが、同じ手順を使えば、ビジネスインテリジェンスアプリケーションが実行するすべてのネームスペースをセットアップすることも可能です。
ドキュメント
ドキュメントに含まれる初回セットアップの実行に関するページの内容を理解しておくことをお勧めします。 このページには、Web アプリケーションのセットアップ、DeepSee グローバルを個別のデータベースに配置する方法、および DeepSee グローバルの代替マッピングのリストが含まれています。
* * *
このシリーズの第 2 部では、基本的なアーキテクチャモデルの実装について説明します。
記事
Mihoko Iijima · 2023年5月24日
開発者の皆さん、こんにちは!
IRISのInteroperability(相互運用性)機能(Ensembleのプロダクション)を使用してどのようなことができるか、コード例をご覧いただきながら/サンプルを動かしながらご確認いただける記事をまとめてみました。
ぜひご参照ください。
記事一覧:
インストール環境をお持ちでない方でもお試しいただけるチュートリアル
ファイル連携を試してみたい
常駐プロセスを作りたい
レコードマップ機能を使いたい
MQTTを使いたい
FHIRサーバサイドアプリケーションを試したい
Interoperabilityを学習したい
システム連携の自動的な流れの中にユーザからの指示を介入できる「ワークフローコンポーネント」を試したい(2023/6/1 追記)
✅ インストール環境をお持ちでない方でもお試しいただけるチュートリアル
Developer Hubというページをご存知でしょうか。最近追加された開発者向け情報をまとめたポータルで、この中に事前準備不要でお試しいただけるチュートリアルが4種類含まれています。
その中の、Interoperability(相互運用性)チュートリアルでは、Redditに新しく投稿された記事=(https://www.reddit.com/new/)を一定間隔で取得し、全投稿の中から「猫(cat)」🐈について記載されている情報のみを抽出し、対象記事をファイル出力する流れをご体験いただけます。
上記画像をクリックするとチュートリアルトップページに移動します。後は画面右下に表示される ボタンをクリックするだけでチュートリアルがスタートします。
体験内容詳細については「IRIS の Interoperability(相互運用性)を試せるチュートリアル」で画面例付きでご紹介しています。ぜひご覧ください。
✅ ファイル連携を試してみたい
Interoperability機能には、ファイルやHTTP、ODBC/JDBCなど標準的なアクセスの基本処理を記述したアダプタというクラスが用意されています。
「IRIS Interoperability機能を使ったファイル連携」の中では、ファイル入力で行われる特定ディレクトリの定期的な監視、データ取り込みの部分をアダプタを利用して作成する例をご紹介しています。
✅ 常駐プロセスを作りたい
ObjectScriptレベルでも常駐プロセスを作成できますが、「Interoperability機能を使った常駐プロセスの作り方」の中では、「常駐プロセスの監視」や「ログ」の機能も付いたInteroperabilityを利用した常駐プロセスの作り方を解説しています。
手順とコード解説付き+サンプルコードも公開されています。
✅ レコードマップ機能を使いたい
固定長または、区切りマーク付きファイルの入出力を支援する機能です。(CSVを使用する場合は、CSVファイルを読み込ませてレコードマップ機能を作ることもできます)
「レコードマップで何ができるか?」には、サンプルコードに沿った使用例の解説PDFがあります。(説明中「Ensemble」と出てきますが「Interoperability」と同様の機能です)
✅ MQTTを使いたい
Interoperabilityのアダプタの中にはMQTTに対応したアダプタ含まれています。
「【GettingStarted with IRIS】MQTT を使った遠隔モニタリング(IRIS の MQTT アダプタを試してみよう!)」では、インターネット上に公開されているMQTTテスト用ブローカーを使用してサブスクライブする例をご紹介しています。
コンテナを使ったサンプルをご用意しています。
もう1つ、「IRISにてMQTTブローカーから気象データを取得しデータベースに格納する」では、プロダクション作成手順も含めた解説があります。ぜひご参照ください。
✅ FHIRサーバサイドアプリケーションを試したい
IRIS for Healthでは、医療情報交換標準規格 FHIR のリポジトリを作成できるのと、Interoperabilityを利用してFHIRサーバサイドアプリケーションを開発できます。
「FHIR R4 リソースリポジトリを簡単にお試しいただける開発環境テンプレートのご紹介」では、解説ビデオ付きでFHIRリポジトリへのアクセス方法、FHIRサーバーサイドアプリケーション開発例をご紹介しています。
また、コンテナでお試しいただけるサンプルもご用意しています。
✅ Interoperabilityを学習したい
「【はじめてのInterSystems IRIS】Interoperability(相互運用性)を使ってみよう!」は、サンプルコードをご覧いただきながら、Interoperability機能概要を確認できるシリーズ記事です。
これからInteroperabilityをはじめたい方、学習してみたい方に最適です。
✅ システム連携の自動的な流れの中にユーザからの指示を介入できる「ワークフローコンポーネント」を試したい
「システム連携の自動的な流れの中にユーザからの指示を介入できる「ワークフローコンポーネント」のサンプル」では、コンテナで動くサンプル環境を利用して、ユーザからの指示待ち、ユーザからの指示による処理の再開をご体験いただけます。
また、ご自身の環境でワークフローコンポーネントの動作を試されたい方向けに「ワークフローコンポーネントを使ってみよう!~使用手順解説~」もご用意しています。
ぜひお試しください。
記事
Tomohiro Iwamoto · 2021年5月18日
## 目的
Japan Virtual Summit 2021で、Kubernetesに関するセッションを実施させていただいたのですが、AzureのアカウントやIRIS評価用ライセンスキーをお持ちの方が対象になっていました。もう少し手軽に試してみたいとお考えの開発者の方もおられると思いますので、本記事では仮想環境でも利用可能なk8sの軽量実装である[mirok8s](https://microk8s.io/)で、IRIS Community Editionを稼働させる手順をご紹介いたします。
2022/1/7 若干の加筆・修正しました
マルチノード化する手順は[こちら](https://github.com/IRISMeister/iris_mk8s/blob/main/microk8s%E3%83%9E%E3%83%AB%E3%83%81%E3%83%8E%E3%83%BC%E3%83%89%E5%8C%96.md)に記載しています。
参考までに私の環境は以下の通りです。
|用途|O/S|ホストタイプ|IP|
|:--|:--|:--|:--|
|クライアントPC|Windows10 Pro|物理ホスト|172.X.X.30/24, (vmware NAT)192.168.11.1/24|
|mirok8s環境|ubuntu 20.04.1 LTS|上記Windows10上の仮想ホスト(vmware)|192.168.11.49/24|
ubuntuは、[ubuntu-20.04.1-live-server-amd64.iso](http://old-releases.ubuntu.com/releases/20.04.1/ubuntu-20.04.1-live-server-amd64.iso)を使用して、最低限のサーバ機能のみをインストールしました。
## 概要
IRIS Community EditionをKubernetesのStatefulSetとしてデプロイする手順を記します。
IRISのシステムファイルやユーザデータベースを外部保存するための永続化ストレージには、microk8s_hostpathもしくはLonghornを使用します。
使用するコードは[こちら](https://github.com/IRISMeister/iris_mk8s)にあります。
## インストレーション
microk8sをインストール・起動します。
```
$ sudo snap install microk8s --classic --channel=1.20
$ sudo usermod -a -G microk8s $USER
$ microk8s start
$ microk8s enable dns registry storage metallb
・
・
Enabling MetalLB
Enter each IP address range delimited by comma (e.g. '10.64.140.43-10.64.140.49,192.168.0.105-192.168.0.111'):192.168.11.110-192.168.11.130
```
ロードバランサに割り当てるIPのレンジを聞かれますので、適切な範囲を設定します。私の環境はk8sが稼働しているホストのCIDRは192.168.11.49/24ですので適当な空いているIPのレンジとして、[192.168.11.110-192.168.11.130]と指定しました。
この時点で、シングルノードのk8s環境が準備されます。
```
$ microk8s kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
metallb-system speaker-gnljw 1/1 Running 0 45s
metallb-system controller-559b68bfd8-bkrdz 1/1 Running 0 45s
kube-system hostpath-provisioner-5c65fbdb4f-2z9j8 1/1 Running 0 48s
kube-system calico-node-bwp2z 1/1 Running 0 65s
kube-system coredns-86f78bb79c-gnd2n 1/1 Running 0 57s
kube-system calico-kube-controllers-847c8c99d-pzvnb 1/1 Running 0 65s
container-registry registry-9b57d9df8-bt9tf 1/1 Running 0 48s
$ microk8s kubectl get node
NAME STATUS ROLES AGE VERSION
ubuntu Ready 10d v1.20.7-34+df7df22a741dbc
```
kubectl実行時に毎回microk8sをつけるのは手間なので、下記コマンドでエリアスを設定しました。以降の例ではmicrok8sを省略しています。
> 注意
> すでに"普通の"kubectlがインストールされていると、そちらが優先されてしまいますので、alias名をkubectl2にするなど衝突しないようにしてください。
```
$ sudo snap alias microk8s.kubectl kubectl
$ kubectl get node
NAME STATUS ROLES AGE VERSION
ubuntu Ready 10d v1.20.7-34+df7df22a741dbc
```
> 元の状態に戻すには sudo snap unalias kubectl
環境が正しく動作することを確認するためにIRISを起動してみます。下記コマンドの実行で、USERプロンプトが表示されるはずです。
```
$ kubectl run iris --image=intersystemsdc/iris-community:2022.1.0.209.0-zpm
$ watch kubectl get pod
$ kubectl exec -ti iris -- iris session iris
USER>
```
今後の作業に備えて、作成したPODを削除しておきます。
```
$ kubectl delete pod iris
```
## 起動
```
$ kubectl apply -f mk8s-iris.yml
```
> IRIS Community版なので、ライセンスキーもコンテナレジストリにログインするためのimagePullSecretsも指定していません。
しばらくするとポッドが2個作成されます。これでIRISが起動しました。
```
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
data-0 1/1 Running 0 107s
data-1 1/1 Running 0 86s
$ kubectl get statefulset
NAME READY AGE
data 2/2 3m32s
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.152.183.1 443/TCP 30m
iris ClusterIP None 52773/TCP 8m55s
iris-ext LoadBalancer 10.152.183.137 192.168.11.110 52773:31707/TCP 8m55s
```
この時点で、下記コマンドでirisのREST/APIで提供されているメトリックスを取得できるはずです。
```
$ curl http://192.168.11.110:52773/api/monitor/metrics
```
ポッドのSTATUSがrunningにならない場合、下記コマンドでイベントを確認できます。イメージ名を間違って指定していてPullが失敗したり、なんらかのリソースが不足していることが考えられます。
```
$ kubectl describe pod data-0
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 4m (x3 over 4m3s) default-scheduler 0/1 nodes are available: 1 pod has unbound immediate PersistentVolumeClaims.
Normal Scheduled 3m56s default-scheduler Successfully assigned default/data-0 to ubuntu
Normal Pulling 3m56s kubelet Pulling image "containers.intersystems.com/intersystems/iris-community:2021.1.0.215.3"
Normal Pulled 69s kubelet Successfully pulled image "containers.intersystems.com/intersystems/iris-community:2021.1.0.215.3" in 2m46.607639152s
Normal Created 69s kubelet Created container iris
Normal Started 68s kubelet Started container iris
```
下記コマンドでirisにO/S認証でログインできます。
```
$ kubectl exec -it data-0 -- iris session iris
Node: data-0, Instance: IRIS
USER>
```
下記で各IRISインスタンスが使用するPVCが確保されていることが確認できます。
```
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
dbv-mgr-data-0 Bound pvc-fbfdd797-f90d-4eac-83a8-f81bc608d4bc 5Gi RWO microk8s-hostpath 12m
dbv-data-data-0 Bound pvc-b906a687-c24c-44fc-acd9-7443a2e6fec3 5Gi RWO microk8s-hostpath 12m
dbv-mgr-data-1 Bound pvc-137b0ccf-406b-40ac-b8c5-6eed8534a6fb 5Gi RWO microk8s-hostpath 9m3s
dbv-data-data-1 Bound pvc-4f2be4f1-3691-4f7e-ba14-1f0461d59c76 5Gi RWO microk8s-hostpath 9m3s
```
dfを実行すると、データべースファイルを配置するための/vol-dataがマウント対象に表示されていなくて、一瞬、?となりますが、--all指定すると表示されます。
```
irisowner@data-0:~$ df --all
Filesystem 1K-blocks Used Available Use% Mounted on
・
・
/dev/sda2 205310952 26925908 167883056 14% /iris-mgr
/dev/sda2 205310952 26925908 167883056 14% /vol-data
/dev/sda2 205310952 26925908 167883056 14% /irissys/cpf
/dev/sda2 205310952 26925908 167883056 14% /etc/hosts
/dev/sda2 205310952 26925908 167883056 14% /dev/termination-log
/dev/sda2 205310952 26925908 167883056 14% /etc/hostname
/dev/sda2 205310952 26925908 167883056 14% /etc/resolv.conf
・
・
```
/dev/sda2はコンテナ内のデバイスではなく、ホスト上のデバイスなので、microk8s-hostpathの仕組み上、そのような表示になるのでしょう。
## 個別のポッド上のIRISの管理ポータルにアクセスする
下記コマンドで各ポッドの内部IPアドレスを確認します。
```
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
data-0 1/1 Running 0 46m 10.1.243.202 ubuntu
data-1 1/1 Running 0 45m 10.1.243.203 ubuntu
$
```
私の仮想環境のLinuxはGUIがありませんので、下記のコマンドを実行することで、Windowsのブラウザから管理ポータルにアクセスできるようにしました。
```
$ kubectl port-forward data-0 --address 0.0.0.0 9092:52773
$ kubectl port-forward data-1 --address 0.0.0.0 9093:52773
```
|対象|URL|ユーザ|パスワード|
|:--|:--|:--|:--|
|ポッドdata-0上のIRIS|http://192.168.11.49:9092/csp/sys/%25CSP.Portal.Home.zen|SuperUser|SYS|
|ポッドdata-1上のIRIS|http://192.168.11.49:9093/csp/sys/%25CSP.Portal.Home.zen|SuperUser|SYS|
> パスワードはCPFのPasswordHashで指定しています
データベースの構成を確認してください。下記のデータベースがPV上に作成されていることを確認できます。
|データベース名|path|
|:--|:--|
|IRISSYS|/iris-mgr/IRIS_conf.d/mgr/|
|TEST-DATA|/vol-data/TEST-DATA/|
## 停止
作成したリソースを削除します。
```
$ kubectl delete -f mk8s-iris.yml --wait
```
これで、IRISのポッドも削除されますが、PVCは保存されたままになっていることに留意ください。これにより、次回に同じ名前のポッドが起動した際には、以前と同じボリュームが提供されます。つまり、ポッドのライフサイクルと、データベースのライフサイクルの分離が可能となります。次のコマンドでPVCも削除出来ます(データベースの内容も永久に失われます)。
```
$ kubectl delete pvc -l app=iris
```
O/Sをシャットダウンする際には下記を実行すると、k8s環境を綺麗に停止します。
```
$ microk8s stop
```
O/S再起動後には下記コマンドでk8s環境を起動できます。
```
$ microk8s start
```
microk8s環境を完全に消去したい場合は、microk8s stopを「実行する前」に下記を実行します。(やたらと時間がかかりました。日頃は実行しなくて良いと思います)
```
$ microk8s reset --destroy-storage
```
## 観察
### ストレージの場所
興味本位の観察ではありますが、/iris-mgr/はどこに存在するのでしょう?microk8sはスタンドアロンで起動するk8s環境ですので、storageClassNameがmicrok8s-hostpathの場合、ファイルの実体は同ホスト上にあります。まずはkubectl get pvで、作成されたPVを確認します。
```
$ kubectl apply -f mk8s-iris.yml
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-ee660281-1de4-4115-a874-9e9c4cf68083 20Gi RWX Delete Bound container-registry/registry-claim microk8s-hostpath 37m
pvc-772484b1-9199-4e23-9152-d74d6addd5ff 5Gi RWO Delete Bound default/dbv-data-data-0 microk8s-hostpath 10m
pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb 5Gi RWO Delete Bound default/dbv-mgr-data-0 microk8s-hostpath 10m
pvc-e360ef36-627c-49a4-a975-26b7e83c6012 5Gi RWO Delete Bound default/dbv-mgr-data-1 microk8s-hostpath 9m55s
pvc-48ea60e8-338e-4e28-9580-b03c9988aad8 5Gi RWO Delete Bound default/dbv-data-data-1 microk8s-hostpath 9m55s
```
ここで、data-0ポッドのISC_DATA_DIRECTORYに使用されている、default/dbv-mgr-data-0 をdescribeします。
```
$ kubectl describe pv pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb
・
・
Source:
Type: HostPath (bare host directory volume)
Path: /var/snap/microk8s/common/default-storage/default-dbv-mgr-data-0-pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb
```
このpathが実体ファイルのありかです。
```
$ ls /var/snap/microk8s/common/default-storage/default-dbv-mgr-data-0-pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb/IRIS_conf.d/
ContainerCheck csp dist httpd iris.cpf iris.cpf_20210517 _LastGood_.cpf mgr
$
```
> storageClassNameにhostpathは使用しないでください。microk8s_hostpathとは異なり、同じフォルダに複数IRISが同居するような状態(破壊された状態)になってしまいます。
### ホスト名の解決
StatefulSetでは、各ポットにはmetadata.nameの値に従い、data-0, data-1などのユニークなホスト名が割り当てられます。
ポッド間の通信に、このホスト名を利用するために、[Headless Service](https://kubernetes.io/ja/docs/concepts/services-networking/service/#headless-service)を使用しています。
```
kind: StatefulSet
metadata:
name: data
kind: Service
spec:
clusterIP: None # Headless Service
```
> この特徴は、ノード間で通信をするShardingのような機能を使用する際に有益です。本例では直接の便益はありません。
nslookupを使いたいのですが、kubectlやk8sで使用されているコンテナランタイム(ctr)にはdockerのようにrootでログインする機能がありません。また、IRISのコンテナイメージはセキュリティ上の理由でsudoをインストールしていませんので、イメージのビルド時以外のタイミングで追加でソフトウェアをapt install出来ません。ここではbusyboxを追加で起動して、そこでnslookupを使ってホスト名を確認します。
```
$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
/ # nslookup data-0.iris
Server: 10.152.183.10
Address 1: 10.152.183.10 kube-dns.kube-system.svc.cluster.local
Name: data-0.iris
Address 1: 10.1.243.202 data-0.iris.default.svc.cluster.local
/ #
```
10.152.183.10はk8sが用意したDNSサーバです。data-0.irisには10.1.243.202というIPアドレスが割り当てられていることがわかります。FQDNはdata-0.iris.default.svc.cluster.localです。同様にdata-1.irisもDNSに登録されています。
## 独自イメージを使用する場合
現在のk8sはDockerを使用していません。ですので、イメージのビルドを行うためには別途Dockerのセットアップが必要です。
> k8sはあくまで運用環境のためのものです
### サンプルイメージを使用する場合
イメージはどんな内容でも構いませんが、ここでは例として[simple](https://github.com/IRISMeister/simple)を使用します。このイメージはMYAPPネームスペース上で、ごく簡単なRESTサービスを提供します。データの保存場所をコンテナ内のデータベース(MYAPP-DATA)から外部データベース(MYAPP-DATA-EXT)に切り替えるために、cpfのactionにModifyNamespaceを使用しています。
mk8s-simple.ymlとしてご用意しました(mk8s-iris.ymlとほとんど同じです)。これを使用して起動します。
### 自分でイメージをビルドする場合
ご自身でビルドを行いたい場合は、下記の手順でmicrok8sが用意した組み込みのコンテナレジストリに、イメージをpushします。
> 内容のわからない非公式コンテナイメージって...ちょっと気持ち悪いかも、ですよね。
(Docker及びdocker-composeのセットアップが済んでいること)
```
$ git clone https://github.com/IRISMeister/simple.git
$ cd simple
$ ./build.sh
$ docker tag dpmeister/simple:latest localhost:32000/simple:latest
$ docker push localhost:32000/simple:latest
```
このイメージを使用するように、ymlを書き換えます。
```
mk8s-simple.ymlを編集
前) image: dpmeister/simple:latest
後) image: localhost:32000/simple
```
### 起動方法
既にポッドを起動しているのであれば、削除します。
```
$ kubectl delete -f mk8s-iris.yml
$ kubectl delete pvc -l app=iris
```
```
$ kubectl apply -f mk8s-simple.yml
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.152.183.1 443/TCP 3h36m
iris ClusterIP None 52773/TCP 20m
iris-ext LoadBalancer 10.152.183.224 192.168.11.110 52773:30308/TCP 20m
$
$ curl -s -H "Content-Type: application/json; charset=UTF-8" -H "Accept:application/json" "http://192.168.11.110:52773/csp/myapp/get" --user "appuser:SYS" | python3 -mjson.tool
{
"HostName": "data-1",
"UserName": "appuser",
"Status": "OK",
"TimeStamp": "05/17/2021 19:34:00",
"ImageBuilt": "05/17/2021 10:06:27"
}
```
curlの実行を繰り返すと、HostName(RESTサービスが動作したホスト名)がdata-0だったりdata-1だったりしますが、これは(期待通りに)ロードバランスされているためです。
> まれにログインに失敗したり、待たされることがあります。Community EditionはMAX 5セッションまでですが、以前の何らかの操作によりその上限を超えてしまっている可能性があります。その場合、ライセンス上限を超えた旨のメッセージがログされます。
```
$ kubectl logs data-0
・
・
05/17/21-19:21:17:417 (2334) 2 [Generic.Event] License limit exceeded 1 times since instance start.
```
## Longhornを使用する場合
> 分散KubernetesストレージLonghornについては、[こちら](https://www.rancher.co.jp/pdfs/doc/doc-02-Hajimete_Longhorn.pdf)を参照ください。
IRISのようなデータベース製品にとってのメリットは、クラウド環境でアベーラビリティゾーンをまたいだデータベースの冗長構成を組めることにあります。アベーラビリティゾーン全体の停止への備えは、ミラー構成を組むことで実現しますが、Kubernetes環境であれば、分散Kubernetesストレージの採用という選択子が増えます。
> ミラー構成とは異なり、データベース以外のファイルも保全できるというメリットもあります。ただしパフォーマンスへの[負のインパクト](https://longhorn.io/blog/performance-scalability-report-aug-2020/)には要注意です。
### 起動方法
longhornを起動し、すべてのポッドがREADYになるまで待ちます。
```
$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/deploy/longhorn.yaml
$ kubectl -n longhorn-system get pods
NAME READY STATUS RESTARTS AGE
longhorn-ui-5b864949c4-72qkz 1/1 Running 0 4m3s
longhorn-manager-wfpnl 1/1 Running 0 4m3s
longhorn-driver-deployer-ccb9974d5-w5mnz 1/1 Running 0 4m3s
instance-manager-e-5f14d35b 1/1 Running 0 3m28s
instance-manager-r-a8323182 1/1 Running 0 3m28s
engine-image-ei-611d1496-qscbp 1/1 Running 0 3m28s
csi-attacher-5df5c79d4b-gfncr 1/1 Running 0 3m21s
csi-attacher-5df5c79d4b-ndwjn 1/1 Running 0 3m21s
csi-provisioner-547dfff5dd-pj46m 1/1 Running 0 3m20s
csi-resizer-5d6f844cd8-22dpp 1/1 Running 0 3m20s
csi-provisioner-547dfff5dd-86w9h 1/1 Running 0 3m20s
csi-resizer-5d6f844cd8-zn97g 1/1 Running 0 3m20s
csi-resizer-5d6f844cd8-8nmfw 1/1 Running 0 3m20s
csi-provisioner-547dfff5dd-pmwsk 1/1 Running 0 3m20s
longhorn-csi-plugin-xsnj9 2/2 Running 0 3m19s
csi-snapshotter-76c6f569f9-wt8sh 1/1 Running 0 3m19s
csi-snapshotter-76c6f569f9-w65xp 1/1 Running 0 3m19s
csi-attacher-5df5c79d4b-gcf4l 1/1 Running 0 3m21s
csi-snapshotter-76c6f569f9-fjx2h 1/1 Running 0 3m19s
```
mk8s-iris.ymlの全て(2箇所あります)のstorageClassNameをlonghornに変更してください。
もし、microk8s_hostpathで既に起動しているのであれば、ポッド、PVCともに全て削除したうえで、上述の手順を実行してください。つまり...
```
$ kubectl delete -f mk8s-iris.yml --wait
$ kubectl delete pvc -l app=iris
mk8s-iris.yml編集
前)storageClassName: microk8s-hostpath
後)storageClassName: longhorn
$ kubectl apply -f mk8s-iris.yml
```
> マウントしたLonghorn由来のボリュームのオーナがrootになっていたのでsecurityContext:fsGroupを指定しています。これ無しでは、データベース作成時にプロテクションエラーが発生します。
> fsGroup指定なしの場合
> ```
> $ kubectl exec -it data-0 -- ls / -l
> drwxr-xr-x 3 root root 4096 May 18 15:40 vol-data
> ```
> fsGroup指定ありの場合
> ```
> $ kubectl exec -it data-0 -- ls / -l
> drwxrwsr-x 4 root irisowner 4096 Jan 5 17:09 vol-data
> ```
> 2021.1まではfsGroup:52773を指定すると動きましたが、2022.1以後はfsGroup:51773を指定すると動きました。
下記を実行すれば、Windowsのブラウザから、[Longhorn UI](http://192.168.11.49/)を参照できます。
```
$ microk8s enable ingress
$ kubectl apply -f longhorn-ui-ingress.yml
```
ポート80を他の用途に使ってる場合、下記のようにport-forwardを使う方法もあります。この場合ポートは8080なので、URLは[こちら](http://192.168.11.49:8080/)になります。
```
$ kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80 --address 0.0.0.0
```
> UIで、VolumeのStateが"Degraded"になっていますが、これはReplicaの数がnumberOfReplicasの既定値3を満たしていないためです。
以降の操作は、同様です。不要になれば削除します。
```
$ kubectl delete -f mk8s-iris.yml
$ kubectl delete pvc --all
```
### 削除方法
Longhorn環境が不要になった場合は、下記のコマンドで削除しておくと良いようです(いきなりdeleteしてはダメ)。
```
$ kubectl create -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/uninstall/uninstall.yaml
$ kubectl get job/longhorn-uninstall -n default -w
NAME COMPLETIONS DURATION AGE
longhorn-uninstall 1/1 79s 97s
^C
$ kubectl delete -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/deploy/longhorn.yaml
$ kubectl delete -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/uninstall/uninstall.yaml
```
### apply時のエラー
Longhornの前回の使用時に綺麗に削除されなかった場合に、apply時に下記のようなエラーが出ることがあります。
```
$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
・
・
Error from server (Forbidden): error when creating "https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml": serviceaccounts "longhorn-service-account" is forbidden: unable to create new content in namespace longhorn-system because it is being terminated
Error from server (Forbid
```
上記のuninstall.yamlを使った削除手順をもう一度実行したら回復しました。
### その他気づいた事
storageClassにmicrok8s_hostpathを指定した場合、[マルチノード環境](https://microk8s.io/docs/clustering)ではsecurityContext:fsGroupが正しく機能しないようです。その結果、下記のようなエラーが発生して、データベースの作成に失敗します(Error=-13はPermission denieです)。longhornは問題なく動作しました。
```
01/07/22-23:11:32:729 (1205) 1 [Utility.Event] ERROR #503: Error executing [Actions] section in file /iris-mgr/IRIS_conf.d/merge_actions.cpf
01/07/22-23:11:32:729 (1205) 1 [Utility.Event] ERROR #507: Action 'CreateDatabase' failed at line 2, Method Config.CPF:CreateOneDatabase(), Error=ERROR #5032: Cannot create directory '/vol-data/db/TEST-DATA/, Error=-13'
```
## InterSystems Kubernetes Operator
IKOもmicrok8sで動作しますが、Community向けの機能ではないので、今回のご紹介は見送りました。
記事
Minoru Horita · 2020年4月30日
データを格納するための魔法の剣であるグローバルは、かなり前から存在しています。しかしながら、これを効率的に使いこなせる人や、この素晴らしい道具の全貌を知る人はそう多くありません。 グローバルを本当に効果を発揮できるタスクに使用すると、パフォーマンスの向上やソリューション全体の劇的な単純化といった素晴らしい結果を得ることができます(1、2)。
グローバルは、SQLテーブルとはまったく異なる特別なデータの格納・処理方法を提供します。 グローバルは1966年にM(UMPS)プログラミング言語で初めて導入され、医療データベースで使用されていました。 また、現在も同じように使用されていますが、金融取引など信頼性と高いパフォーマンスが最優先事項である他のいくつかの業界でも採用されています。
M(UMPS)は後にCaché ObjectScript(COS)に進化しました。 COSはInterSystemsによってMの上位互換として開発されました。 元の言語は現在も開発者コミュニティに受け入れられており、いくつかの実装で生き残っています。 ウェブ上では、MUMPS Googleグループ、Mumpsユーザーグループ、ISO規格といった複数の活動が見られます。
最新のグローバルベースのDBMSは、トランザクション、ジャーナリング、レプリケーション、パーティショニングをサポートしています。 つまり、現代的で、信頼性が高く、高速な分散システムの構築に使用できます。
グローバルは、リレーショナルモデルの限界に制限されません。 特定のタスクに最適化されたデータ構造を自由に作成できます。 多くのアプリケーションにとって、グローバルの合理的な使用は、従来のリレーショナルアプリケーション開発者の理想でしかなかった速度を実現する真の特効薬になるかもしれません。
グローバルは、高レベルおよび低レベル両方の多くの最新のプログラミング言語でデータ保存方法として使用できます。 そのため、この記事ではグローバルの由来となった言語ではなく、グローバルに限定して焦点を当てることにします。
2. グローバルの仕組み
まずはグローバルの仕組みと、そのメリットを理解しましょう。 グローバルはさまざまな視点から見ることができます。 このパートでは、グローバルをツリーまたは階層型データストレージと見なします。
簡単に言えば、グローバルは永続的配列です。 つまり、自動的にディスクに保存される配列ということです。
これ以上簡単にデータを保存する方法を想像するのは困難です。 プログラムコード(COS/M言語で記述)では、その名前の前に^記号があるという点だけが通常の連想配列とは異なっています。
必要なコマンドはすべて非常に簡単で、1時間で習得が可能で、データをグローバルに保存するのにSQLの知識は必要ありません
最も単純な例である、2つの分岐を持つ単一階層のツリーから始めましょう。 以下の例はCOSで記述されています。
Set ^a("+7926X") = "John Sidorov"
Set ^a("+7916Y") = "Sergey Smith"
データがグローバルに挿入されると(Setコマンド)、次の3つの処理が自動的に行われます。
ディスクへのデータ保存。
インデックスの構築。 括弧内にあるのは添え字、等号の右側にあるのはノードの値です。
ソート処理。 データはキーでソートされます。 データの探索を行うと「Sergey Smith」が最初に返され、その後に「John Sidorov」が返されます。 グローバルからユーザーのリストを取得する場合、データベースはソートに時間を費やしません。 実在しないキーも含め、任意のキーから始まるソート済みのリストをリクエストできます(出力は実在しないキーの後に続く最初の実在するキーから始まります)。
これらの処理はすべて驚異的な速度で実行されます。 筆者宅のシステム(i5-3340、16GB、HDD WD 1TB Blue)では、1回のプロセスで毎秒1,050,000件のレコードが挿入されました。 マルチコアシステムでは、毎秒数億件のレコードを挿入できる可能性があります。
もちろん、レコードの挿入速度自体にはあまり意味がありません。 噂ですが、例えばVisaではデータをテキストファイルに書き込んでいるそうです。 ただし、グローバルを使用すれば、高速で使いやすく、構造化され、インデックス化されたストレージを得ることができます。
グローバルの最大の強みは、新しいノードをグローバルに挿入する速度です。
データは常にグローバル内でインデックス化されています。 単一階層や階層を下るツリー探索は常に大変高速です。
グローバルの第2階層と第3階層にいくつか枝を追加してみましょう。
Set ^a("+7926X", "city") = "Moscow"
Set ^a("+7926X", "city", "street") = "Req Square"
Set ^a("+7926X", "age") = 25
Set ^a("+7916Y", "city") = "London"
Set ^a("+7916Y", "city", "street") = "Baker Street"
Set ^a("+7916Y", "age") = 36
、グローバルを使用して複数階層のツリーを構築することができます。 挿入が発生するたびに自動インデックス付けが行われるため、どのノードにもほぼ瞬時にアクセスできます。 ツリー内のどの階層の枝も、キーでソートされます。
ご覧のとおり、キーと値との両方にデータを保存できます。 Cachéではキーを組み合わせた長さ(すべてのインデックスの長さの合計)は511バイトまで保存でき、値は最大3.6 MB まで保存できます。 ツリーの階層数(次元数)の上限は31です。
もう1つのすばらしい点は、上位階層のノードの値を定義せずにツリーを構築できることです。
Set ^b("a", "b", "c", "d") = 1
Set ^b("a", "b", "c", "e") = 2
Set ^b("a", "b", "f", "g") = 3
空の円は値のないノードを表しています。 グローバルをより深く理解するため、グローバルを庭木やファイルシステム名ツリーと比較してみましょう。
グローバルを庭や畑で育つ普通の木やファイルシステムといった、よく見慣れた階層構造と比較してみます。
ご覧のとおり、普通の木では葉と果実は枝の先端でのみ育ちます。 ファイルシステムの場合も、情報は完全ファイル名とも呼ばれる枝の先端に保存されます。
そして、こちらがグローバルのデータ構造です。 違い:
内部ノード:グローバルの情報は枝の先端だけでなく、すべてのノードに保存できます。
外部ノード: グローバルにはファイルシステムと庭木には必須ではありませんが、定義された枝の端(値のある先端)が必要です。
内部ノードに関しては、グローバルの構造をファイルシステム名ツリーと庭木構造の上位セットとして扱うことができます。 したがって、グローバルの構造はより柔軟になっています。
大まかに言うと、グローバルは各ノードでのデータ保存に対応した構造化ツリーです。
グローバルの仕組みをよりよく理解するため、ファイルシステムの作成者が情報の格納にグローバルと同じ手法を採用した場合にどうなるかを想像してみましょう。
あるフォルダー内の最後のファイルが削除された場合、そのフォルダー自体だけでなく、その削除対象のフォルダーのみを含むすべての上位階層のフォルダーも削除されることになります。
この場合、フォルダーはまったく必要ありません。 サブファイルを含むファイルとサブファイルのないファイルがあるとします。 普通の木と比較して見ると、一つ一つの枝が実になることが分かります。
README.txtのようなものは不要になりそうです。 ただし、フォルダーの内容に関して言及したいすべての情報はフォルダーファイル自身に書き込まれる可能性があります。 通常、ファイル名はフォルダー名と区別されません(例えば、 /etc/readme はファイルにもフォルダーにもなり得ます)。つまり、ファイルのみを操作することで十分ということになります。
サブフォルダーとファイルを含むフォルダーは、はるかに高速に削除できるでしょう。 数百万件の小さなファイルの削除がいかに時間がかかり、困難であるかを伝える記事がネット上にいくつか存在します(1、2、3)。 ただし、グローバルに基づいて疑似ファイルシステムを作成した場合は数秒か数分の1秒しかかかりません。 筆者の自宅PCでサブツリーの削除をテストしたときは、HDD(SDDではない)上の2階層のツリーから9,600万ノード~3億4,100万ノードを削除できました。 また、重要な事ですが、ここで話題にしているのはグローバルを含むファイル全体の削除ではなく、グローバルツリーの一部を削除することです。
グローバルのもう1つの長所は、再帰的処理をすることなくサブツリーを削除できることです。信じられないほど高速です。
上記のツリーでは、次のようなKillコマンド1つでサブツリーを削除できます。
Kill ^a("+7926X")
グローバルで実行できるアクションをよりよく理解できるよう、以下の小さな表にまとめています。
COSのグローバルに関連する主なコマンドと機能
Set
ノードまでの枝(未定義の場合)とノード値を設定(初期化)します。
Merge
サブツリーをコピーします。
Kill
サブツリーを削除します。
ZKill
特定ノードの値を削除します。 そのノードから生じたサブツリーは影響を受けません。
$Query
ツリー全体を深さ優先探索します。
$Order
同じ階層の次の添え字を返します。
$Data
ノードが定義されているかどうかを確認します。
$Increment
ACIDの読み取りと書き込みを回避するため、ノード値のアトミックなインクリメント操作を実行します。 最近は、$Sequenceを代わりに使用することを推奨しています。
最後までお読みいただき、ありがとうございました。喜んで皆様からのご質問にお答えします。
免責事項:この記事は筆者(英語原文はSergey Kamenev氏によるものです)の私見を反映したものであり、InterSystemsの公式見解とは関係ありません。
続きは「グローバルはデータを保存するための魔法の剣です パート2 - ツリー」を読み進めてください。 グローバルに表示できるデータのタイプと、グローバルが最適に機能する場所について学習します。
記事
Shintaro Kaminaka · 2020年9月11日
開発者の皆さん、こんにちは。
今回の記事では前回の記事に引き続き、IRIS for Health上で、FHIRリポジトリ+OAuth2認可サーバ/リソースサーバを構成する方法をご案内します。
(注意:2020.4以降のバージョンではこの記事に記載されているスコープ指定では正しくリソースが取得できません。詳細はこちらの記事をご覧ください。)
パート1では、事前準備と、OAuth2認可サーバを構成し、アクセストークンを取得するとこまでをご紹介しました。このパート2では、FHIRリポジトリの構築方法と、OAuth2クライアント/リソースサーバの構成方法をご紹介していきます。
今日構成する、FHIRリポジトリおよび、OAuth2クライアント/リソースサーバの構成は、前回パート1で構成したOAuth2認可サーバのIRISインスタンスと分けることもできますし、同じインスタンスに同居させることもできます。この記事の中では前回と同じインスタンス上に構成していきます。
FHIRリポジトリの構築とOAuth Client Nameの指定
FHIRリポジトリの構築方法は、過去の記事「Azure上でIRIS for Healthをデプロイし、FHIR リポジトリを構築する方法」で紹介しています。
管理ポータルにアクセスし、ネームスペース/データベースを作成、さらにHealthメニューからFHIRリポジトリを構築する手順はこの記事を参考にして進めてください。この記事の中でもネームスペース/データベース=FHIRSERVER , FHIR endpointを/csp/healthshare/fhirserver/fhir/r4として構成しています。
構築後の以下の画面で、endpoint URL /csp/healthshare/fhirserver/fhir/r4 をクリックして構成画面を開きます。
構成画面で OAuth Client Name を欄に、これから作成するOAuth2クライアントの構成名を入力しておきます。先にOAuth2クライアントを構成している場合はその名前に合わせてください。
ここでは、「FHIRResource」という文字列にしておきます。変更するには上記画面で「Edit」ボタンを押して変更し、「Update」ボタンで保存します。
OAuth2クライアントの構成
次は、OAuth2クライアント構成を作成していきましょう。
管理ポータルのシステム管理→セキュリティ→OAuth2.0 と進み、前回パート1とは異なり、「サーバ」ではなく、「クライアント」を選択します。
次の画面では「サーバの説明を作成」をクリックして、OAuth2認可サーバへ接続するための構成を作成します。
サーバデスクリプション ページで発行者エンドポイントには、パート1で構成した認可サーバのエンドポイントを入力します。
以下はパート1で構成したOAuth2認可サーバの構成画面です。
SSL/TLS構成には、パート1の事前準備で作成した、SSL/TLS構成「SSL4CLIENT]を入力します。
項目を入力したら「発見して保存」を実行して、OAuth2認可サーバから情報を取得します!
アクセスに成功すると以下のように取得できた情報が表示されます。パート1事前準備で用意したホスト名を指定したSSL証明書が正しく作成・認識されていない場合、この過程でエラーが発生することがありますので、ご注意ください。
注意:この連載のパート1でDLしたdocker-containerファイルを使っている場合でも、IRISコンテナ→Apacheコンテナへホスト名を指定したアクセスがうまくいかない場合があります。この場合、以下のようにextra_hostsとして、docker-compose.ymlファイルに自分のマシンのホスト名とIPアドレスを入力すると解決できることがあります。
extra_hosts: - <yourhostname>:<your ip address>
「保存」を押して構成を保存すると、以下のページに戻りますので、続いて「クライアント構成」を選択してFHIRリポジトリ用の構成を作成していきます。
OAuth2クライアントにクライアント構成を追加する
ややこしいタイトルですが、次は今作成したOAuth2クライアント設定(どのOAuth2認証サーバに接続するかという情報をもつ)に、クライアント構成(OAuth2クライアントとしてOAuth2認可サーバに接続したい、具体的なFHIRリポジトリやCSPアプリケーションなどの情報)を追加します。
次の画面では「クライアントの構成を作成」をクリックして以下の画面を表示し、必要な項目を設定していきます。
最初に、クライアントの種別=リソース・サーバ を選択すると、下記入力画面と同じになります。
アプリケーション名
FHIRResource : FHIRリポジトリの構成で「OAuth Client Name」に入力した値を入力します。
クライアント名
OAuth2認可サーバに登録されるクライアント名です。アプリケーション名と同じでもかまいませんが、ここでは違う名前にしました。
説明
この構成の説明を入力します。
クライアントの種別
「リソース・サーバ」を選択します。
SSL/TLS構成
パート1事前準備で用意したSSL/TLS構成を指定します。
入力が完了したら、「動的登録と保存」ボタンをクリックして保存とサーバへの登録を行います。(ちょっとわかりにくいですが)ボタンの表示が「動的登録と保存」から「更新メタデータを取得して保存」に変わったら登録が成功しています。
OAuth2認可サーバ側の構成情報を見て、本当に登録されているか確認してみましょう。
管理ポータル→システム管理→セキュリティ管理→OAuth2.0→サーバ の画面で「クライアントデスクリプション」をクリックすると以下のように登録されていることがわかります。
名前がクライアント名で指定した名前になっていることが確認できます。
パート1ではPostmanからアクセステストする際は、このクライアントデスクリプション画面をさらに進んで表示される、クライアント認証情報(クライアントIDとくらい案tの秘密鍵)を手動でコピーしましたが、今回は動的登録の過程でこれらの情報はクライアント側に受け渡されています。
PostmanからOAuth2アクセストークンを使って、FHIRリポジトリへアクセスする
それではいよいよ、Postmanからアクセスしてみましょう!
まずアクセストークンを取得します。基本はパート1の最後で取得した方法と同じですが、アクセストークンの発行先を表すaudienceパラメータを追加する必要があります。
aud=https://<hostname>/csp/healthshare/fhirserver/fhir/r4
Postmanで具体的に追加するには、以下のようにAuthorization Codeのendpoint URLにパラメータとして追加します。(Postmanの画面の都合上パラメータの全体が見えませんが、上記の aud=https://<hostname>/csp/healthshare/fhirserver/fhir/r4 をすべて記載してください)
注意:Postmanに入力するClient IDやClient Secretは先ほどのリソースサーバの動的登録で発行されたものに変更する必要はありません。パート1で追加した postman用に発行されたクライアントIDと秘密鍵を使用します。
アクセストークンが取得できたら、その内容をコピーしておいてください。
PostmanではこのままAuthorizationのTYPEをOAuth2にしておくと、アクセストークンを送信する機能がありますが、IRIS for HealthのFHIRリポジトリでは、OAuth2のアクセストークンだけではなく、Basic認証のユーザ・パスワード情報も送信する必要があります。
そのため、Postmanからアクセスする場合は、(ちょっと手間ですが)AuthorizationのTYPEはBasic Authにして、ユーザ名パスワードを入力し、アクセストークンはFHIRリポジトリへのRESTリクエストのParameterとして送信する必要があります。
具体的には、まず以下の画面のようにユーザ名・パスワードを入力します。このユーザ情報は、アクセストークンのsub内に含まれるユーザ情報と一致しているかの確認が行われるため、必ずアクセストークン取得時に入力したユーザ情報と同じユーザである必要があります。
次に、Paramsタブで、 access_token にパラメータに先ほどのアクセストークン値を入力します。
FHIRリポジトリを構築したばかりであれば、リポジトリには何のデータもはいってはいませんが、Patientデータをリクエストしてみましょう!
Request URL には https://<hostname>/csp/healthshare/fhirserver/fhir/r4/Patient を入力し、HTTPのメソッドはGETを選択します(上の図のようになります)
Sendボタンを押してリクエストを投げてみましょう!以下のようにFHIRのBundleが取得できれば、アクセストークンを使用したFHIRリポジトリへのアクセスは成功です!
FHIRリポジトリへのデータの登録や検索の方法については、IRIS for Healthのドキュメントやコミュニティの記事をご参照ください。
IRIS for Health 2020.1 日本語ドキュメント:リソースリポジトリ
IRIS for Health 2020.3 英語ドキュメント:Resource Repository
(この記事を執筆した段階では、2020.3はPreview Editionです。)
いかがでしたか?FHIRリポジトリへのアクセスが成功したでしょうか?
この連載で紹介した構成内容は最も単純な構成ですが、実際のFHIRプロジェクトではユーザの認めたスコープによってどの範囲のデータまで返すように実装するか?といった検討と実装が必要になってきます。
開発者コミュニティでは引き続きFHIRに関する情報を発信していきたいと思います。 「発見して保存」を押した後、下記エラーが表示されます。
自己署名証明書を使用しているせいかと思いましたが、何か原因がございますでしょうか?
※ブラウザで管理ポータルに繋ぐ際、自己署名証明書の警告は表示されます。
お試しいただきありがとうございます。
IRIS for Health のコンテナから、ホストに対してホスト名でアクセスできていない可能性があります。以下のコマンド試してみてください。
#docker exec -it irishealth bashirisowner@iris:~$ ping <yourhostname>ping: <yourhostname>: Name or service not known
このメッセージが出る場合、記事内にある extra_hosts: の追加を試してみてください。(この注意を書く位置がよくなったです。。。お手数かけて申し訳ありません。)
エラーが出た時点でストップしてしまい、少し下の注釈を見落としておりました。申し訳ございません。
記載頂いたpingは通る状態だったのですが、extra_hostsの設定は行っていなかったため設定したところ無事動作しました。
ただ以下のようにlocalhostではNGで、実際のIPアドレスを指定しないとエラーが解消しませんでしたので念のため共有させて頂ければと思います。
NG
```
iris:
image: store/intersystems/irishealth-community:2020.3.0.200.0
init: true
container_name: irishealth
ports:
- "52773:52773"
- "1972:1972"
environment:
- ISC_DATA_DIRECTORY=/ISC/dur
- TZ=JST-9
volumes:
- .:/ISC
extra_hosts:
- my-ubuntu:172.0.0.1
```
OK
```
iris:
image: store/intersystems/irishealth-community:2020.3.0.200.0
init: true
container_name: irishealth
ports:
- "52773:52773"
- "1972:1972"
environment:
- ISC_DATA_DIRECTORY=/ISC/dur
- TZ=JST-9
volumes:
- .:/ISC
extra_hosts:
- my-ubuntu:192.168.0.123
``` ご確認と情報提供ありがとうございました。
記事の注意書きの位置も修正しておきました。 記事の修正誠にありがとうございます。
その後手順通り試したところ、FHIRのBundle取得まで正常に完了することが出来ました。
記事
Hiroshi Sato · 2020年12月16日
この文書では、インターシステムズの製品の中で、InterSystems CachéおよびIRIS data platformに関するライセンスポリシーを説明します。
インターシステムズのライセンスポリシーは、ソリューション・パートナー契約の際の契約書一式あるいはエンドユーザーに直接販売する際の契約書一式に含まれる製品に関する条件(この文書は製品別に存在します)という文書で規定されています。
なおこの文書は一般には公開していません。
ここでは、CachéおよびIRIS data platformのライセンスポリシーについてこの文書に書かれていることを要約および補足して説明します。
まずCachéシステムおよびIRIS data platformはこの文書で規定されているライセンスポリシーにでき得る限り忠実にそうようにライセンスチェック機構を実装しています。
しかしながら様々な技術的な制約によりライセンスポリシーとこれら製品のライセンスシステムを完全に一致させることはできません。
そしてもしシステム上のライセンスチェック機構の動作とライセンスポリシー上に不一致が発生した場合には、いかなる場合でもライセンスポリシーが優先されます。
つまりライセンスシステム上許容されている動作であっても、ライセンスポリシーに合致していない場合には、ライセンスポリシーに合うような運用を行わなければなりません。
次にライセンスの形態ですが、Cachéは、ユーザー単位の同時ユーザーライセンスとなっています。
IRIS data platformは、同時ユーザーライセンスに加えて、CPUコア数単位に課金するライセンスタイプも提供しています。
同時ユーザーライセンスの場合、同時ユーザー1名が課金の単位となり、その課金の単位をLU(License Unit)と呼びます。
システムの稼働に必要なライセンス容量を決定する際に、システムのピーク時に同時アクセスする最大ユーザー数を見積もることで必要なLU数が求められます。
さらにこの同時という概念も少し説明が必要です。
ここで言う同時とは、サーバーとユーザーが使用するクライアントデバイス間のソフトウェア的な接続(TCPセッションなど)が確立されているかどうかは関係なく、その仮想的な接続を通してサーバー上のインターシステムズデータプラットフォームの機能を実行できる準備がクライアントデバイス側にできている状態を意味しています。
例えば、接続プールやHTTPのように毎回接続を確立して終了後切断するような技術を使い、クライアント側とサーバー側のコネクションが切れたとしても、クライアント側のアプリケーションが何等かの状態で動作している限り(画面表示され、次のアクションを待っているなど)は、その同時接続が維持されていると考えます。
(これを技術的に完全に検知するのは不可能です。)
そして1LU当たり一人のユーザーの1つのデバイス当たり最大12個の接続あるいはプロセスを(合算して)同時に持つことができます。
同一人物が同時に複数のデバイスを使う場合にはデバイス毎にLUを消費します。
また人と直接結びつかずに単独でサーバーと接続するような1デバイス(医療検査機器など)も1ユーザーとみなします。
さらに人と直接結びつかないバッチプロセス(日時、週時、月時処理など)もLUの1つの形態とみなし最大12同時プロセスまで1LUとしてカウントできます。
なお、同一ユーザーの同一デバイスからの同時アクセスが25を超えると接続数分のLUがカウントされるというライセンスシステムの動作は、ライセンスポリシーで規定されたものではなく、システムが同じ所から多数の同時アクセスがあったことを検知した結果、シングルユーザーではなくマルチユーザーアクセスしていると判断してライセンスカウントの方法を変えたということを意味しています。
実案件でもし本当に一人のユーザーからの同時接続が12を超える可能性がある場合にLUをどうカウントするかあるいはその他ライセンス数の数え方の不明な点については、インターシステムズ営業部門までお問い合わせください。IRIS data platformのCPUコアライセンスは、CPUコアがライセンスユニット(LU)の単位となります。
物理サーバー上でIRIS data platformを稼働する場合は、IRISインスタンスまたはIRISクラスタを構成する全てのIRISインスタンスが稼働する物理サーバー上のCPUコア数の総計をカウントします。
仮想サーバー上でIRIS data platformを稼働する場合は、IRISインスタンスまたはIRISクラスタを構成する全てのIRISインスタンスが稼働する仮想サーバー上のCPUコア数の総計をカウントします。
なおインテル社のCPUに採用されているハイパースレッディングのようなSMT(Simultaneous Multithreading Technology)は物理サーバー上ではCPUのコア数としてはカウントしません。
しかし一方で仮想環境やAWSなどのクラウド環境では仮想CPU (製品やベンダー、クラウドサービスによって呼び方が異なるが一般的にはvCPUと表現される)がコア数の単位になります。
記事
Toshihiko Minamoto · 2022年3月2日
これは、IRIS でリレーショナルデータをクエリするアナリストとアプリケーションに、さらに優れた適応性とパフォーマンスによるエクスペリエンスを提供する IRIS SQL のイノベーションをトピックとした短い連載の 3 つ目の記事です。 2021.2 では連載の最後の記事になるかもしれませんが、この分野ではさらにいくつかの機能強化が行われています。 この記事では、このリリースで収集し始めた**ヒストグラム**という追加のテーブル統計について、もう少し詳しく説明します。
### ヒストグラムとは?
ヒストグラムは数値フィールド(またはより広範には、厳密な順序を持つデータ)のデータ分布の近似表現です。 このようなフィールドの最小値、最大値、および平均値がわかれば役立ちますが、データが 3 つのポイント間でどのように分布しているかはほとんどわかりません。 ここで役立つのがヒストグラムです。値の範囲をバケットに分割し、バケットごとに出現するフィールド値の数をカウントします。
これは非常に柔軟な定義であるため、バケットがフィールド値に関して同じ「幅」になるように、またはカバーされるサンプル値の数に関して同じ「大きさ」になるように、バケットのサイズを選択することができます。 後者の場合、各バケットには同じパーセンテージの値が含まれるため、バケットはパーセンタイルを表します。 以下のグラフは、日数で表現された同じバケット幅を使用して、[Aviation Demo データセット](https://github.com/intersystems/Samples-Aviation)の EventData フィールドのヒストグラムをプロットしています。
### ヒストグラムが必要な理由
カリフォルニア州で 2004 年より前のすべてのイベントについて、このデータセットのクエリを実行しているとします。
SELECT * FROM Aviation.Event WHERE EventDate < '2004-05-01' AND LocationCountry = 'California'
「[ランタイムプランの選択](https://jp.community.intersystems.com/node/510746)」という前の記事では、テーブル統計で LocationCounty のようなフィールドの選択性と潜在的な外れ値をキャプチャする方法についてすでに説明しています。 しかし、そのような個別のフィールド値の統計は、EventDate での `<` 条件ではあまり実用的ではありません。 この条件の選択制を計算するには、2004 年 5 月 1 日までのすべての潜在的な EventDate 値の選択制を集計する必要があり、クエリのプランニング時に行えるような手っ取り早い見積もりではなく、それだけで非常に厳しいクエリとなる可能性があります。 ここで使用できるのがヒストグラムです。
EventDate 値の分布のヒストグラムデータを見てみましょう。今回は、データを同じサイズの 16 個のバケットに分割し、各バケットには 6.667% のデータが保持されています。 このようにすると、クエリコストの見積もりに使用できるパーセンタイルと選択制の数値に簡単に変換できます。 このテーブルを読み取るために、4 行目を見てみましょう。値の 20%(各 6.667% の 3 つのバケット)がこのバケットの下限である 2003 年 6 月 22 日より前にあり、さらに 6.667% の値が 2003 年 9 月 19 日まで保持されています。
<colgroup><col style="width:48pt" width="64"><col style="width:61pt" width="81"><col style="width:64pt" width="85"></colgroup>
Bucket
Percentile
Value
0%
21/12/2001
1
7%
02/07/2002
2
13%
19/01/2003
3
20%
22/06/2003
4
27%
19/09/2003
5
33%
30/12/2003
6
40%
01/10/2004
7
47%
01/10/2005
8
53%
20/08/2006
9
60%
14/01/2007
10
67%
02/04/2008
11
73%
14/05/2008
12
80%
29/11/2008
13
87%
01/06/2010
14
93%
30/10/2011
15
100%
26/09/2012
上記のクエリ例で使用されているカットオフ日(2004 年 5 月 1 日)は、5 番目のバケットにあり、その日付より前には 33% から 40% の値があります。 バケットが小さくなるにつれ、その_中_の分布はほぼ均一であると見なすことができ、下限と上限の間を単に補完することができます。つまり、この場合、選択性は約 37% となり、これをクエリコストの見積もりに使用することができます。
ヒストグラムの使用を可視化するには、もう一つ、累積分布グラフとしてプロットする方法があります。 X 軸で 2004 年 5 月 1 日の線(値)がどのように描かれるかを確認すれば、Y 軸で 約 37% と解釈できます。
上記の例では、わかりやすくするために上限のみの範囲条件を使用していますが、このアプローチは、下限または間隔条件(`BETWEEN` 句を使用するなど)を使用しても当然動作します。
2021.2 より、文字列を含むすべての照合フィールドのテーブル統計の一環としてヒストグラムを収集しており、それを使用して RTPC の一部として範囲選択性を推定することができるようになっています。 実世界での多くのクエリには日付(およびその他の)フィールドでの範囲条件が伴うため、この IRIS SQL の機能強化によって、多くのお客様のクエリプランに役立つと信じています。いつものように、皆さんの体験をお聞かせください。
記事
Mihoko Iijima · 2023年5月26日
これは InterSystems FAQ サイトの記事です。
IRISではジャーナルファイルが自動的に圧縮される仕組みが導入されています。
ジャーナルファイルの圧縮機能について詳しくは、別の記事「ジャーナル圧縮機能について」をご参照ください。
例えば、CachéからIRISへ移行された後に、念のためIRISで更新されたデータを手動でCachéにも反映させたいことばある場合に、IRISのジャーナルファイルをCachéにリストアすることができます。
手順は以下の通りです。
(手順1) IRISのジャーナルファイル(YYYYMMDD.nnnz) ファイルを解凍する(手順2,3) 解凍した ジャーナルファイルを Cachéに転送してリストアする
リストアの方法として、以下の2パタンをご紹介
(A) 指定グローバルとデータベースについて、指定ジャーナルから、全エントリをリストア(B) 指定グローバルとデータベースについて、指定ジャーナルから、特定のアドレスまでリストアする
(A) 指定グローバルとデータベースについて、指定ジャーナルから、全エントリをリストア
(手順1) IRISサーバ上で以下のコマンドにより、ジャーナルを解凍する
IRIS 2022.1 以降、現在実行中のジャーナル以外は、拡張子 z で圧縮されています。
以下のコマンドで解凍し、指定のフォルダにコピーします。
これをCachéにリストアしたいジャーナルファイルすべてに対して繰り返し実行してください。
例)
set j1="/usr/irissys/mgr/journal/20230413.002z"
set j2="/temp/20230413.002"
write ##class(%SYS.Journal.File).Decompress(j1,j2)
set j1="/usr/irissys/mgr/journal/20230413.003z"
set j2="/temp/20230413.003"
write ##class(%SYS.Journal.File).Decompress(j1,j2)
以下の実行例では、リストア対象データを作成しています。
USER>set ^abc="Dummyデータ"
USER>set ^test1="USERデータベースで登録( I R I S )"
USER>set $namespace="test1"
TEST1>set ^Dummy=11000
TEST1>set ^DummyXX=2000
USERネームスペースはUSERデータベースにグローバルがセットされ、USERデータベースディレクトリは「/usr/irissys/mgr/user/」です。
TEST1ネームスペースはTEST1データベースにグローバルがセットされ、TEST1データベースディレクトリは「/usr/irissys/mgr/test1/」です。
(手順2) 手順1で解凍した、リストアしたいジャーナルを、すべて Cachéサーバに転送します。
(手順3) Cachéサーバ上で、上記コピーしてきたジャーナルを、連続してリストアします。
IRISからコピーして /temp に置いた 20230413.002 と 20230413.003 ファイルを以下のルールでリストアするように指定します。
[IRIS]/usr/irissys/mgr/user/ → [Cache]/usr/cachesys/mgr/user/ : ^abc と ^test1をリストア対象に指定する
[IRIS]/usr/irissys/mgr/test1/ → [Cache]/usr/cachesys/mgr/test1/ : ^Dummy と ^DummyXXをリストア対象に指定する
実行コードは以下の通りです。
注意:Windows以外のOSにリストアする場合、データベースディレクトリの末尾にパスのマーク(/)を必ず入れて下さい。
//ジャーナルリストアに利用するクラスのインスタンスを生成
set jrn=##class(Journal.Restore).%New()
//リストア開始ファイルの指定
set jrn.FirstFile="20230413.002"
//リストア終了ファイルの指定
set jrn.LastFile ="20230413.003"
//ジャーナルファイルの配置場所
do jrn.UseJournalLocation("/home/isjedu/irisjrn/")
//カレントインスタンスのジャーナルログファイルを使うかどうか(-1は使わない)
set jrn.JournalLog=-1
//ジャーナルリストア終了時、コミットされてないトランザクションをロールバックするかどうか
set jrn.RollBack=0
//1つ目のデータベース
set src(1)="/usr/irissys/mgr/user/"
set to(1)="/usr/cachesys/mgr/user/"
//第1引数:IRISのデータベースのデータベースディレクトリ
//第2引数:リストア対象グローバル変数名(前方一致可)
//IRISのUSERデータベースにある ^abcと、^iijimaをリストア
do jrn.SelectUpdates(src(1),"abc") //^abc
do jrn.SelectUpdates(src(1),"test1") //^test1
//ジャーナルリストア対象データベースディレクトリのリダイレクト指定
//第1引数:ジャーナルに記録されているIRISのデータベースディレクトリ
//第2引数:リストア対象のCacheのデータベースディレクトリ
set status=jrn.RedirectDatabase(src(1),to(1))
//もしエラーがあったらエラー情報を出力して終了
if $$$ISERR(status) {
write $system.Status.GetErrorText(status)
quit
}
//2つ目のデータベース
set src(2)="/usr/irissys/mgr/test1/"
set to(2) ="/usr/cachesys/mgr/test1/"
do jrn.SelectUpdates(src(2),"Dummy*") ;; ^Dummy ^DummyXX
set status=jrn.RedirectDatabase(src(2),to(2))
if $$$ISERR(status) {
write $system.Status.GetErrorText(status)
quit
}
//n個目のデータベース
/*
set src(n)="..(IRISのDBフォルダ名).."
set to(n) ="..(CacheのDBフォルダ名).."
do jrn.SelectUpdates(src(n),"xxx*") // ^xxx
do jrn.SelectUpdates(src(n),"yyy") // ^yyy
set status=jrn.RedirectDatabase(src(n),to(n))
if $$$ISERR(status) {
write $system.Status.GetErrorText(status)
quit
}
*/
//リストア実行
set status=jrn.Run()
if $$$ISERR(status) {
write $system.Status.GetErrorText(status)
quit
}
(B) 指定グローバルとデータベースについて、指定ジャーナルから、特定のアドレスまでリストア
(手順1)(手順2)は、(A)と同様です。
(手順3) Cachéサーバ上で、上記コピーしてきたジャーナルを、連続してリストアします。
指定アドレス以降はリストアしない、というフィルタルーチンを、Caché サーバの %SYS に作成します。ルーチンの引数名はそのままにしておいてください。
作成詳細は、別記事「誤って削除したグローバルを復旧させる方法」の実施例以下をご覧ください。
%SYSネームスペースにルーチン名:ZJRNFILT で作成します。
例)
ZJRNFILT(pid,dir,glo,type,restmode,addr,time)
set restmode=1
// グローバル変数名に TEST1 が含まれいる
// かつ ジャーナルレコードが9060180以降は restmode=0 に設定(=リストアしない)
if (glo [ "TEST1") & (addr>=9060180) {
set restmode=0
}
quit
(A)の手順のRun()メソッド実行の直前にフィルタルーチンの名称を設定する1文を加えるだけです。
//実行
set jrn.Filter="^ZJRNFILT" ;; <--- フィルタルーチン指定
set status=jrn.Run()
実行すると、ジャーナルをリストア完了後、以下のように2回聞かれます。
Do you want to rename your journal filter?
Do you want to delete your journal filter?
どちらも N と入力して、リターンしてください。