【IRIS】【JSON】%DynamicObject

今回は、IRISでJSON的な操作を行う「%DynamicObject」について解説します。

※この記事は下記の方向けになります。
  • %DynamicObjectの操作を行ったことがない方
  • ObjectScriptで外部APIとの連携を行いたい方
  • データを格納する箱的な位置づけを探している方

はじめに

%DynamicObjectは、JSONのような動的なデータ構造を扱うためのクラスです。
JSON形式のデータを柔軟に生成・操作・変換したり、外部APIとの連携でJSONデータを扱い場合にも便利です。

サンプルは全てIRIS 2023.1.2.450.0で作成しています。

今回は、IRISでJSONを扱うクラスについて解説します。

基本操作

オブジェクト生成

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":(2*6)}
	w json.%ToJSON()
	
	s json = {}
	w !,json.%ToJSON()
}

javascriptでjsonを扱う感じでオブジェクトの作成が可能です。
また、初期値に変数や式を含める場合は、()で括る必要があります(wifeが該当)。

【関数の実行結果】
{“name”:”藤原道長”,”wife”:”源倫子”,”child”:12}
{}

値の追加

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":(2*6)}
	, json.emperor = "一条天皇"
	w json.%ToJSON(),!
	
	s json = {}
	, json.name = "紫式部"
	d json.%Set("sex","女").%Set("exitFlg",0, "boolean").%Set("tale", "", "null").%ToJSON()
}

javascriptと同じ操作でプロパティの追加が可能です。

ObjectScriptでは「true, false」「null」を表現できません。
これに対応するには、「%Set(key, value, type)」を使用します(typeは省略可)。

また、%Set()はチェーンメソッド可能です。

【関数の実行結果】
{“name”:”藤原道長”,”wife”:”源倫子”,”child”:12,”emperor”:”一条天皇”}
{“name”:”紫式部”,”sex”:”女”,”exitFlg”:false,”tale”:null}

%Set()のtype値一覧

type設定値説明
“null”JSONのnullを表現する。valueは””である事
“boolean”JSONのtrue or falseを表現する。valueは1 or 0である事
“number”値を数値に変換する
“string”値を文字列に変換する
“string>base64”文字列に変換後、base64にエンコードする
“string<base64”文字列に変換後、base64にデコードする
“stream”ストリームを文字列に変換する
“stream>base64”ストリームをbase64にエンコードする
“stream<base64”ストリームがbase64からバイト文字列にデコードする

値の取得

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":(2*6)}
	, json.emperor = "一条天皇"
	w "emperor", ",",json.emperor,!
	w "name",", ",json.name,!
	
	s json = {}
	, json.name = "紫式部"
	d json.%Set("sex","女").%Set("exitFlg",0, "boolean").%Set("tale", "", "null")
	
	w "exitFlg",", ",json.exitFlg,!
	w "exitFlg", ",",json.%Get("exitFlg",,"json"),!
	w "tale", ",",json.tale,!
	w "xxx",", ",json.%Get("xxx","default")
}

「%Get(key, default, type)」は、$get(var, default)の感覚で使えます。
また、typeに型を設定する事で、値を変換する事が可能です。

【関数の実行結果】
emperor,一条天皇
name, 藤原道長
exitFlg, 0
exitFlg,false
tale,
xxx, default

%Get()のtype値一覧

type設定値説明
“”変換なし
“string”値を文字列に変換する
“string>base64”文字列に変換後、base64にエンコードする
“string<base64”文字列に変換後、base64にデコードする
“stream”文字列をストリームに変換する
“stream>base64”base64にエンコードされた文字列をストリームに変換する
“stream<base64”base64にデコードされた文字列をストリームに変換する
“json”json表現に変換する

プロパティの削除

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":{"tomoko":6, "akiko":6}}
	, json.emperor = "一条天皇"
	w json.%Remove("child").%ToJSON(),!
	w json.%ToJSON(),!
	
	s json = {}
	, json.name = "紫式部"
	d json.%Set("sex","女").%Set("exitFlg",0, "boolean").%Set("tale", "", "null")
	w json.%Remove("exitFlg"),!
	d json.%ToJSON()
}

