クリアフィルター
お知らせ
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を使用したアプリケーションの移行が簡単にできるということをご理解いただき、アプリケーションの移行にチャレンジしていただきたいと思います。
もう一つ、サーバー側の処理は全く修正していない点も強調しておきたいと思います。
記事
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 - ツリー」を読み進めてください。 グローバルに表示できるデータのタイプと、グローバルが最適に機能する場所について学習します。