はじめに
今回と次回の2回に分けて、インターオペラビリティ機能について詳しく説明し、アプリケーションを完成させます。今回は、
- 車載器データを記録するクラス
- メッセージ(リクエスト、レスポンス)
- ビジネス・オペレーション
- データ変換
- ビジネス・ルール
の作成について説明します。
アプリケーションの全体構成図を再掲します。
図に示したように、MQTTプロトコル経由で車載器のデータをIRISに取り込む部分は既に説明しました。今回と次回で作成するのは次の機能です。
- MQTT経由で入ってきた車載器データを変換する
- 変換されたデータを適切なコンポーネントに配送するためのビジネス・ルール、ビジネス・プロセスを定義する
- 車載器データをデータベースに保存する
- 車載器データから「イベント」を検知し、MQTTブローカにPublishする
それでは早速開発を始めましょう。今回もVisual Studio Codeを使用して開発を行います。準備として、前回の内容を反映したコンテナを起動しておいてください。そのコンテナがない方は、前回の記事の内容を実施してください。
今回説明するクラスは、DriveDemoディレクトリのIRIS-MQTT/projects/srcs/iris以下にあります。説明を読みながら適宜利用してください。なお、ソースファイルなど一式をまだダウンロードしていない方は、GitHubからpullしてください。
クラスの定義
まずは、車載器のデータをすべて記録するクラス(DriveDemo.Data.DriveRecord)の定義です。
Class DriveDemo.Data.DriveRecord Extends(%Persistent, %XML.Adaptor) { Property CarId As %String; Property RelativeTm As %Integer; Property Longitude As %Float; Property Latitude As %Float; Property Azimuth As %Float; Property Speed As %Float; Property EngineRPM As %Float; Property AccelPos As %Float; Property BrakeSW As %Boolean; Property AcumDistance As %Float; Property AcumFuel As %Float; Property FuelInjection As %Float; Property aX As %Float; Property aY As %Float; Property aZ As %Float; Index MainIdx On CarId; }
前回定義したDriveDemo.Data.CarLatestクラスとほぼ同様です。後の項で説明するリクエストメッセージで使用するため、Extends (%Persistent, %XML.Adaptor)と定義しています。%XML.Adaptorクラスは、インスタンスデータをXML形式で表現することを可能にするクラスです。また、IRISでは、このようにクラスの多重継承が可能です。
クラスの定義ができたらコンパイルしてください。
メッセージクラスの定義
これまでの連載で、ビジネス・サービスやビジネス・プロセスなどIRISのコンポーネント間で「メッセージが流れる」という説明をしてきました。ここでは、この「メッセージ」というものについて改めて説明し、定義を行っていきます。
メッセージ
IRISのインターオペラビリティ機能のコンポーネント同士は、メッセージを送受信することで動作します。IRISでは、メッセージは特別な機能を持ったクラスとして定義されます。
メッセージは、リクエストとレスポンスに分類されます。次の図をご覧ください。
赤で囲んだ部分は、CheckDriveProcess(ビジネスプロセス)とCheckDriveOperation(ビジネス・オペレーション)との間でメッセージが送受信されている様子を示しています。CheckDriveProcessは、CheckDriveRequestというリクエストをCheckDriveOperationに送信し、CheckDriveOperationは、CheckDriveResponseというレスポンスを返しています。
次に示すのは、CheckDriveRequestクラスの定義です。
Class DriveDemo.Request.CheckDriveRequest Extends Ens.Request [ ClassType = persistent, ProcedureBlock ] { Property CarId As %String; Property RelativeTm As %Integer; }
Extends句でEns.Requestクラスを継承することによって、このクラスがリクエストとして機能することを指定しています(その後の[ ]で囲まれた部分は今回は“おまじない”として理解してください)。
その後のProperty定義は、通常のクラス定義と同様です。CheckDriveRequestでは、該当する車のID(CarId)と、相対時刻(RelativeTm)がメッセージに含まれるデータとして定義されています。
次に示すのは、CarUpdateRequestです。
Class DriveDemo.Request.CarUpdateRequest Extends Ens.Request [ ClassType = persistent, ProcedureBlock ] { Property Data As DriveDemo.Data.DriveRecord; }
Ens.Requestを継承する点は、先ほどのCheckDriveRequestと同様です。
プロパティはData一つだけです。As以下で DriveDemo.Data.DriveRecord を指定しています。これは、先ほど定義した DriveDemo.Data.DriveRecordクラスのインスタンスへの参照を、このリクエストに含めることを意味しています。
今度はレスポンスの例を見てみましょう。CheckDriveResponseの定義です。
Class DriveDemo.Response.CheckDriveResponse Extends Ens.Response [ ClassType = persistent, ProcedureBlock ] { Property hasEvent As %Boolean; Property DrivingBehavior As DriveDemo.Data.DrivingBehavior; Property MessageText As %String(MAXLEN = 32767); }
Ens.Responseクラスを継承することによって、このクラスはレスポンスとして機能します。
プロパティには、何らかのイベントが発生したかどうかを示すhasEvent、発生したイベントの内容を保持するMessageTextが定義されています。
DrivingBehaviorプロパティのAs句では、DriveDemo.Data.DrivingBehaviorと指定されています。先ほどのDriveDemo.Data.DriveRecordと違い、ここでは、プロパティに別のクラスのインスタンスを「埋め込む」ことを指定しています。この違いは、Asで指定されているクラスの種類の違いによるものです。
では、そのDriveDemo.Data.DrivingBehaviorの定義を見てみましょう。
Class DriveDemo.Data.DrivingBehavior Extends ( %SerialObject, %XML.Adaptor ) { Property IsOverSpeed As %Boolean; Property IsHighEngineRPM As %Boolean; Property IsHighXG As %Boolean; Property IsHighYG As %Boolean; }
これまで紹介したクラスの定義と同様、4つの%Boolean型のプロパティを定義して、どのようなイベントがあったのか(速度超過、エンジン高回転、大きな加速度)を表すようになっています。
これまで定義したクラスとの重要な違いは、Extends句にあります。このクラスは%SerialObjectを継承しています。
%SerialObjectは%Persistentと違い、インスタンスをデータベースに保存することはできません。しかし、他のクラスのインスタンスのプロパティに、「シリアル化」して埋め込むことが可能になっています。
このDrivingBehaviorクラスを例にとると、インスタンスのDrivingBehaviorプロパティに、DrivingBehaviorのインスタンスが埋め込まれることになります。次の図が、そのイメージです。
今回のアプリケーションでは、他に、DriveDemo.Request.CarRegisterRequestというリクエストを定義しています。
以上のクラスをコンパイルしてください。
ビジネス・オペレーションの定義
連載の以前の回で、ビジネス・オペレーションについて、
プロダクション内の他のコンポーネントからメッセージを受け取り、1つの機能を実行します。例えば、IRIS内部のデータベースにアクセスしたり、外部システムにデータを送信したりするなどの機能を実行します。
と説明しました。これから、ビジネス・オペレーションを具体的に定義していきましょう。
IRISに取り込まれた車載器の情報をリクエストとして受け取り、データベースを更新するためのビジネス・オペレーションDriveDemo.Operation.CarUpdateOperationを作成します。
まずは、定義の部分です。
Class DriveDemo.Operation.CarUpdateOperation Extends Ens.BusinessOperation [ ClassType = "", ProcedureBlock ] { Parameter INVOCATION = "Queue"; XData MessageMap { <MapItems> <MapItem MessageType="DriveDemo.Request.CarRegisterRequest"> <Method>RegisterCar</Method> </MapItem> <MapItem MessageType="DriveDemo.Request.CarUpdateRequest"> <Method>UpdateCar</Method> </MapItem> </MapItems> } … }
クラス宣言のExtends句で、Ens.BusinessOperationを継承しています。Ens.BusinessOperationクラスは、ビジネス・オペレーションに共通の機能を提供するIRISのクラスです。ビジネス・オペレーションを作成する場合は、必ずEns.BusinessOperationを継承することになります。
次に、XData MessageMapで始まるXMLデータを説明します。このデータはメッセージマップと呼ばれ、ビジネス・オペレーションが受信するリクエストの種類(クラス)と、その種類のリクエストを受信した際に起動されるメソッドの対応づけを定義します。
ここでは、
- DriveDemo.Request.CarRegisterRequest: RegisterCarメソッド
- DriveDemo.Request.CarUpdateRequest: UpdateCarメソッド
という対応が定義されています。
では、RegisterCarメソッドの定義を見てみましょう。
// 車のstartトッピックがきたら、そのCar IDに対しレコードを作成する Method RegisterCar( pRequest As DriveDemo.Request.CarRegisterRequest, Output pResponse As Ens.Response) As %Status { Set carid = pRequest.CarId Set ts = pRequest.StartTime // デモ目的のため、以前の同一Car IDのレコードを削除する &sql(DELETE FROM DriveDemo_Data.DriveRecord where CarId = :carid) &sql(DELETE FROM DriveDemo_Data.CarLatest where CarId = :carid) &sql(INSERT INTO DriveDemo_Data.CarLatest (CarId, Tm) values (:carid, :ts)) Quit $$$OK }
メソッドの引数には、
- pRequest: DriveDemo.Request.CarRegisterRequest
- pResponse: Ens.Response
が指定されています。pRequestは、このビジネス・オペレーションに送信されたリクエストが保持されています。先ほど紹介したメッセージマップより、このメソッドは、DriveDemo.Request.CarRegisterRequestクラスのメッセージが送られてきた時に呼び出されますので、引数のAs句にそのように型指定しています。
このメソッドでは、指定されたIDの車の情報を初期化します。初期化にはSQL文を使います。IRISのObjectScriptでは、&sql( )という構文でSQL文を記述することができます。この構文は、ObjectScriptのコードにSQLが埋め込まれる形をとるので、埋め込みSQLと呼ばれます。
このビジネス・オペレーションにはもう一つ、UpdateCarメソッドがあります。定義を次に示します。
Method UpdateCar( pRequest As DriveDemo.Request.CarUpdateRequest, Output pResponse As Ens.Response) As %Status { // 現在の運転情報を、CarLatestに記録 Set tCarId = pRequest.Data.CarId Set tRelativeTm = pRequest.Data.RelativeTm Set tLongitude = pRequest.Data.Longitude Set tLatitude = pRequest.Data.Latitude Set tAzimuth = pRequest.Data.Azimuth Set tSpeed = pRequest.Data.Speed Set tEngineRPM = pRequest.Data.EngineRPM Set tAccelPos = pRequest.Data.AccelPos Set tBrakeSW = pRequest.Data.BrakeSW Set tAcumDistance = pRequest.Data.AcumDistance Set tAcumFuel = pRequest.Data.AcumFuel Set tFuelInjection = pRequest.Data.FuelInjection Set taX = pRequest.Data.aX Set taY = pRequest.Data.aY Set taZ = pRequest.Data.aZ &sql(UPDATE DriveDemo_Data.CarLatest SET RelativeTm = :tRelativeTm, Longitude = :tLongitude, Latitude = :tLatitude, Azimuth = :tAzimuth, Speed = :tSpeed, EngineRPM = :tEngineRPM, AccelPos = :tAccelPos, BrakeSW = :tBrakeSW, AcumDistance = :tAcumDistance, AcumFuel = :tAcumFuel, FuelInjection = :tFuelInjection, aX = :taX, aY = :taY, aZ = :taZ where CarId = :tCarId) $$$TRACE("Car ID: "_tCarId_", Speed: "_tSpeed) Quit $$$OK }
メッセージマップの定義から、DriveDemo.Request.CarUpdateRequestを受信した時にこのメソッドが呼ばれますので、このメソッドの引数pRequestには、DriveDemo.Request.CarUpdateRequestのインスタンスが渡されます。
このメソッドは、リクエストから車載器データを取り出し、埋め込みSQLを使って車の最新情報(CarLatest)テーブルを更新します。
今回のアプリケーションにはもう一つビジネス・オペレーションがあります。車の運転状況をチェックし、イベントが発生していないかをチェックするDriveDemo.Operation.CheckDriveOperationです。
定義を次に示します。
Class DriveDemo.Operation.CheckDriveOperation Extends Ens.BusinessOperation [ ClassType = "", ProcedureBlock ] { Parameter INVOCATION = "Queue"; XData MessageMap { <MapItem MessageType="DriveDemo.Request.CheckDriveRequest"> <Method>CheckDrive</Method> </MapItem> } Method CheckDrive( pRequest As DriveDemo.Request.CheckDriveRequest, Output pResponse As DriveDemo.Response.CheckDriveResponse) As %Status { Set pResponse=##class(DriveDemo.Response.CheckDriveResponse).%New() Set pResponse.hasEvent = 0 Set pResponse.DrivingBehavior = ##class(DriveDemo.Data.DrivingBehavior).%New() Set tCarId = pRequest.CarId Set tRelTm = pRequest.RelativeTm &sql(SELECT AVG(speed) into :tAvgSpeed FROM DriveDemo_Data.DriveRecord WHERE CarId = :tCarId and RelativeTm BETWEEN :tRelTm - 5 AND :tRelTm) &sql(SELECT EngineRPM, aX, aY into :tEngineRPM, :taX, :taY FROM DriveDemo_Data.DriveRecord WHERE CarId = :tCarId and RelativeTm = :tRelTm) Set tMsgTxt = "" Set tDlm = "" if SQLCODE = 0 { if +$g(tAvgSpeed) > 100 { Set pResponse.hasEvent = 1 Set pResponse.DrivingBehavior.IsOverSpeed = 1 Set tMsgTxt = tMsgTxt_tDlm_"Average Speed: "_$fnumber(tAvgSpeed, "", 2)_" km/h" Set tDlm = " , " } if +$g(tEngineRPM) > 3000 { Set pResponse.hasEvent = 1 Set pResponse.DrivingBehavior.IsHighEngineRPM = 1 Set tMsgTxt = tMsgTxt_tDlm_"Engine RPM: "_tEngineRPM_" rpm" Set tDlm = " , " } if $zabs(+$g(taX)) > 300 { Set pResponse.hasEvent = 1 Set pResponse.DrivingBehavior.IsHighXG = 1 Set tMsgTxt = tMsgTxt_tDlm_"X acceleration: "_$fnumber((taX / 1000.0), "+", 3)_" G" Set tDlm = " , " } if $zabs(+$g(taY)) > 200 { Set pResponse.hasEvent = 1 Set pResponse.DrivingBehavior.IsHighYG = 1 Set tMsgTxt = tMsgTxt_tDlm_"Y acceleration: "_$fnumber((taY / 1000.0), "+", 3)_" G" Set tDlm = " , " } if pResponse.hasEvent = 1 { Set tMsgTxt = tMsgTxt_tDlm_"Car ID: "_pRequest.CarId } } Set pResponse.MessageText = tMsgTxt Quit $$$OK } }
他のビジネス・オペレーション同様、Extends句でEns.BusinessOperationクラスを継承しています。
DriveDemo.Operation.CheckDriveOperationのメッセージマップには一つだけエントリがあります。
- DriveDemo.Request.CheckDriveRequest: CheckDrive
CheckDriveメソッドを確認してみましょう。引数は、
- pRequest: DriveDemo.Request.CheckDriveRequest
- pResponse: DriveDemo.Response.CheckDriveResponse
となっています。レスポンスとしてDriveDemo.Response.CheckDriveResponseのインスタンスを返すようになっています。Outputキーワードは、その引数がメソッド内で変更され、呼び出し元に返すのに使用されることを指定します。
上に示したコード例の太字にした3行で、レスポンスのインスタンスを生成、初期化していることを確認してください。
以上で、2つのビジネス・オペレーションの定義ができましたので、コンパイルしてください。
データ変換の定義
データ変換とは、ある形式のデータを別の形式のデータに変換するための規則を定義したもので、IRISではクラスとして実装します。
/DriveDemo/IRIS-MQTT/projects/srcs/iris/DriveDemoに3つのデータ変換クラス(VDoc*.cls)がありますので、Visual Studio CodeのワークスペースのDriveDemoフォルダにコピーしてコンパイルしてください。
次に示すのは、DriveDemo.VDocToCarUpdateRequestクラスです。このクラスは、XML形式のデータからDriveDemo.Request.CarUpdateRequestへの変換を定義します。
Class DriveDemo.VDocToCarUpdateRequest Extends Ens.DataTransformDTL [ DependsOn = (EnsLib.EDI.XML.Document, DriveDemo.Request.CarUpdateRequest) ] { Parameter IGNOREMISSINGSOURCE = 1; Parameter REPORTERRORS = 1; Parameter TREATEMPTYREPEATINGFIELDASNULL = 0; XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ] { <transform sourceClass='EnsLib.EDI.XML.Document' targetClass='DriveDemo.Request.CarUpdateRequest' sourceDocType='mqtt_schema_driverecord:message' create='new' language='objectscript' > <assign value='source.{DriveRecord}' property='target.Data' action='set' /> <assign value='..Piece(source.{type},"/",2)' property='target.Data.CarId' action='set' /> <assign value='source.{DriveRecord.time}' property='target.Data.RelativeTm' action='set' /> <assign value='source.{DriveRecord.longitude}' property='target.Data.Longitude' action='set' /> <assign value='source.{DriveRecord.azimuth}' property='target.Data.Azimuth' action='set' /> <assign value='source.{DriveRecord.car_speed}' property='target.Data.Speed' action='set' /> <assign value='source.{DriveRecord.engine_rpm}' property='target.Data.EngineRPM' action='set' /> <assign value='source.{DriveRecord.accel_pos}' property='target.Data.AccelPos' action='set' /> <assign value='source.{DriveRecord.brake_sw}' property='target.Data.BrakeSW' action='set' /> <assign value='source.{DriveRecord.accum_dist}' property='target.Data.AcumDistance' action='set' /> <assign value='source.{DriveRecord.accum_fuel}' property='target.Data.AcumFuel' action='set' /> <assign value='source.{DriveRecord.fuel_injection}' property='target.Data.FuelInjection' action='set' /> <assign value='source.{DriveRecord.a_000_x}' property='target.Data.aX' action='set' /> <assign value='source.{DriveRecord.a_000_y}' property='target.Data.aY' action='set' /> <assign value='source.{DriveRecord.a_000_z}' property='target.Data.aZ' action='set' /> <assign value='source.{DriveRecord.latitude}' property='target.Data.Latitude' action='set' /> </transform> } }
データ変換のクラスは、Ens.DataTransformDTLクラスを継承して作成します。また、データ変換の定義は、XDataブロックにXML形式で記述されています。
では、データ変換の内容を確認してみましょう。管理ポータルから、「Interoperability」→「構築」→「データ変換」を選択します。そこから、「開く」ボタンを押して、「DriveDemo」→「VDocToCarUpdateRequest」を選択します(次図)。
そうすると、次の図に示す通り、データ変換の定義がビジュアルに表示されます。
左側のソース(データ変換元)には、EnsLib.EDI.XML.Documentクラスのプロパティが表示されています。上の図に示したように、DriveRecordプロパティの左側にある三角をクリックすると、入れ子になっているDriveRecordのサブプロパティが展開表示されます。
右側のターゲット(データ変換先)には、DriveDemo.Request.CarUpdateRequestクラスのプロパティが表示されています。同様に、Dataプロパティの三角をクリックすると、サブプロパティが展開されます。
ソースのプロパティからターゲットのプロパティに引かれている線が、データ変換を表します。各線がどのような変換を表すかは、下側のペインに表示されています。ここでは、ほとんどがデフォルトである「そのままコピー」となっていますが、#2(上の図の赤で囲った行)では、ソースのtypeプロパティの値から”/”で区切られた2番目の文字列を切り出し、それをターゲットのCarIdに設定することが指定されています。
VDocToCarUpdateRequestの他に、2つデータ変換のクラスがありますので、同様に確認してみてください。
紹介したように、IRISではデータ変換をクラス定義として実装します。では、これらのデータ変換は、どこからどのようにして呼び出されるのでしょうか? その答えの一つがビジネス・ルールです。次のセクションで、ビジネス・ルールについて説明します。
ビジネス・ルールの定義
ビジネス・ルールとは、条件を指定して、メッセージの送信先を決定したり、データの変換方法を指定したりできるコンポーネントです。
IRISではビジネス・ルールもクラスとして定義します。/DriveDemo/IRIS-MQTT/projects/srcs/iris/DriveDemo/DemoProcessRoutingRule.clsにコードがありますので、Visual Studio CodeのワークスペースのDriveDemoフォルダにDemoProcessRoutingRule.clsという名前でファイルを作成し、内容をコピーの上、コンパイルしてください。
次のコードは、DriveDemo.DemoProessRoutineRuleクラスの定義です。
Class DriveDemo.DemoProcessRoutingRule Extends Ens.Rule.Definition { Parameter RuleAssistClass = "EnsLib.MsgRouter.VDocRuleAssist"; XData RuleDefinition [ XMLNamespace = "http://www.intersystems.com/rule" ] { <ruleDefinition alias="" context="EnsLib.MsgRouter.VDocRoutingEngine" production="DriveDemo.Production"> <ruleSet name="" effectiveBegin="" effectiveEnd=""> <rule name="" disabled="false"> <constraint name="source" value="MQTTIn"></constraint> <constraint name="msgClass" value="EnsLib.EDI.XML.Document"></constraint> <constraint name="docCategory" value="mqtt_schema_driverecord"></constraint> <constraint name="docName" value="message"></constraint> <when condition="Piece(Document.{type},"/",3,3)="start""> <send transform="DriveDemo.VDocToCarRegisterRequest" target="CarOperation"></send> </when> <when condition="Piece(Document.{type},"/",3,3)="drive""> <send transform="DriveDemo.VDocToCarUpdateRequest" target="CarOperation"></send> <send transform="DriveDemo.VDocToCheckDriveRequest" target="CheckDriveProcess"></send> </when> </rule> </ruleSet> </ruleDefinition> } }
クラス定義のExtends句で、Ens.Rule.Definitionを継承することで、ビジネス・ルールの機能を引き継ぎます。
ビジネス・ルールの定義は、XData宣言とともにXML形式で行います。
定義内容を確認するために、管理ポータルにアクセスし、「Interoperability」→「構築」→「ビジネス・ルール」を選択してください。そこで、次の図のように、「DemoProcessRoutingRule」を開いてください。
そうすると、次の図のようにビジネス・ルールの定義がビジュアルに表示されます。
詳細は機会を改めて説明したいと思いますが、ここでは、クラス定義のXMLの内容が、ビジュアルに表示されたコンポーネントに対応していることを確認してみてください。特に、
- “when”で条件を指定し、それに当てはまれば、”send”アクションを実行する
- “send”では、”transform”によりデータ変換を行い、”target”でデータ送信先のコンポーネント(ここではビジネス・オペレーション)を指定する
というところに注意して確認してください。このtransformで指定できるのは、前のセクションで定義したデータ変換のクラスです。
今回は説明の関係で、クラス定義でビジネス・ルールを作成しましたが、通常は管理ポータルからビジュアルにビジネス・ルールを作成していくのが一般的です。
定義されたビジネス・ルールを呼び出すコンポーネントの一つが、ビジネス・プロセスです。ビジネス・プロセスについては次回説明したいと思います。
まとめ
今回は、「IRISのインターオペラビリティ機能を使いこなそう(前編)」と題して、メッセージ、ビジネス・オペレーション、データ変換、ビジネス・ルールといったコンポーネントを作成する方法について紹介しました。
次回の後編は、この連載の最終回として、ビジネス・プロセスを作成し、アプリケーションを完成させます。また、IRISをRESTサーバとして動作させる方法についても紹介する予定です。
参考
インターシステムズでは、開発者の皆様にIRISを知っていただくために、いろいろな機能を紹介する短い動画を作成しています(日本語字幕付き)。
インターオペラビリティ機能は以下のURLで説明されています。ぜひご覧ください。