%Removeはプロパティを削除し、削除した値を受け取ります。

【関数の実行結果】
{“tomoko”:6,”akiko”:6}
{“name”:”藤原道長”,”wife”:”源倫子”,”emperor”:”一条天皇”}
0
{“name”:”紫式部”,”sex”:”女”,”tale”:null}

プロパティの存在確認

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":{"tomoko":6, "akiko":6}}
	, json.emperor = "一条天皇"
	
	w json.%IsDefined("emperor"),!
	w json.%IsDefined("dummy")
}

プロパティの存在有無を確認します。

【関数の実行結果】
1
0

プロパティの型を確認する

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "child":(2*6)}
	d json.%Set("regentFlg",1,"boolean").%Set("testDt","","null")
	
	w json.%GetTypeOf("name"),!
	w json.%GetTypeOf("child"),!
	w json.%GetTypeOf("regentFlg"),!
	w json.%GetTypeOf("testDt")
}

プロパティの型を取得します。

【関数の実行結果】
string
number
boolean
null

取得できる型一覧

型名説明
“null”JSONのnullを示す
“boolean”true or falseの値
“number”数値
“string”文字列
“oref”オブジェクト
“object”%DynamicObject
“array”%DynamicArray
“unassingned”プロパティはあるが、値がない

全件取得する

%DynamicArrayも同様に取得できます。
%DynamicObjectは順番が保障されていないので、セットした順に取得できるとは限りません。

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "child":(2*6), "wife":(wife), "sex":1}
	
	s iter = json.%GetIterator()
	while iter.%GetNext(.prop, .val, .type) {
		w "prop:",prop,", val:",val,", type:",type,!
	}
}

%GetIterator」でイテレータを取得して、「%GetNext」で全件取得します。
プロパティ名・値・型が取得可能です。

【関数の実行結果】
key:name, val:藤原道長, type:string
key:child, val:12, type:number
key:wife, val:源倫子, type:string
key:sex, val:1, type:number

プロパティ数取得

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":{"tomoko":6, "akiko":6}}
	, json.emperor = "一条天皇"
	
	d json.%ToJSON()
	w !,"size, ",json.%Size()
}

直下のプロパティ数を取得します。

【関数の実行結果】
{“name“:”藤原道長”,”wife“:”源倫子”,”child“:{“tomoko”:6,”akiko”:6},”emperor“:”一 条天皇”}
size, 4

jsonからストリーム、ストリームからjson

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "child":(2*6), "wife":(wife), "sex":1}
	
	s strm = ##class(%Stream.GlobalCharacter).%New()
	d json.%ToJSON(strm)
	w strm.Read(),!!
	
	s dt = {}.%FromJSON(strm)
	d dt.%ToJSON()
}

%ToJSON」の引数にストリームを渡すと、jsonをストリーム化します。
また、ストリームを「%FromJSON」に渡すと、json化します。

【関数の実行結果】
{“name”:”藤原道長”,”child”:12,”wife”:”源倫子”,”sex”:1}

{“name”:”藤原道長”,”child”:12,”wife”:”源倫子”,”sex”:1}

ファイルからjson化

拡張子は問わないようで「.txt」「.json」の両方で確認しています。
文字コードは「UTF-8」でBOMが必用みたいです。

{
    "name":"藤原道長",
    "wife":"源倫子",
    "child":{"tomoko":6, "akiko":6},
    "emperor":"一条天皇"
}

%FromJSON()を使う方法と、%FromJSONFile()を使う方法の2通りあります。

ClassMethod sample(fileName As %String)
{
	s strm = ##class(%Stream.FileCharacter).%New()
	d strm.LinkToFile(fileName)
	s json ={}.%FromJSON(strm)
	d json.%ToJSON()
	
	w !
	s json = {}.%FromJSONFile(fileName)
	d json.%ToJSON()
}

ファイル情報を一端ストリームにして「%FromJSON」でjsonにするか、直接「%FromJSONFile」でjsonにするかの二択になります。

