クリアフィルター
記事
Toshihiko Minamoto · 2022年7月26日
 [Jupyter Notebook](https://jupyter.org/) は、多数の異なるマークアップ言語とプログラミング言語でコードを実行できるセルで構成された対話型環境です。
Jupyter はこれを実現するために適切なカーネルに接続しなければなりませんが、 ObjectScript カーネルがなかったため、それを作成することにしました。
[こちら](objectscriptkernel.eastus.cloudapp.azure.com)から試すことができます。
結果を少し覗いてみましょう。

## Jupyter カーネルの基礎
[Jupyter カーネル](https://jupyter-client.readthedocs.io/en/stable/kernels.html)はいくつかの方法で作成できます。 ここでは、Python ラッパーカーネルを作成することにしましょう。
`ipykernel.kernelbase.Kernel` のサブクラスを作成して、特定の言語で実行されるコードを受け取る `do_execute` メソッドを実装する必要があります。
つまり、ある ObjectScript コードを取得して、何らかの方法で実行し、ノートブックにその結果を返すという概念です。
でも、実際にはどうすればよいのでしょうか。 では、その方法をさらに噛み砕いて説明しましょう。
## ObjectScript コードを IRIS に送る
まず初めに、コードを IRIS に送る必要があります。 ここで使用するのが、[Python 用の IRIS Native API](https://irisdocs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=PAGE_PYTHON_NATIVE) です。
ここでは、`irisnative` パッケージをインポートして、接続を確立するだけです。
```
def get_iris_object():
# InterSystems IRIS への接続を作成する
connection = irisnative.createConnection('iris', 51773, 'IRISAPP', '_SYSTEM', 'SYS')
# iris オブジェクトを作成する
return irisnative.createIris(connection)
```
その後で、この接続を使用して、IRIS データベースに格納されているクラスを呼び出すことができます。
```
def execute_code(self, code):
class_name = "JupyterKernel.CodeExecutor"
return self.iris.classMethodValue(class_name, "CodeResult", code)
```
`CodeExecutor` クラスと `CodeResult` メソッドは何に使用されているのでしょうか。
ではそれを見てみましょう。
## ObjectScript コードを実行する
このクラスの目的は、1 行の ObjectScript コードを実行して、実行の結果を含む JSON オブジェクトを返すことです。 コードを `CodeResult` の `vstrCommand` 変数に渡します。
まず、IO を現在のルーチンにリダイレクトします。その後、渡されたコードを [XECUTE](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_cxecute) コマンドで実行し、IO を元の場所にリダイレクトして、結果を返します。
```
Include %sySystem
Class JupyterKernel.CodeExecutor
{
ClassMethod CodeResult(vstrCommand As %String) As %String [ ProcedureBlock = 0 ]
{
set tOldIORedirected = ##class(%Device).ReDirectIO()
set tOldMnemonic = ##class(%Device).GetMnemonicRoutine()
set tOldIO = $io
try {
set str=""
set status = 1
//IO を現在のルーチンにリダイレクトする。以下に定義するラベルを利用します
use $io::("^"_$ZNAME)
//リダイレクトを有効にする
do ##class(%Device).ReDirectIO(1)
XECUTE (vstrCommand)
} catch ex {
set str = ex.DisplayString()
set status = 0
}
//元のリダイレクト/ニーモニックルーチン設定に戻す
if (tOldMnemonic '= "") {
use tOldIO::("^"_tOldMnemonic)
} else {
use tOldIO
}
do ##class(%Device).ReDirectIO(tOldIORedirected)
quit {"status":(status), "out":(str)}.%ToJSON()
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
output(s)
set str = str _ s
quit
}
}
```
## 結果を表示する
ObjectScript コードを実行しましたが、次はどうすればよいでしょうか。 その結果を表示する必要があります。
例外がなければ、行単位で結果を表示するだけで済みます。
が、渡されたコードで例外が発生したのであれば、実行を停止し、失敗した行番号、行そのもの、そして発生した例外を表示しなければなりません。

## アプリを起動する
このカーネルを自分で試してみましょう。以下のようにして試すことができます。
## 前提条件
[git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) と [Docker](https://www.docker.com/products/docker-desktop) がインストール済みであることを確認してください。
リポジトリを、以下のようにローカルディレクトリに Clone/git pull します。
```
$ git clone https://github.com/Vekkby/objectsriptkernel.git
```
このディレクトリでターミナルを開き、以下を実行します。
```
$ docker-compose up -d --build
```
## 操作方法
ブラウザから以下を使用してノートブックサーバーにアクセスできます。
```
localhost:8888
```
'work' ディレクトリ内に 'hello.ipynb' というサンプルノートブックがあります。 
### 投票をお願いします
このアプリは、IRIS Native API コンテストに参加しています。 こちら からこのアプリに投票してください。
記事
Toshihiko Minamoto · 2022年12月16日
母体リスクは、医学界でよく知られているいくつかのパラメーターから測定できます。 この測定により、医学界とコンピューター化されたシステム(特に AI)を支援すべく、科学者である Yasir Hussein Shakir は、母体リスクの検出/予測における ML アルゴリズムをトレーニングするための非常に便利なデータセットを公開しました。 このデータセットは、ML の最大級のデータリポジトリとして最もよく知られている Kaggle に公開されています。
https://www.kaggle.com/code/yasserhessein/classification-maternal-health-5-algorithms-ml
## データセットについて
妊娠中と出産後の母体のヘルスケアに関する情報の不足により、妊娠中の女性の多くは、妊娠に関わる問題で死亡しています。 これは、農村地域や新興国の下位中流家庭の間でより一般的に起きている問題です。 妊娠中は、状態を絶えず観察することで、胎児の適切な成長と安全な出産を保証する必要があります(出典: https://www.kaggle.com/code/yasserhessein/classification-maternal-health-5-algorithms-ml)。
データは、IoT ベースのリスク監視システムを通じて、様々な病院、地域の診療所、妊産婦ヘルスケアから収集されています。
* Age: 女性が妊娠したときの年齢
* SystolicBP: 最高血圧(mmHg)。妊娠中に重要な属性の 1 つ。
* DiastolicBP: 最低血圧(mmHg)。妊娠中に重要な属性の 1 つ。
* BS: モル濃度(mmol/L)による血糖値。
* HeartRate: 1 分あたりの通常の安静時心拍数。
* Risk Level: 前の属性を考慮した妊娠中の予測リスク強度レベル。
## Kaggle から母体リスクデータを取得する
Kaggle の母体リスクデータは、Health-Dataset アプリケーション(https://openexchange.intersystems.com/package/Health-Dataset)を使って IRIS テーブルに読み込めます。 これを行うには、module.xml プロジェクトから依存関係(Health Dataset 用の ModuleReference)を設定します。
Health Dataset アプリケーションリファレンスを含む Module.xml
<?xml version="1.0" encoding="UTF-8"?>
<Export
generator="Cache" version="25">
<Document name="predict-diseases.ZPM">
<Module>
<Name>predict-diseases</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
<SourcesRoot<src/iris</SourcesRoot>
<Resource Name="dc.predict.disease.PKG"/>
<Dependencies>
<ModuleReference>
<Name>swagger-ui</Name>
<Version>1.*.*</Version>
</ModuleReference>
<ModuleReference>
<Name>dataset-health</Name>
<Version>*</Version>
</ModuleReference>
</Dependencies>
<CSPApplication
Url="/predict-diseases"
DispatchClass="dc.predict.disease.PredictDiseaseRESTApp"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="1"
UnauthenticatedEnabled="1"
Recurse="1"
UseCookies="2"
CookiePath="/predict-diseases"
/>
<CSPApplication
CookiePath="/disease-predictor/"
DefaultTimeout="900"
SourcePath="/src/csp"
DeployPath="${cspdir}/csp/${namespace}/"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="0"
Recurse="1"
ServeFiles="1"
ServeFilesTimeout="3600"
UnauthenticatedEnabled="1"
Url="/disease-predictor"
UseSessionCookie="2"
/>
</Module>
</Document>
</Export>
## 母体リスクを予測するための Web フロントエンドとバックエンドのアプリケーション
Open Exchange アプリのリンク(https://openexchange.intersystems.com/package/Disease-Predictor)に移動し、以下の手順に従います。
* リポジトリを任意のローカルディレクトリに Clone/git pull します。
$ git clone https://github.com/yurimarx/predict-diseases.git
* このディレクトリで Docker ターミナルを開き、以下を実行します。
$ docker-compose build
* IRIS コンテナを実行します。
$ docker-compose up -d
* AI モデルをトレーニングするための Execute Query into Management Portal(http://localhost:52773/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=USER)に移動します。
* トレーニングに使用するビューを作成します。
CREATE VIEW MaternalRiskTrain AS SELECT BS, BodyTemp, DiastolicBP, HeartRate, RiskLevel, SystolicBP, age FROM dc_data_health.MaternalHealthRisk
* ビューを使用して AI モデルを作成します。
CREATE MODEL MaternalRiskModel PREDICTING (RiskLevel) FROM MaternalRiskTrain
* モデルをトレーニングします。
TRAIN MODEL MaternalRiskModel
* [http://localhost:52773/disease-predictor/index.html](http://localhost:52773/disease-predictor/index.html) に移動し、Disease Predictor フロントエンドを使用して、以下のように疾患を予測します。

## 背後の処理
### 母体リスク疾患を予測するためのバックエンドのクラスメソッド
InterSystems IRIS では、前に作成されたモデルを使って、SELECT の実行により予測することができます。
母体リスク疾患を予測するためのバックエンドのクラスメソッド
/// 母体リスクの予測
ClassMethod PredictMaternalRisk() As %Status
{
Try {
Set data = {}.%FromJSON(%request.Content)
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Set qry = "SELECT PREDICT(MaternalRiskModel) As PredictedMaternalRisk, "
_"age, BS, BodyTemp, DiastolicBP, HeartRate, SystolicBP "
_"FROM (SELECT "_data.BS_" AS BS, "
_data.BodyTemp_" As BodyTemp, "
_data.DiastolicBP_" AS DiastolicBP, "
_data.HeartRate_" AS HeartRate, "
_data.SystolicBP_" As SystolicBP, "
_data.Age_" AS age)"
Set tStatement = ##class(%SQL.Statement).%New()
Set qStatus = tStatement.%Prepare(qry)
If qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
Set rset = tStatement.%Execute()
Do rset.%Next()
Set Response = {}
Set Response.PredictedMaternalRisk = rset.PredictedMaternalRisk
Set Response.Age = rset.Age
Set Response.SystolicBP = rset.SystolicBP
Set Response.DiastolicBP = rset.DiastolicBP
Set Response.BS = rset.BS
Set Response.BodyTemp = rset.BodyTemp
Set Response.HeartRate = rset.HeartRate
Write Response.%ToJSON()
Return 1
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return
}
}
これで、どの Web アプリケーションもこの予測を使用して、結果を表示できるようになりました。 predict-diseases アプリケーションのソースコードは、frontend フォルダをご覧ください。
記事
Megumi Kakechi · 2022年12月1日
複数のインスタンス間でライセンスを共有する際に、ライセンスサーバを立ててライセンスの使用量を管理します。IRISライセンスサーバには、ライセンスの使用量管理に加えて便利な新しい機能が追加されました。
-- ライセンスサーバでできること --1. マルチサーバライセンス(共有ライセンス)の統合管理(使用量の管理)2. 各インスタンスへのライセンスキーの配布・管理【New】
1 は従来からのライセンスサーバの機能で、関連記事 にて機能紹介をしております。
2 はIRIS以降使用できるようになった新しい機能です。
複数のインスタンスを構成している場合、中央管理しているディレクトリに格納されているライセンスキーファイル(*.key)を各インスタンスに配布・管理するようにライセンスサーバを構成することができます。その場合、個々のインスタンスにライセンスキー(iris.key)を配置する必要はありません。ライセンスキーはユニークな LicenseID (※1, ※2) によって識別され、各インスタンスの起動時にロード&有効化されます。
※1 ライセンスキーをテキストエディタで開くと、[ConfigFile] セクションにて設定されている LicenseID を確認できます。※2 ライセンスキーに記載されたLicenseIDの値を各インスタンスで事前に設定します(方法詳細は後述)。 ライセンス反映時、ライセンスキーに記載されたLicenseIDと各インスタンスの設定値を照らし合わせ、一致した場合にキーが各インスタンスにロードされます。
【設定方法】1.ライセンスサーバを構成するインスタンスで KeyDirectory プロパティを設定します。
KeyDirectory で指定したディレクトリ内に、すべての ライセンスキーを配置します。 複数のライセンスキーを配置する場合は、LicenseID で区別するようにします(同じ LicenseID のキーは複数配置できません)。 KeyDirectoryを設定している場合、起動時にそのインスタンスで見つかった有効な *.key ファイルをすべて読み取り、ライセンスサーバに送信します。 ライセンスサーバの設定は、管理ポータルで行います。
管理ポータル: [システム管理] > [ライセンス] > [ライセンス・サーバ]
2.各インスタンスで LicenseID プロパティ(キーファイルの新しいプロパティ)を 設定します。
各インスタンスは LicenseID プロパティを使用して、起動時にライセンスサーバからライセンスキーを要求することができます。 LicenseID が設定されていると、インスタンス起動時に指定したLicenseID のライセンスキーがライセンスサーバーよりロードされて有効化されます。 インスタンスは定期的に、期限切れのキーまたはアップグレードされた新しいキーがあるかどうかを確認します。 LicenseID の設定は、管理ポータルで行います。
管理ポータル: [システム管理] > [構成] > [追加設定] > [開始]:LicenseID
もしくは、構成ファイル(iris.cpf)で直接設定することも可能です 。CPF の [Startup] セクションの LicenseID を設定します。
ライセンスサーバはディレクトリからキーファイルを手動で再ロードしてライセンスを更新することも可能です(※3)。
※3 %SYS ネームスペースで do ReloadKeys^%SYS.LICENSE を実行します。 ライセンスの種類によっては、各インスタンスでインスタンスの再起動が求められる場合があります。
ライセンスサーバーを起動するインスタンスは、LicenseIDが異なる複数のキーをロードすることができます。ライセンスキーは、一意の LicenseID により各インスタンス上で識別されることになります。
コンテナを使用している場合は、すべてのコンテナライセンスを外部ディレクトリ(KeyDirectory プロパティで設定)に配置することもできます。そうすることで、最終的にコンテナ (および /mgr) 内にライセンスファイルを置かないようにすることが可能となります。
詳細は以下のドキュメントをご覧ください。InterSystems IRIS ライセンスの管理
【関連】複数インスタンスでライセンスを共有する場合に必要な設定
記事
Tomoko Furuzono · 2020年9月15日
Cachéデータベースのオブジェクトおよびリレーショナルデータモデルは、標準、ビットマップ 、ビットスライスの3種類のインデックスをサポートします。 これら3つのネイティブタイプに加えて、開発者は独自のカスタムタイプのインデックスを宣言し、バージョン2013.1以降の任意のクラスで使用できます。 たとえば、iFindテキストインデックスは、そのメカニズムを使用しています。
カスタムインデックスタイプは、挿入、更新、削除を実行するための%Library.FunctionalIndexインターフェースのメソッドを実装するクラスです。 新しいインデックスを宣言するときに、そのようなクラスをインデックスタイプとして指定できます。
例:
Property A As %String;Property B As %String;Index someind On (A,B) As CustomPackage.CustomIndex;
CustomPackage.CustomIndex クラスは、カスタムインデックスを実装するまさにそのクラスです。
たとえば、ハッカソン中に私たちのチーム(Andrey Rechitsky 、 Aleksander Pogrebnikov、そして私)が開発した空間データのクワッドツリーベースのインデックスの小さなプロトタイプを分析してみましょう。 (ハッカソンは、InterSystems Russia Innovation School Training(インターシステムズ・ロシア・イノベーション・スクール・トレーニング)で毎年開催されます。ハッカソンの主な閃きとなったTimur Safin氏に感謝いたします。)
この記事では、クワッドツリーとそれらの操作方法については説明しません。 代わりに、既存のクワッドツリーアルゴリズム実装のための%Library.FunctionalIndexインターフェースを実装する新しいクラスを作成する方法を検討してみましょう。 私たちのチームでは、このタスクはAndreyに割り当てられました。 Andeyは、次の2つの方法で SpatialIndex.Indexerクラスを作成しました。
Insert(x, y, id)
Delete(x, y, id)
SpatialIndex.Indexerの新しいインスタンスを作成するとき、インデックスデータを格納するためのグローバルノード名を定義する必要がありました。
InsertIndex、 UpdateIndex 、DeleteIndex 、 PurgeIndex メソッドを使って SpatialIndex.Indexのクラスを作成するだけで済みました。 最初の3つのメソッドは、変更する文字列のIdを受け入れ、インデックス付きの値は、対応するクラス内のインデックス宣言で定義されているのとまったく同じ順序です。 この例では、入力引数はpArg(1) — A and pArg(2) — Bです。
Spoiler
Class SpatialIndex.Index Extends %Library.FunctionalIndex [ System = 3 ]{ClassMethod InsertIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)") } }ClassMethod UpdateIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Delete(pArg(3),pArg(4),pID)") $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)") } }ClassMethod DeleteIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Delete(pArg(1),pArg(2),pID)") } }ClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"kill " _ IndexGlobal) } }ClassMethod IndexLocation(className As %String, indexName As %String) As %String { set storage = ##class(%Dictionary.ClassDefinition).%OpenId(className).Storages.GetAt(1).IndexLocation quit $Name(@storage@(indexName)) }}
IndexLocation は、インデックス値が保存されているグローバル内のノードの名前を返す補足メソッドです。
ここで、 SpatialIndex.Indexタイプのインデックスが使われているテストクラスを分析しましょう。
Class SpatialIndex.Test Extends %Persistent{Property Name As %String(MAXLEN = 300);Property Latitude As %String;Property Longitude As %String; Index coord On (Latitude, Longitude) As SpatialIndex.Index;}
SpatialIndex.Testクラスがコンパイルされている場合、システムは、SpatialIndex.Indexの各インデックスのINTコードで次のメソッドを生成します。
zcoordInsertIndex(pID,pArg...) public {set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))do indexer.Insert(pArg(1),pArg(2),pID) }zcoordPurgeIndex() public {kill ^SpatialIndex.TestI("coord") }zcoordSegmentInsert(pIndexBuffer,pID,pArg...) public {do ..coordInsertIndex(pID, pArg...) }zcoordUpdateIndex(pID,pArg...) public {set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))do indexer.Delete(pArg(3),pArg(4),pID)do indexer.Insert(pArg(1),pArg(2),pID) }
%SaveData 、%DeleteData 、%SQLInsert 、%SQLUpdate および%SQLDelete メソッドはインデックス内のメソッドを呼び出します。 たとえば、次のコードは%SaveDataメソッドの一部です。
if insert { ... do ..coordInsertIndex(id,i%Latitude,i%Longitude,"") ... } else { ... do ..coordUpdateIndex(id,i%Latitude,i%Longitude,zzc27v3,zzc27v2,"") ... }
実際の例は常に理論よりも優れているため、次のリポジトリからファイルをダウンロードできます: https://github.com/intersystems-ru/spatialindex/tree/no-web-interface 。 これは、Web UIのないブランチへのリンクです。 このコードを使用するには、以下を行います。
クラスをインポートする
RuCut.zipを解凍する
次の呼び出しを使用してデータをインポートする:
do $system.OBJ.LoadDir("c:\temp\spatialindex","ck")do ##class(SpatialIndex.Test).load("c:\temp\rucut.txt")
rucut.txtファイルには、ロシアの10万の都市や町に関するデータとその名前と座標が含まれています。 Loadメソッドは各ファイル文字列を読み取り、それをSpatialIndex.Testクラスの個別のインスタンスとして保存します。 Loadメソッドが実行されると、グローバル^ SpatialIndex.TestI( 「coord」)には、緯度と経度の座標を持つクワッドツリーが含まれます。
では、クエリを実行しましょう!
インデックスの構築は、一番おもしろい部分ではありません。 さまざまなクエリでインデックスを使用します。 Cachéには、非標準インデックスの標準構文があります。
SELECT *FROM SpatialIndex.TestWHERE %ID %FIND search_index(coord, 'window', 'minx=56,miny=56,maxx=57,maxy=57')
%ID %FIND search_index は構文の固定部分です。 次に、インデックス名coordがあります。引用符は必要ありません。 他のすべてのパラメーター( 'window'、 'minx = 56、miny = 56、maxx = 57、maxy = 57')は Findメソッドに渡されます。これもインデックスタイプのクラスで定義する必要があります(この例では、 SpatialIndex.Indexです )。
ClassMethod Find(queryType As %Binary, queryParams As %String) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ]{ if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) set IndexGlobalQ = $$$QUOTE(IndexGlobal) $$$GENERATE($C(9)_"set result = ##class(SpatialIndex.SQLResult).%New()") $$$GENERATE($C(9)_"do result.PrepareFind($Name("_IndexGlobal_"), queryType, queryParams)") $$$GENERATE($C(9)_"quit result") }}
このコードサンプルでは、パラメーターは2つしかありません。 queryType と queryParams ですが、必要な数のパラメータを自由に追加できます。
SpatialIndex.Indexが使われているクラスをコンパイルする場合、 Find メソッドは、 z <IndexName> Findと呼ばれる補足メソッドを生成しますが 、これはSQLクエリの実行に使用されます。
zcoordFind(queryType,queryParams) public { s:'$isobject($g(%sqlcontext)) %sqlcontext=##class(%Library.ProcedureContext).%New()set result = ##class(SpatialIndex.SQLResult).%New()do result.PrepareFind($Name(^SpatialIndex.TestI("coord")), queryType, queryParams)quit result }
Findメソッドは、%SQL.AbstractFindインターフェースを実装するクラスのインスタンスを返す必要があります。 このインターフェイスのメソッド、 NextChunk および PreviousChunk は、それぞれ64,000ビットのチャンクでビット文字列を返します。 特定のIDのレコードが選択基準を満たし、対応するビット(chunk_number * 64000 + position_number_within_chunk)が1に設定されます。
Spoiler
Class SpatialIndex.SQLResult Extends %SQL.AbstractFind{Property ResultBits [ MultiDimensional, Private ];Method %OnNew() As %Status [ Private, ServerOnly = 1 ] { kill i%ResultBits kill qHandle quit $$$OK }Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status { if queryType = "window" { for i = 1:1:4 { set item = $Piece(queryParams, ",", i) set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"kill " _ IndexGlobal) set param = $Piece(item, "=", 1) set value = $Piece(item, "=" ,2) set arg(param) = value } set qHandle("indexGlobal") = indexGlobal do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy")) set id = "" for { set id = $O(qHandle("data", id),1,idd) quit:id="" set tChunk = (idd\64000)+1, tPos=(idd#64000)+1 set $BIT(i%ResultBits(tChunk),tPos) = 1 } } quit $$$OK } Method ContainsItem(pItem As %String) As %Boolean{ set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1 quit $bit($get(i%ResultBits(tChunk)),tPos)}Method GetChunk(pChunk As %Integer) As %Binary { quit $get(i%ResultBits(pChunk)) }Method NextChunk(ByRef pChunk As %Integer = "") As %Binary { set pChunk = $order(i%ResultBits(pChunk),1,tBits) quit:pChunk="" "" quit tBits }Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary { set pChunk = $order(i%ResultBits(pChunk),-1,tBits) quit:pChunk="" "" quit tBits }}
Class SpatialIndex.SQLResult Extends %SQL.AbstractFind{Property ResultBits [ MultiDimensional, Private ];Method %OnNew() As %Status [ Private, ServerOnly = 1 ]{ kill i%ResultBits kill qHandle quit $$$OK}Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status{ if queryType = "window" { for i = 1:1:4 { set item = $Piece(queryParams, ",", i) set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"kill " _ IndexGlobal) set param = $Piece(item, "=", 1) set value = $Piece(item, "=" ,2) set arg(param) = value } set qHandle("indexGlobal") = indexGlobal do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy")) set id = "" for { set id = $O(qHandle("data", id),1,idd) quit:id="" set tChunk = (idd\64000)+1, tPos=(idd#64000)+1 set $BIT(i%ResultBits(tChunk),tPos) = 1 } } quit $$$OK }Method ContainsItem(pItem As %String) As %Boolean{ set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1 quit $bit($get(i%ResultBits(tChunk)),tPos)}Method GetChunk(pChunk As %Integer) As %Binary{ quit $get(i%ResultBits(pChunk))}Method NextChunk(ByRef pChunk As %Integer = "") As %Binary{ set pChunk = $order(i%ResultBits(pChunk),1,tBits) quit:pChunk="" "" quit tBits}Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary{ set pChunk = $order(i%ResultBits(pChunk),-1,tBits) quit:pChunk="" "" quit tBits}}
上記のコードサンプルに示すように、 SpatialIndex.QueryExecutorクラスの InternalFindWindow メソッドは、指定された長方形内にあるポイントを検索します。 次に、一致する行のIDがFORループのビットセットに書き込まれます。
私たちのハッカソンプロジェクトでは、Andreyは楕円の検索機能も実装しました。
SELECT *FROM SpatialIndex.TestWHERE %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2')and name %StartsWith 'Z'
%FINDに関する補足情報
%FIND 述語には追加パラメーター SIZEがあり、これは、SQLエンジンが一致する行の数を推定するのに役立ちます。 SQLエンジンは、このパラメーターに基づいて%FIND述語で指定されたインデックスを使用するかどうかを決定します。
たとえば、次のインデックスをSpatialIndex.Testクラスに追加してみましょう。
Index ByName on Name;
次に、クラスを再コンパイルして、このインデックスを作成します。
write ##class(SpatialIndex.Test).%BuildIndices($LB("ByName"))
最後に、TuneTableを実行します。
do $system.SQL.TuneTable("SpatialIndex.Test", 1)
クエリプランは次のとおりです。
SELECT *FROM SpatialIndex.TestWHERE name %startswith 'za'and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((10))
coordインデックスは数行を返す可能性が高いため、SQLエンジンは Nameプロパティのインデックスを使用しません。
次のクエリには別のプランがあります。
SELECT *FROM SpatialIndex.TestWHERE name %startswith 'za'and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((1000))
SQLエンジンは、両方のインデックスを使用してこのクエリを実行します。
そして最後の例として、coordインデックスはおそらく約10万行を返すため、ほとんど使用できないので、 Nameフィールドのインデックスのみを使用するリクエストを作成しましょう。
SELECT *FROM SpatialIndex.TestWHERE name %startswith 'za'and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((100000))
この記事を読んでくださった方、または少なくとも最後までスクロールしてくださったすべての方に感謝いたします。
参考:
%FIND
search_index
%SQL.AbstractFind
%Library.FunctionalIndex
記事
Toshihiko Minamoto · 2021年7月1日
クラスのコンパイル時に、定義済みのプロパティ、クエリ、またはインデックスごとに対応する複数のメソッドが自動的に生成されます。 これらのメソッドは非常に便利です。 この記事では、その一部について説明します。
プロパティ
「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クエリまたはカスタムクラスクエリのどちらでも構いません。これに関する[記事](https://community.intersystems.com/post/class-queries-intersystems-cache%CC%81)をご覧ください)、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には、これらのメソッドを実演するデモクラス](https://github.com/eduard93/Utils/blob/master/Utils/GeneratedMethods.cls.xml)があります。
お知らせ
Shintaro Kaminaka · 2021年7月8日
開発者の皆さん、こんにちは!
HL7v2メッセージをFHIR(Fast Healthcare Interoperability Resources)に変換するニーズがあり、その変換プロセスが複雑で分かりにくいと感じたことはありませんか?インターシステムズは、HL7v2メッセージをFHIR(Fast Healthcare Interoperability Resources)に変換するプロセスを簡単にする、HealthShareメッセージ変換サービス(HealthShare Message Transformation Services)と呼ばれる新しいクラウドベースのSaaSサービスを展開しています。 この新しいサービスの早期アクセス・プレビュー・プログラムを発表できることを嬉しく思います。 必要なのは、無料のAWSアカウントと、HL7v2メッセージをドロップするためのS3バケット、そしてFHIR出力を得るための別のS3バケットだけです。
この機能の簡単なデモをご覧ください。
インターシステムズ社のラーニングサイトでは、AWS の無料アカウントとインターシステムズ社のクラウド・ポータルの無料アカウントにサインアップして、変換サービスの強力な機能を利用するための簡単な[ステップ・バイ・ステップ・ガイド](https://learning.intersystems.com/course/view.php?name=HMTSExerciseS3)を提供しています。 完全なドキュメントは、[インターシステムズ社のドキュメント](http://www.intersystems.com/hmts)でご覧いただけます。
このサービスは、7月下旬に正式に開始される予定です。プレビューが終了しても、最初の100万件の変換を無料で利用することができます。
### インターシステムズ社のこの新しいオファーの詳細は以下の内容です:
HealthShareメッセージ変換サービスの紹介。
医療IT業界では、FHIR® (Fast Healthcare Interoperability Resources)が、ヘルスケアデータを交換するための最新のデータ標準として採用されています。 オンデマンドのHealthShareメッセージ変換サービスを利用することで、医療機関、保険会社、製薬会社は、既存のデータフォーマットをFHIR規格に変換し、データから最大限の価値を引き出すことができます。 インターシステムズは、最新のFHIR規格だけでなく、HL7v2、X12、CDA、C-CDA、DICOMを含むすべての主要なヘルスケア規格を実装しており、ヘルスケアの相互運用性におけるリーダー的存在です。
HealthShareメッセージ変換サービスは、これらの以前の標準からFHIR R4へのメッセージ変換を簡単に行えるように設計されており、初期リリースではHL7 v2メッセージのFHIR R4への変換をサポートしています。 FHIRメッセージは、AWS S3バケットまたはAmazon HealthLake (Preview)に送ることができ、将来的には他のFHIRリポジトリオプションも追加される予定です。
HealthShareメッセージ変換サービスは、HL7v2メッセージをFHIRに変換することを簡単にします。 変換ロジックを気にする必要がないので、メッセージ変換の複雑な作業はインターシステムズに任せて、優れたヘルスケア・アプリケーションの構築に集中することができます。このサービスが提供するのは
- AWS上での簡単なプロビジョニングと起動
- インバウンドS3バケットのHL7v2メッセージのチェック
- HL7コンテンツのバリデーション
- FHIR R4へのメッセージ変換
- 変換されたメッセージを、アウトバウンドの S3 バケット、[InterSystems FHIR Accelerator (Preview) サービス](https://community.intersystems.com/post/new-video-what-fhir-accelerator-service)、または [Amazon HealthLake (Preview)](https://aws.amazon.com/jp/healthlake/) リポジトリにルーティングする
- 変換パイプラインのステータスと統計情報のモニタリング
さらに、このサービスはAWSのインフラ上に構築されているため、ISO 27001:2013およびHIPAAをサポートするHITRUST認証を取得しています。インターシステムズは、このサービスの運用、監視、バックアップを管理しています。
そして何より、このサービスが商業的に開始されると、最初の100万回の変換が無料で提供され、その後は使用した分だけを支払うことになり、変換されたメッセージあたりのコストは非常に低く抑えられます。 このサービスを利用するための長期契約はなく、いつでも解約できます。
皆様からのフィードバックをお待ちしております。 この記事のコメント欄にご意見をお寄せいただくか、 HMTS@InterSystems.com まで直接お問い合わせください。
HL7 ® と FHIR ® は Health Level Seven International の登録商標であり、これらの商標の使用は HL7 による推奨を意味するものではありません。FHIR商標の使用は、HL7によるHealthShare Message Transformation ServicesまたはHealthShare Message Transformation Servicesの推奨を意味するものではありません。
記事
Megumi Kakechi · 2021年8月25日
これは InterSystems FAQ サイトの記事です。
リレーションシップが設定されており 1対n の n が多量の場合、そのリレーションシップの順次処理などで大量のメモリ消費となるケースがあります。
プログラムの中で多側オブジェクトを参照し内部的にスウィズル処理した後には、そのOREFを含む変数の解放(削除、他の値の設定など)だけでは、その多側オブジェクトとリレーションシップオブジェクトが解放されないことが原因です。
それらを完全にメモリから解放するためには、OREF変数の解放とRelationshipオブジェクトの%UnSwizzleAt<%Library.RelationshipObject >メソッドの実行による明示的なメモリ解放が必要です。
- 使用例 -
Do {
Set employee = company.Employees.GetNext(.key)
If (employee '= "") {
Write employee.Name,!
// remove employee from memory
Do company.Employees.%UnSwizzleAt(key)
}
} While (key '= "")
多側オブジェクトが数個しかない場合は特に問題はありませんが、紐づいている数が大量な場合に、ループ文で連続アクセスした場合、その分大量にメモリにオブジェクトが展開され続けることになり、メモリー圧迫要因のひとつになります。
その様な状況を避けるために明示的な開放処理を入れる必要があります。
Kakechi さん
貴重な情報を公開いただきありがとうございます!Relationshipオブジェクトの理解が浅く申し訳ないのですが、一点だけ教えてください。
今回の場合、Relationshipオブジェクトをループで回した際にのみ事象が発生するという理解なのですが、Interoperabilityで利用可能なRecordMap利用時に生成される、Batchクラス内のRecordsプロパティも上記に該当するのでしょうか?
今私が携わるシステムではRecordMapを多用しており、数万件のデータをこの仕組みを用いて取り込みをおこなっているため、上記の事象が発生するのではないかと懸念しております。 Ohata様
こんにちは、ご質問ありがとうございます。Interoperabilityの場合、RecordMapのBatchサービスでは内部で対応しているため明示的に%UnSwizzleAt() する必要はありません。また、オペレーションについても UnSwizzleRecordsプロパティ(デフォルトはTrue:チェックあり)により、処理後にメモリから解放されるため、こちらについても明示的に %UnSwizzleAt() する必要はありません。UnSwizzleRecordsプロパティを 0 (False:チェックなし) に指定し大量のデータを処理する場合には、正しく開放処理を行わないと <STORE> エラーになる場合がありますのでご注意ください。
Kakechi さん
回答いただきありがとうございます!BSもBOも標準のまま利用するのであれば開放処理がシステム内で行われると理解しました。
私が作成しているシステムでは、BS/BP/BOをそれぞれ標準を継承してカスタマイズしたり、Batch.RecordをObjectScript内でループさせて利用するような処理もあるのですが、この場合には%UnSwizzleAt()が必要との理解でよろしいでしょうか?
例)BSでファイル検知を行い、RecordMapの仕組みを利用してファイルからBatchクラスを生成。↓BOに連携し、BatchクラスをObjectScript内のクラスに譲与。↓取得したBatch.Recordの内容をループで回しながら処理を実行。
このループの中では%UnSwizzleAt()が必要?もしくはInteroperabilityのプロセスで実行する処理の中では、BatchクラスのRecordの内容は勝手に開放される? Ohata様
継承しているBOのクラスが、EnsLib.RecordMap.Operation.Batch* のクラスである場合は、 UnSwizzleRecords プロパティをオーバーライドして変更していない限り、デフォルトの True(=1) となるため、明示的に %UnSwizzleAt() する必要はありません。
「BatchクラスをObjectScript内のクラスに譲与」というのが、例えば %Persistent などのユーザ作成クラスに対してである場合は、別途 %UnSwizzleAt() していただく必要があります。
どのような構成にしているかによって変わってくるところはありますので、詳細についてのご相談があれば弊社サポートセンター(jpnsup@intersystems.com)までお気軽にお問い合わせください。 Kakechi さん
回答いただきありがとうございます。内容理解できました!私のケースでは対応必要そうです。
相談に乗っていただきありがとうございました! Kakechi さん
回答いただきありがとうございます。内容理解できました!私のケースでは対応必要そうです。
相談に乗っていただきありがとうございました!
記事
Toshihiko Minamoto · 2022年3月8日
InterSystemsを使用してExcelファイルを生成する方法はたくさんあります。ZENレポートやIRISレポート(Logiレポートまたは正式にはJReportsと呼ばれるレポート)のほか、サードパーティのJavaライブラリを使用するなど、可能性はほぼ無限です。
_しかし、Caché ObjectScriptだけで単純なスプレッドシートを作成したい場合はどうでしょうか。 (サードパーティアプリケーションを使用せずに、です)_
私の場合、大量の生データを含むレポート(金融関係の人たちが好むレポート)を生成する必要がありますが、私のZEN/IRISでは対応できません。私が呼ぶところの「ゼロバイトファイル」が生成され、基本的にJavaのメモリ不足となり、レポーティングサーバーに大きな負荷を生じてしまいます。
これは、Office Open XML(OOXML)を使って実現できます。 Office Open XML形式は、多数のXMLファイルで構成されるZIPパケージです。 つまり基本的には、これらのXMLファイルを生成してZIP圧縮し、.xslxに名前を変更すればよいのです。 それくらい単純です。
ファイルは、Open Packaging Conventionsという単純な命名規則に従っています。 パーツのコンテンツタイプを宣言し、消費するアプリケーションにどこから開始するかを指示する必要があります。
単純なスプレッドシートを作成するには、少なくとも5つのファイルが必要です。
* workbook.xml
* worksheet.xml
* [Content_Types].xml
* styles.xml
* _rels
* .rels
* workbook.xml.rels
**workbook.xml**
workbookは、様々なワークシートをまとめるコンテナーです。 workbookでは、スタイルパーツ、共有文字列テーブル、およびスプレッドシートファイル全体に適用するその他の情報を参照できます。
ClassMethod GenerateWorkbookXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"workbook.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine(" ")
do stream.WriteLine(" ")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
**_rels/workbook.xml.rels**
workbook.xmlパーツからの参照に一致するように、rId1というIDを持つリレーションを作成する必要があります。
ClassMethod CreateRelsXML(){
set status =$$$OK
set isunix=$zcvt($p($zv," ",3,$l($p($zv," (")," ")),"U")["UNIX"
if isunix {
set ext="/"
}else{
set ext="\"
}
set xmlfile = fileDirectory_"_rels"_ext_"workbook.xml.rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
set xmlfile = fileDirectory_"_rels"_ext_".rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("
")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
**[Content_Types].xml**
静的ファイル(現時点では、ワークシートの数に応じた動的ファイル)はworkbookのワークシートとスタイルを紐づけます。Office Open XMLファイルごとに、ZIPパッケージ使用されるコンテンツタイプを宣言する必要があります。 これは、[Content_Types].xmlファイルで行います。
ClassMethod GenerateConntentTypesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"[Content_Types].xml"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
try{
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
**styles.xml**
すべてのフォーマットがこのファイルに含まれます。現時点では、静的スタイルが追加されています(より動的なworkbook固有のスタイルに変換する予定です)。
Excelスタイル
ID
スタイル
Excelフォーマット
1
デフォルト
テキスト
2
#;[Red]-#
数値
3
#.##;[Red]-#.##
数値
4
yyyy/mm/dd
日付
5
hh:mm
日付
6
ヘッダーと中央揃え
テキスト
7
ヘッダー2左寄せ
テキスト
8
良い(緑ハイライト)
全般
9
悪い(赤ハイライト)
全般
10
どちらでもない(オレンジハイライト)
全般
11
yyyy/mm/dd hh:mm
日付
ClassMethod CreateStylesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"styles.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine(" ")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.WriteLine("")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
**worksheet.xml**
このファイルに日付が含まれます。 シートの最初の行は列のタイトルです。 次の行には、最初の列にのみデータが含まれます。
デフォルトで列が自動調整されない場合は、ここで各列の列幅を定義します。
サンプルworksheet.xml
Name
Amount
Jhon Smith
1000.74
Tracy A
6001.74
サンプルExcel
.png)
_worksheet内の数式は、関数 タグ_を使って表現できます。
B2*0.08
B2+C2
そして最後にそれらをzip圧縮し、名前を.xlsxに変更します(unix zipを使用)。
set cmd ="cd "_fileDirectory_" && find . -type f | xargs zip .."_ext_xlsxFile
#### Excelドキュメントを生成します。
以下のサンプルコードは、Excelドキュメントを生成します。
set file = "/temp/test.xlsx"
set excelObj = ##class(XLSX.writer).%New(file)
do excelObj.SetWorksheetName("test1")
set status = excelObj.BeginWorksheet()
set row = 0
set row = row+1
;----------- excelObj.Cells(rowNumber,columnNumber,style,content)
set status = excelObj.Cells(row,1,1,"Header1")
set row = row+1
set status = excelObj.Cells(row,1,2,"Content 1")
set status = excelObj.EndWorksheet()
W !,excelObj.fileName
ExcelのWriterクラスは、こちらの[xlsx.writer.xml.zip](/sites/default/files/inline/files/xlsx.writer.xml.zip)にあります。
記事
Toshihiko Minamoto · 2023年3月10日
糖尿病は、医学会でよく知られるいくつかのパラメーターから発見することが可能です。 この測定により、医学界とコンピューター化されたシステム(特に AI)を支援すべく、(米)国立糖尿病・消化器・腎疾病研究所(NIDDK)は、糖尿病の検出/予測における ML アルゴリズムをトレーニングするための非常に便利なデータセットを公開しました。 このデータセットは、ML の最大級のデータリポジトリとして最もよく知られている Kaggle に公開されています: https://www.kaggle.com/datasets/mathchi/diabetes-data-set。
糖尿病データセットには、以下のメタデータ情報が含まれています(出典: https://www.kaggle.com/datasets/mathchi/diabetes-data-set):
* Pregnancies: 妊娠回数
* Glucose: 経口ブドウ糖負荷試験における 2 時間後の血漿グルコース濃度
* BloodPressure: 拡張期血圧(mm Hg)
* SkinThickness: 上腕三頭筋皮下脂肪厚(mm)
* Insulin: 2 時間血清インスリン(mu U/ml)
* BMI: ボディマス指数(体重 kg/(身長 m)^2)
* DiabetesPedigreeFunction: 糖尿病血統要因(親族の糖尿病歴、およびこれらの親族と患者の遺伝的関係に関するいくつかのデータが提供されました。 この遺伝的影響の測定により、真性糖尿病の発症に伴う遺伝的リスクについての考えが得られました - 出典: https://machinelearningmastery.com/case-study-predicting-the-onset-of-diabetes-within-five-years-part-1-of-3/)
* Age: 年齢
* Outcome: クラス変数(0 または 1)
#### インスタンス数: 768
#### 属性数: 8 + class
#### 各属性について:(すべて数値)
1. 妊娠回数
2. 経口ブドウ糖負荷試験における 2 時間後の血漿グルコース濃度
3. 拡張期血圧(mm Hg)
4. 上腕三頭筋皮下脂肪厚(mm)
5. 2 時間血清インスリン(mu U/ml)
6. ボディマス指数(体重 kg/(身長 m)^2)
7. 糖尿病血統要因
8. 年齢
9. クラス変数(0 または 1)
#### 属性値の欠落: あり
#### クラス分布:(クラス値 1 は「糖尿病の検査で陽性」として解釈)
## Kaggle から糖尿病データを取得する
Kaggle の糖尿病データは、Health-Dataset アプリケーション(https://openexchange.intersystems.com/package/Health-Dataset)を使って IRIS テーブルに読み込めます。 これを行うには、module.xml プロジェクトから依存関係(Health Dataset 用の ModuleReference)を設定します。
Health Dataset アプリケーションリファレンスを含む Module.xml
<?
xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="predict-diseases.ZPM">
<Module>
<Name>predict-diseases</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
<SourcesRoot>src/iris</SourcesRoot>
<Resource Name="dc.predict.disease.PKG"/>
<Dependencies>
<ModuleReference>
<Name>swagger-ui</Name>
<Version>1.*.*</Version>
</ModuleReference>
<ModuleReference>
<Name>dataset-health</Name>
<Version>*</Version>
</ModuleReference>
</Dependencies>
<CSPApplication
Url="/predict-diseases"
DispatchClass="dc.predict.disease.PredictDiseaseRESTApp"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="1"
UnauthenticatedEnabled="1"
Recurse="1"
UseCookies="2"
CookiePath="/predict-diseases"
/>
<CSPApplication
CookiePath="/disease-predictor/"
DefaultTimeout="900"
SourcePath="/src/csp"
DeployPath="${cspdir}/csp/${namespace}/"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="0"
Recurse="1"
ServeFiles="1"
ServeFilesTimeout="3600"
UnauthenticatedEnabled="1"
Url="/disease-predictor"
UseSessionCookie="2"
/>
</Module>
</Document>
</Export>
## 糖尿病を予測するための Web フロントエンドとバックエンドのアプリケーション
Open Exchange アプリのリンク(https://openexchange.intersystems.com/package/Disease-Predictor)に移動し、以下の手順に従います。
リポジトリを任意のローカルディレクトリに Git pull します。
$ git clone https://github.com/yurimarx/predict-diseases.git
このディレクトリで Docker ターミナルを開き、以下を実行します。
$ docker-compose build
IRIS コンテナを実行します。
$ docker-compose up -d
AI モデルをトレーニングするための Execute Query into Management Portal(http://localhost:52773/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=USER)に移動します。
トレーニングに使用するビューを作成します。
CREATE VIEW DiabetesTrain AS SELECT Outcome, age, bloodpressure, bmi, diabetespedigree, glucose, insulin, pregnancies, skinthickness FROM dc_data_health.Diabetes
ビューを使用して AI モデルを作成します。
CREATE MODEL DiabetesModel PREDICTING (Outcome) FROM DiabetesTrain
モデルをトレーニングします。
TRAIN MODEL DiabetesModel
http://localhost:52773/disease-predictor/index.html に移動し、Disease Predictor フロントエンドを使用して、以下のように疾患を予測します。
## 背後の処理
### 糖尿病を予測するためのバックエンドのクラスメソッド
InterSystems IRIS では、前に作成されたモデルを使って予測するSELECT を実行することができます。
糖尿病を予測するためのバックエンドのクラスメソッド
/// Predict Diabetes
ClassMethod PredictDiabetes() As %Status
{
Try {
Set data = {}.%FromJSON(%request.Content)
Set qry = "SELECT PREDICT(DiabetesModel) As PredictedDiabetes, "
_"age, bloodpressure, bmi, diabetespedigree, glucose, insulin, "
_"pregnancies, skinthickness "
_"FROM (SELECT "_data.age_" AS age, "
_data.bloodpressure_" As bloodpressure, "
_data.bmi_" AS bmi, "
_data.diabetespedigree_" AS diabetespedigree, "
_data.glucose_" As glucose, "
_data.insulin_" AS insulin, "
_data.pregnancies_" As pregnancies, "
_data.skinthickness_" AS skinthickness)"
Set tStatement = ##class(%SQL.Statement).%New()
Set qStatus = tStatement.%Prepare(qry)
If qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
Set rset = tStatement.%Execute()
Do rset.%Next()
Set Response = {}
Set Response.PredictedDiabetes = rset.PredictedDiabetes
Set Response.age = rset.age
Set Response.bloodpressure = rset.bloodpressure
Set Response.bmi = rset.bmi
Set Response.diabetespedigree = rset.diabetespedigree
Set Response.glucose = rset.glucose
Set Response.insulin = rset.insulin
Set Response.pregnancies = rset.pregnancies
Set Response.skinthickness = rset.skinthickness
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write Response.%ToJSON()
Return 1
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return
}
}
これで、どの Web アプリケーションもこの予測を使用して、結果を表示できるようになりました。 predict-diseases アプリケーションのソースコードは、frontend フォルダをご覧ください。
記事
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を使用したアプリケーションの移行が簡単にできるということをご理解いただき、アプリケーションの移行にチャレンジしていただきたいと思います。
もう一つ、サーバー側の処理は全く修正していない点も強調しておきたいと思います。