【関数の実行結果】
{“name”:”藤原道長”,”wife”:”源倫子”,”child”:{“tomoko”:6,”akiko”:6},”emperor”:”一 条天皇”}
{“name”:”藤原道長”,”wife”:”源倫子”,”child”:{“tomoko”:6,”akiko”:6},”emperor”:”一 条天皇”}

オブジェクトの複製

ClassMethod sample()
{
	s json = {"base":"コピー元"}
	, clone = {}.%FromJSON(json.%ToJSON())
	
	s clone.base = "コピー先"
	w json.%ToJSON(),!
	, clone.%ToJSON()
}

ダイナミック・オブジェクトには「クローンコマンド」がありません。
複製を行うには一端シリアル化し、「%FromJSON」で再度オブジェクト化します。

【関数の実行結果】
{“base”:”コピー元”}
{“base”:”コピー先”}

見やすく表示する

jsonを一行で表示されても、人間には理解し難いです。
そこで、人が見やすいように整形してくれるのが「%JSON.Formatter」です。

簡単なサンプルを用いて確認してみましょう。

ClassMethod sample()
{
	s wife = "源倫子"
	, json = {"name":"藤原道長", "wife": (wife), "child":{"tomoko":6, "akiko":6}}
	, json.emperor = "一条天皇"
	
	s fmt = ##class(%JSON.Formatter).%New()
	d fmt.Format(json)
}

【関数の実行結果】
{
  ”name”:”藤原道長”,
  ”wife”:”源倫子”,
  ”child”:{
    ”tomoko”:6,
    ”akiko”:6
  },
  ”emperor”:”一条天皇”
}

個々がネストされて見やすい形に整形されています。
運用上は不要ですが、デバッグ時にはとても重宝します。

静的クラスとのパフォーマンス比較

静的クラスとのパフォーマンス比較のため、下記データクラスを作成して検証を行いました。

 ・関数「test1」は、静的クラスを規定してデータを格納します。
 ・関数「test2」は、%DynamicObjectを利用してデータを格納します。

Class developer.json.Data Extends %RegisteredObject
{

Property name As %String;

Property wife As %String;

Property child As %Integer;

Property sex As %Boolean;

ClassMethod test1(cnt As %Integer)
{
	s start = $zh
	f pos=1:1:cnt {
		s obj = ##class(developer.json.Data).%New()
		, obj.name = "藤原道長"
		, obj.wife = "源倫子"
		, obj.child = 2*6
		, obj.sex = 1
	}
	w !,$zh - start
}

ClassMethod test2(cnt As %Integer)
{
	s start = $zh
	f pos=1:1:cnt {
		s obj ={"name":"藤原道長", "child":(2*6), "wife":"源倫子", "sex":1}
	}
	w !,$zh - start
}
}

10,000万件のループで実施した所、下記結果が得られました。

  • 静的クラス → 0.022192
  • %DynamicObject → 0.066437

静的クラスから見ると、3倍遅い結果となりました。
プロパティの数やループ数によっては、この差が別途変化するとは思いますが、静的クラスから見ると遅いという結果は変わらないでしょう。

ただし、%DynamicObjectは、静的クラスと比較すると汎用性が高く、値の出し入れが自由自在です。

汎用性と取るか速度を取るか、システムの仕様によって採用してください。

おわりに

いかがだったでしょうか。

%DynamicObjectは柔軟性の高さが魅力になります。
ただし、柔軟性の高さ故か静的クラスから見ると、パフォーマンスが劣るのは否めません。

なんでも入る箱が必用であれば%DynamicObject、入れるものが決まっていれば静的クラスを使用すればよいかと思います。

javascript等との連携時では、ObjectScriptとの仕様差があります。
nullと”の差別化や、true/falseがObjectScriptでは1,0であったりします。

連携情報のやり取り時は、値の適切な変換を考慮に入れてコーディングしてください。

兎にも角にもIRISでJSONを扱えるようになったのは、最大のメリットになります。
後は、分割代入等の機能が実装されてくれれば御の字ですね。