#Code Snippet

0 フォロワー · 15 投稿

コードスニペットは、再利用可能なソースコード、マシンコード、またはテキストの小さな領域を表すプログラミング用語です。

記事 Toshihiko Minamoto · 11月 5, 2024 1m read

Studio で最も便利な機能の 1 つにコードスニペットがあります。

以下は、スニペットを VSCode に追加する方法です。

以下は、一般的な手順です。

1. ファイル - 設定 - ユーザースニペットに移動し、objectscript を選択します。

2. スニペットを追加します。以下に例を示します。

"SQL Statement": {
    "prefix": ["sql"],
    "body": ["#dim rs As %SQL.ISelectResult",
            "set rs = ##class(%SQL.Statement).%ExecDirect(,\"SELECT * FROM\")",
            "while rs.%Next() {",
            "\twrite rs.ID, !",
            "}"]
}

要素は以下のように定義されています。

  • prefix - スニペットを表示するのに入力する必要のある文字
  • body - スニペットの本体

さらにスニペットには、以下のようにプレースホルダーも含められます。

"Method": {
    "prefix": ["method"],
    "body": ["set sc = ##class(${1:class}).${2:method}()"]
}

このスニペットを挿入すると、最初のプレースホルダーの開始点に自動的に移動します。プレースホルダーは <TAB> でスクロールできます。

コーディングをお楽しみください!

0
0 116
記事 Toshihiko Minamoto · 4月 18, 2024 13m read

我々には、Redditユーザーが書いた、おいしいレシピデータセット がありますが, 情報のほとんどは投稿されたタイトルや説明といったフリーテキストです。埋め込みPythonLangchainフレームワークにあるOpenAIの大規模言語モデルの機能を使い、このデータセットを簡単にロードし、特徴を抽出、分析する方法を紹介しましょう。

データセットのロード

まず最初に、データセットをロードするかデータセットに接続する必要があります。

これを実現するにはさまざまな方法があります。たとえばCSVレコードマッパーを相互運用性プロダクションで使用したり csvgenのようなすばらしい OpenExchange アプリケーションを使用することもできます。

今回、外部テーブルを使用します。これは物理的に別の場所に保存されているデータをIRIS SQLで統合する非常に便利な機能です。

まずは外部サーバ(Foreign Server)を作成します。

CREATE FOREIGN SERVER dataset FOREIGN DATA WRAPPER CSV HOST '/app/data/'

その上でCSVファイルに接続する外部テーブルを作成します。

CREATE FOREIGN TABLE dataset.Recipes (
  CREATEDDATE DATE,
  NUMCOMMENTS INTEGER,
  TITLE VARCHAR,
  USERNAME VARCHAR,
  COMMENT VARCHAR,
  NUMCHAR INTEGER
) SERVER dataset FILE 'Recipes.csv' USING
{
  "from": {
    "file": {
       "skip": 1
    }
  }
}

以上です。すぐに「dataset.Recipes」にSQLクエリを実行できます。 image

## どんなデータが必要?

データセットは興味深く、直ぐに処理したいと思うのですが、調理のレシピを決めたいのであれば、分析に使える情報がもう少し必要です。 2つの永続化クラス(テーブル)を使用します。

  • yummy.data.Recipe抽出分析したいレシピのタイトルと説明、他のプロパティが入ったクラス (例: スコア、難易度、材料、調理タイプ、準備時間)
  • yummy.data.RecipeHistory レシピのログを取るためのシンプルなクラス

これで 「yummy.data*」 テーブルにデータセットの内容をロードすることができます。

do ##class(yummy.Utils).LoadDataset()

一見良さそうに見えますが、スコア、難易度、材料、準備時間、調理時間フィールドのデータをどのように生成するのかを見つける必要があります。

## レシピの分析 各レシピのタイトルと説明を処理します

  • 難易度, 材料, 調理タイプなどの抽出
  • 何を作りたいか決められるよう、基準に基づいて独自のスコアを構築

以下を使用します

  • より多くの分析を構築したい場合に再利用できる一般的な分析構造

LLM(大規模言語モデル)は自然言語を処理するための本当に素晴らしいツールです。

LangChainはPythonで動くようになっているので、Embedded Pythonを使ってInterSystems IRISで直接使うことができます。 LangChain is ready to work in Python, so we can use it directly in InterSystems IRIS using Embedded Python.

完全な SimpleOpenAI クラスは以下のようになります。

/// レシピ向けのシンプルな OpenAI 分析
Class yummy.analysis.SimpleOpenAI Extends Analysis
{

Property CuisineType As %String;

Property PreparationTime As %Integer;

Property Difficulty As %String;

Property Ingredients As %String;

/// 実行
/// ターミナルから実行できます。
/// set a = ##class(yummy.analysis.SimpleOpenAI).%New(##class(yummy.data.Recipe).%OpenId(8))
/// do a.Run()
/// zwrite a
Method Run()
{
    try {
        do ..RunPythonAnalysis()

        set reasons = ""

        // 好きな調理タイプ
        if "spanish,french,portuguese,italian,korean,japanese"[..CuisineType {
            set ..Score = ..Score + 2
            set reasons = reasons_$lb("It seems to be a "_..CuisineType_" recipe!")
        }

        // 丸一日調理に費やしたくない :)
        if (+..PreparationTime < 120) {
            set ..Score = ..Score + 1
            set reasons = reasons_$lb("You don't need too much time to prepare it") 
        }
        
        // 好きな材料ボーナス
        set favIngredients = $listbuild("kimchi", "truffle", "squid")
        for i=1:1:$listlength(favIngredients) {
            set favIngred = $listget(favIngredients, i)
            if ..Ingredients[favIngred {
                set ..Score = ..Score + 1
                set reasons = reasons_$lb("Favourite ingredient found: "_favIngred)
            }
        }

        set ..Reason = $listtostring(reasons, ". ")

    } catch ex {
        throw ex
    }
}

/// 分析結果でレシピを更新する
Method UpdateRecipe()
{
    try {
        // 親クラスの処理を先に呼び出す
        do ##super()

        // 個別のOpenAI 解析結果を追加
        set ..Recipe.Ingredients = ..Ingredients
        set ..Recipe.PreparationTime = ..PreparationTime
        set ..Recipe.Difficulty = ..Difficulty
        set ..Recipe.CuisineType = ..CuisineType

    } catch ex {
        throw ex
    }
}

/// 埋め込み Python + Langchain で分析を実行
/// do ##class(yummy.analysis.SimpleOpenAI).%New(##class(yummy.data.Recipe).%OpenId(8)).RunPythonAnalysis(1)
Method RunPythonAnalysis(debug As %Boolean = 0) [ Language = python ]
{
    # load OpenAI APIKEY from env
    import os
    from dotenv import load_dotenv, find_dotenv
    _ = load_dotenv('/app/.env')

    # account for deprecation of LLM model
    import datetime
    current_date = datetime.datetime.now().date()
    # date after which the model should be set to "gpt-3.5-turbo"
    target_date = datetime.date(2024, 6, 12)
    # set the model depending on the current date
    if current_date > target_date:
        llm_model = "gpt-3.5-turbo"
    else:
        llm_model = "gpt-3.5-turbo-0301"

    from langchain.chat_models import ChatOpenAI
    from langchain.prompts import ChatPromptTemplate
    from langchain.chains import LLMChain

    from langchain.output_parsers import ResponseSchema
    from langchain.output_parsers import StructuredOutputParser

    # init llm model
    llm = ChatOpenAI(temperature=0.0, model=llm_model)

    # prepare the responses we need
    cuisine_type_schema = ResponseSchema(
        name="cuisine_type",
        description="What is the cuisine type for the recipe? \
                     Answer in 1 word max in lowercase"
    )
    preparation_time_schema = ResponseSchema(
        name="preparation_time",
        description="How much time in minutes do I need to prepare the recipe?\
                     Anwer with an integer number, or null if unknown",
        type="integer",
    )
    difficulty_schema = ResponseSchema(
        name="difficulty",
        description="How difficult is this recipe?\
                     Answer with one of these values: easy, normal, hard, very-hard"
    )
    ingredients_schema = ResponseSchema(
        name="ingredients",
        description="Give me a comma separated list of ingredients in lowercase or empty if unknown"
    )
    response_schemas = [cuisine_type_schema, preparation_time_schema, difficulty_schema, ingredients_schema]

    # get format instructions from responses
    output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
    format_instructions = output_parser.get_format_instructions()
    
    analysis_template = """\
    Interprete and evaluate a recipe which title is: {title}
    and the description is: {description}
    
    {format_instructions}
    """
    prompt = ChatPromptTemplate.from_template(template=analysis_template)

    messages = prompt.format_messages(title=self.Recipe.Title, description=self.Recipe.Description, format_instructions=format_instructions)
    response = llm(messages)

    if debug:
        print("======ACTUAL PROMPT")
        print(messages[0].content)
        print("======RESPONSE")
        print(response.content)

    # populate analysis with results
    output_dict = output_parser.parse(response.content)
    self.CuisineType = output_dict['cuisine_type']
    self.Difficulty = output_dict['difficulty']
    self.Ingredients = output_dict['ingredients']
    if type(output_dict['preparation_time']) == int:
        self.PreparationTime = output_dict['preparation_time']

    return 1
}

}

「RunPythonAnalysis」メソッドがOpenAIが詰め込むところです :)  ターミナルから直接実行してレシピを受け取れます。

do ##class(yummy.analysis.SimpleOpenAI).%New(##class(yummy.data.Recipe).%OpenId(12)).RunPythonAnalysis(1)

以下のような出力を受け取れます。

USER>do ##class(yummy.analysis.SimpleOpenAI).%New(##class(yummy.data.Recipe).%OpenId(12)).RunPythonAnalysis(1)
======実際の課題
                    レシピタイトルを解釈、評価: 巻きずし - アラスカロール
                    説明: 寿司がたべたいのに巻きすがない? 代わりに簡単なバージョンを試してみてください。超簡単なのに、同じようにおいしい!
[Video Recipe](https://www.youtube.com/watch?v=1LJPS1lOHSM)
# 材料
提供量:  \~サンドイッチ5枚分
* 米1カップ
* 水 3/4 カップ + 大さじ 2 1/2
* 昆布 小口切り 1枚
* 米酢 大さじ2
* さとう 大さじ1
* 塩 小さじ1
* アボカド 2個
* カニカマ 6個
* 和風マヨ 大さじ2
* サーモン 1/2 ポンド  
# レシピ     
* 酢飯1合をボウルに入れ、2回以上、または水が透明になるまで米を洗う。炊飯器に米を移し、昆布の小口切り1枚と水3/4カップ+大さじ2と1/2杯を加える。炊飯器の指示に従って炊く。
* 米酢大さじ2、砂糖大さじ1、塩小さじ1を中くらいのボウルに入れる。全体がよく混ざるまで混ぜる。
* 炊き上がったら昆布を取り除き、すぐに酢を入れた中ボウルに米をすべてすくい入れ、飯ベラを使ってよく混ぜる。米をつぶさないように、切るように混ぜること。炊きあがったら、キッチンタオルをかけて室温まで冷ます。
* アボカド1個の上部を切り、アボカドの中央に切り込みを入れ、ナイフに沿って回転させる。次にアボカドを半分ずつ取り、ひねる。その後、ピットのある側を取り、慎重にピットに切り込みを入れ、ひねって取り除く。その後、手で皮をむく。この手順をもう片方のアボカドでも繰り返す。作業スペースを確保するため、作業台を片付けるのを忘れずに。次に、アボカドを下向きに置き、薄くスライスする。スライスしたら、ゆっくりと広げていく。それが終わったら、脇に置いておく。
* カニカマから包みをはずす。カニカマを縦にむいていく。すべてのカニカマを剥いたら、横に回転させながら細かく刻み、和風マヨ(大さじ2)とともにボウルに入れ、全体がよく混ざるまで混ぜる。
* 鋭利なナイフを斜めに入れ、木目に逆らって薄くスライスする。切り口の厚さは好みによる。ただ、すべてのピースが同じような厚さになるようにする。
* 海苔巻きラップを手に取る。キッチンバサミを使い、海苔巻きラップの半分の位置から切り始め、ラップの中心を少し過ぎるまで切る。ラップを垂直に回転させ、作り始める。すし飯を握るために、手に水をつけておく。酢飯を手に取り、海苔巻きの左上の四辺に広げる。次に、右上にサーモンを2切れ並べる。右下にアボカドを2切れのせる。最後に左下にカニサラダを小さじ2杯ほどのせる。次に、右上の四つ角を右下の四つ角に折り込み、さらに左下の四つ角に折り込む。最後に、左上の四つ角をサンドイッチの残りの部分に折り込む。その後、ラップを上に置き、半分に切って、生姜とわさびを2、3枚添えれば出来上がり。
                    
                    出力は、先頭と末尾の"``json "と"``"を含む、以下のスキーマでフォーマットされたマークダウンのコードスニペットでなければなりません:
json
{
        "cuisine_type": string  // レシピの調理タイプは?                                  小文字の1単語で回答
        "preparation_time": integer  // レシピの準備に必要な時間(分)は? 整数で回答(不明な場合はnull)
        "difficulty": string  // レシピの難易度は?                               「容易」「標準」「難しい」「とても難しい」のうちから1つを回答
        "ingredients": string  // 小文字のカンマ区切りの材料リスト、不明な場合は空
}

                    
======応答
json
{
        "cuisine_type": "japanese",
        "preparation_time": 30,
        "difficulty": "easy",
        "ingredients": "sushi rice, water, konbu, rice vinegar, sugar, salt, avocado, imitation crab sticks, japanese mayo, salmon"
}

良さそうです。OpenAIのプロンプトは有用な情報を返してくれるようです。ターミナルから分析クラス全体を実行してみましょう:

set a = ##class(yummy.analysis.SimpleOpenAI).%New(##class(yummy.data.Recipe).%OpenId(12))
do a.Run()
zwrite a
USER>zwrite a
a=37@yummy.analysis.SimpleOpenAI  ; <OREF>
+----------------- general information ---------------
|      oref value: 37
|      class name: yummy.analysis.SimpleOpenAI
| reference count: 2
+----------------- attribute values ------------------
|        CuisineType = "japanese"
|         Difficulty = "easy"
|        Ingredients = "sushi rice, water, konbu, rice vinegar, sugar, salt, avocado, imitation crab sticks, japanese mayo, salmon"
|    PreparationTime = 30
|             Reason = "It seems to be a japanese recipe!. You don't need too much time to prepare it"
|              Score = 3
+----------------- swizzled references ---------------
|           i%Recipe = ""
|           r%Recipe = "30@yummy.data.Recipe"
+-----------------------------------------------------

## 全レシピを解析する!

当然、読み込んだすべてのレシピで分析を実行したいでしょう。 この方法でレシピ ID の範囲を分析することができます

USER>do ##class(yummy.Utils).AnalyzeRange(1,10)
> Recipe 1 (1.755185s)
> Recipe 2 (2.559526s)
> Recipe 3 (1.556895s)
> Recipe 4 (1.720246s)
> Recipe 5 (1.689123s)
> Recipe 6 (2.404745s)
> Recipe 7 (1.538208s)
> Recipe 8 (1.33001s)
> Recipe 9 (1.49972s)
> Recipe 10 (1.425612s)

その後、レシピテーブルを再度表示させ、結果をチェックします。

select * from yummy_data.Recipe

image

どんぐりカボチャのピザか、豚肉入り韓国風豆腐キムチを試してみます:) いずれにせよ、家で再確認する必要がありますね :)

最後に

サンプルソースは全て https://github.com/isc-afuentes/recipe-inspector にあります。

この簡単な例で、InterSystems IRIS で LLM テクニックを使用して機能を追加したり、データの一部を分析する方法を学びました。

これを起点に以下のことが考えられます

  • InterSystems BIを使い、キューブやダッシュボードでデータの検索やナビゲートをおこなう。
  • Webアプリを作成し、UIを提供する(例:Angular)RESTForms2のようなパッケージを活用することで、永続クラスへのREST APIを自動的に生成することができます。 *レシピが好きか嫌いかを保存し、新しいレシピが好きかどうかを判断するのはいかがでしょうか。IntegratedMLアプローチ、あるいはLLMアプローチでいくつかの例データを提供し、RAG(Retrieval Augmented Generation)ユースケースを構築してみるのも良いでしょう。

他にどんなことが試せそうでしょうか?ご意見をお聞かせください!

0
0 458
記事 Toshihiko Minamoto · 12月 5, 2023 4m read

質問:

特定のフォルダ/ディレクトリにあるファイルをワイルドカード/フィルターによってリスト表示するにはどうすればよいか。

例えば、'C:\Temp' 内にあるすべての '*.txt' をリスト表示する場合です。

回答:

CACHE :

%Library.File の FileSet クラスクエリを使用できます。

以下に、これを使用したサンプルコードを示します(添付されています)。

run(pDir,pFileSpec)

      Set tRS=##class(%ResultSet).%New("%Library.File:FileSet")
      Set tSC=tRS.Execute(pDir,pFileSpec)

      Write "Name",?30,"Date Modified",?53,"Type",!
      Write "--------------------------------------------------------------------------",!

      While tRS.Next() {
            Write tRS.Get("Name"),?30,tRS.Get("DateModified"),?53,tRS.Get("Type"),!

      }

以下は、これを実行した例です。

USER>do run^testFileSet("C:\Temp","*.txt")

Name                          Date Modified          Type

--------------------------------------------------------------------------

C:\Temp\hl7.txt               2014-07-30 12:09:18    F

C:\Temp\hsaa_msgs.txt         2015-06-25 13:02:16    F

C:\Temp\JSONRESTProxy_10.txt  2014-05-04 14:29:04    F

C:\Temp\JSONRESTProxy_8.txt   2014-05-04 14:28:09    F

C:\Temp\myTestFile2.txt       2014-05-05 09:19:31    F

C:\Temp\newStream.txt         2015-07-09 09:41:59    F

C:\Temp\oldStream.txt         2015-07-09 09:41:27    F

C:\Temp\tcpTrace.txt          2014-05-29 12:13:11    F

C:\Temp\tempadts.txt          2015-06-22 08:35:03    F

C:\Temp\WSLic.txt             2014-06-08 10:34:13    F

 

Ensemble:

BO で File Outbound Adapter を使用して、アダプターの NameList() メソッドを呼び出します。

以下に、これを使用したサンプル BO を示します(添付されています)。

Class Play.BO.FileOperation Extends Ens.BusinessOperation
{

Parameter ADAPTER = "EnsLib.File.OutboundAdapter";

Property Adapter As EnsLib.File.OutboundAdapter;

Parameter INVOCATION = "Queue";

Method FileList(
      pRequest As Ens.StringRequest,
      Output pResponse As Ens.Response) As %Status
{
      #dim fileList As %ListOfDataTypes = ""
      Set status = ..Adapter.NameList(.fileList,pRequest.StringValue)
     
      // Returns list of: Filename;Type;Size;DateCreated;DateModified;FullPathName
      //                  1        2    3    4           5            6  
      If $$$ISOK(status)&&$IsObject(fileList) {
            For i=1:1:fileList.Count() {
                  Set fileInfo = fileList.GetAt(i)
                  $$$LOGINFO("Name: "_$Piece(fileInfo,";",1)_" / DateModified: "_$Piece(fileInfo,";",5)_" / Type: "_$Piece(fileInfo,";",2))
            }
           
      }
     
      Quit status
}

 

以下は、イベントログのサンプル出力です。

Text 
Name: WSLic.txt / DateModified: modified=2014-06-08 10:34:13 / Type: =F
Name: tempadts.txt / DateModified: modified=2015-06-22 08:35:03 / Type: =F
Name: tcpTrace.txt / DateModified: modified=2014-05-29 12:13:11 / Type: =F
Name: oldStream.txt / DateModified: modified=2015-07-09 09:41:27 / Type: =F
Name: newStream.txt / DateModified: modified=2015-07-09 09:41:59 / Type: =F
Name: myTestFile2.txt / DateModified: modified=2014-05-05 09:19:31 / Type: =F
Name: JSONRESTProxy_8.txt / DateModified: modified=2014-05-04 14:28:09 / Type: =F
Name: JSONRESTProxy_10.txt / DateModified: modified=2014-05-04 14:29:04 / Type: =F
Name: hsaa_msgs.txt / DateModified: modified=2015-06-25 13:02:16 / Type: =F
Name: hl7.txt / DateModified: modified=2014-07-30 12:09:18 / Type: =F

アダプターのメソッドは背後で %File:FileSet クエリを使用していることに注意してください。

0
0 186
記事 Toshihiko Minamoto · 5月 17, 2022 9m read

はじめに

前の記事では、ObjectScript Package Manager を使用してユニットテストを実行するためのパターンについて説明しました。 この記事では、さらに一歩踏み込み、GitHub Actions を使用してテストの実行とレポート作成を行います。 私の Open Exchange プロジェクトの 1 つである AppS.REST に CI を実行するのが、やる気の出るユースケースでしょう(この導入編の記事は、こちらにあります)。 この記事のスニペットが使用されている完全な実装は、GitHub でご覧ください。ObjectScript Package Manager を使って他のプロジェクトで CI を実行するためのテンプレートとして簡単に利用できます。

紹介する実装の機能は以下のとおりです。

  • ObjectScript パッケージの構築とテスト
  • codecov.io によるテストカバレッジ測定のレポート(TestCoverage パッケージを使用)
  • テスト結果に関するレポートのビルドアーティファクトとしてのアップロード

ビルド環境

GitHub Actions に関する完全なドキュメントはこちらにあります。この記事の目的に準じ、この例で紹介される側面だけを詳しく確認します。

GitHub Actions のワークフローは、構成可能な一連のイベントによってトリガーされ、順番に、または並行して実行できる多数のジョブで構成されています。 それぞれのジョブには一連のステップがあります。このサンプルアクションのステップは、少し後の方で詳しく説明します。 これらのステップは、GitHub で提供されているアクションへの参照で構成されているか、単にシェルコマンドである場合があります。 この例での最初のボイラープレートのスニペットは、以下のようになります。

# Continuous integration workflow
name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events in all branches
on: [push, pull_request]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    
    env:
      # Environment variables usable throughout the "build" job, e.g. in OS-level commands
      package: apps.rest
      container_image: intersystemsdc/iris-community:2019.4.0.383.0-zpm
      # More of these will be discussed later...
    
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # These will be shown later...

この例では、多数の環境変数が使用されています。 この例を ObjectScript Package Manager を使用して他のパッケージに適用する場合、ほとんどの変数を変更する必要はありませんが、一部には変更が必要です。

    env:
      # ** FOR GENERAL USE, LIKELY NEED TO CHANGE: **
      package: apps.rest
      container_image: intersystemsdc/iris-community:2019.4.0.383.0-zpm
      
      # ** FOR GENERAL USE, MAY NEED TO CHANGE: **
      build_flags: -dev -verbose # Load in -dev mode to get unit test code preloaded
      test_package: UnitTest
      
      # ** FOR GENERAL USE, SHOULD NOT NEED TO CHANGE: **
      instance: iris
      # Note: test_reports value is duplicated in test_flags environment variable
      test_reports: test-reports
      test_flags: >-
       -verbose -DUnitTest.ManagerClass=TestCoverage.Manager -DUnitTest.JUnitOutput=/test-reports/junit.xml
       -DUnitTest.FailuresAreFatal=1 -DUnitTest.Manager=TestCoverage.Manager
       -DUnitTest.UserParam.CoverageReportClass=TestCoverage.Report.Cobertura.ReportGenerator
       -DUnitTest.UserParam.CoverageReportFile=/source/coverage.xml

これを独自のパッケージに適応させるには、独自のパッケージ名と優先するコンテナイメージをドロップしてください(zpm を含める必要があります。https://hub.docker.com/r/intersystemsdc/iris-community をご覧ください)。 また、ユニットテストパッケージが独自のパッケージの規則に一致するように変更することもお勧めします(ユニットテストを実行する前に、読み込みとコンパイルを行ってして依存関係の読み込み/コンパイルを処理する必要がある場合。私自身、このパッケージではユニットテストに特有の奇妙な問題に遭遇したためですが、他のケースでは関連性がないかもしれません)。

インスタンス名と test_reports ディレクトリは、他の使用では変更する必要はありませんし、test_flags には有効なデフォルトセットが備わっています。これらは、ユニットテストが失敗すると、ビルドを失敗としてフラグする機能をサポートしており、jUnit 形式のテスト結果とコードカバレッジレポートのエクスポートも処理できます。

ビルドのステップ

GitHub リポジトリの確認

この例では、テストされているリポジトリと、Forgery のフォーク(ユニットテストで必要であるため)の2 つのリポジトリを確認する必要があります。

    # Checks out this repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v2
    
    # Also need to check out timleavitt/forgery until the official version installable via ZPM
    - uses: actions/checkout@v2
      with:
        repository: timleavitt/forgery
        path: forgery

$GITHUB_WORKSPACE は非常に重要な環境変数で、このすべてが実行するルートディレクトリを表します。 権限の観点では、そのディレクトリ内ではほぼあらゆる操作を実行できますが、他の場所では問題に遭遇するかもしれません。

InterSystems IRIS コンテナの実行

最終的にテスト結果レポートを配置するディレクトリをセットアップしたら、ビルドの InterSystems IRIS Community Edition(+ZPM)コンテナを実行します。

    - name: Run Container
      run: |
        # Create test_reports directory to share test results before running container
        mkdir $test_reports
        chmod 777 $test_reports
        # Run InterSystems IRIS instance
        docker pull $container_image
        docker run -d -h $instance --name $instance -v $GITHUB_WORKSPACE:/source -v $GITHUB_WORKSPACE/$test_reports:/$test_reports --init $container_image
        echo halt > wait
        # Wait for instance to be ready
        until docker exec --interactive $instance iris session $instance &lt; wait; do sleep 1; done

コンテナに共有されている GitHub ワークスペース(コードを読み込めるようにするため。ここにはテストカバレッジ情報もレポートします)と jUnit テスト結果を配置する別のディレクトリの 2 つのボリュームがあります。

「docker run」が終了しても、インスタンスが完全に開始され、コマンドを処理する準備が整っているわけではありません。 インスタンスの準備が整うまで、iris セッションを通じて「halt」コマンドを実行し続けます。これは失敗となりますが、(最終的に)成功になるまで、1 秒に 1 回ずつ試行し続けます。成功になれば、インスタンスの準備は完了です。

テスト関連ライブラリのインストール

このユースケースでは、他に TestCoverageForgery という 2 つのライブラリを使用してテストします。 TestCoverage は、Community Package Manager を介して直接インストールできますが、Forgery については、(現時点では)zpm「load」を介して読み込む必要があります。いずれのアプローチも有効です。

    - name: Install TestCoverage
      run: |
        echo "zpm \"install testcoverage\":1:1" > install-testcoverage
        docker exec --interactive $instance iris session $instance -B &lt; install-testcoverage
        # Workaround for permissions issues in TestCoverage (creating directory for source export)
        chmod 777 $GITHUB_WORKSPACE
    
    - name: Install Forgery
      run: |
        echo "zpm \"load /source/forgery\":1:1" > load-forgery
        docker exec --interactive $instance iris session $instance -B &lt; load-forgery

一般的には、ファイルにコマンドを記述して空、IRIS セッションで実行します。 ZPM コマンドに追加されている「:1:1」は、エラーが発生したらエラーコードとともにプロセスを終了し、エラーが発生しない場合は最後に停止することを示しています。つまり、エラーが発生した場合は、失敗したビルドステップとして報告されるため、ファイルの最後に「halt」コマンドを追加する必要はありません。

パッケージの構築とテスト

最後に、パッケージのテストを実際に構築して実行しましょう。 これはいたって単純です。始めの方で定義した $build_flags/$test_flags 環境変数が使用されていることに注目してください。

    # Runs a set of commands using the runners shell
    - name: Build and Test
      run: |
        # Run build
        echo "zpm \"load /source $build_flags\":1:1" > build
        # Test package is compiled first as a workaround for some dependency issues.
        echo "do \$System.OBJ.CompilePackage(\"$test_package\",\"ckd\") " > test
        # Run tests
        echo "zpm \"$package test -only $test_flags\":1:1" >> test
        docker exec --interactive $instance iris session $instance -B &lt; build && docker exec --interactive $instance iris session $instance -B &lt; test && bash &lt;(curl -s https://codecov.io/bash)

これは、見たことのあるパターンに則っています。ファイルにコマンドを書き出してから、そのファイルを iris セッションの入力として使用しています。

最後の行の最後の部分は、コードカバレッジの結果を codecov.io にアップロードしています。 とても簡単です!

ユニットテスト結果のアップロード

ユニットテストが失敗したとしましょう。 ビルドログに戻って、どこで間違ってしまったのかを見つけ出すのは本当に面倒な作業ですが、有用なコンテキストが得られるかもしれません。 作業を楽にするために、jUnit 形式の結果をアップロードできるだけでなく、サードパーティのプログラムを実行して、見栄えの良い HTML レポートに変換することも可能です。

    # Generate and Upload HTML xUnit report
    - name: XUnit Viewer
      id: xunit-viewer
      uses: AutoModality/action-xunit-viewer@v1
      if: always()
      with:
        # With -DUnitTest.FailuresAreFatal=1 a failed unit test will fail the build before this point.
        # This action would otherwise misinterpret our xUnit style output and fail the build even if
        # all tests passed.
        fail: false
    - name: Attach the report
      uses: actions/upload-artifact@v1
      if: always()
      with:
        name: ${{ steps.xunit-viewer.outputs.report-name }}
        path: ${{ steps.xunit-viewer.outputs.report-dir }}

このほとんどは、https://github.com/AutoModality/action-xunit-viewer の Readme から拝借したものです。

最終結果

このワークフローの結果は、以下でご覧いただけます。

intersystems/apps-rest での CI ジョブのログ(ビルドアーティファクトを含む): https://github.com/intersystems/apps-rest/actions?query=workflow%3ACI
テストカバレッジレポート: https://codecov.io/gh/intersystems/apps-rest

ご質問がございましたら、お知らせください!

0
0 198
記事 Toshihiko Minamoto · 10月 5, 2021 2m read

メッセージビューアはメッセージを再送信できますが、大量(>100件)のメッセージを再送信するには適していません。  それを行うには、以下のようなCache Object Scriptコードを使用する必要があります。

Class Sample.Resender Extends %RegisteredObject
{
ClassMethod Resend()
{
//2016-06-15から2016-06-20の間に'FromComponent'から'ToComponent'に送られたすべてのメッセージを再送信
&sql(DECLARE C1 CURSOR FOR
 SELECT ID INTO :id FROM Ens.MessageHeader
 WHERE SourceConfigName='FromComponent' AND TargetConfigName='ToComponent'
 AND TimeCreated BETWEEN '2016-06-15' AND '2016-06-20')
&sql(OPEN C1)
&sql(FETCH C1)
set tSC = $$$OK
while (SQLCODE = 0) {
//idには1つのメッセージのidが含まれます。 それを再送信
set tSC = ##class(Ens.MessageHeader).ResendDuplicatedMessage(id)
quit:$$$ISERR(tSC)
&sql(FETCH C1)
}
&sql(CLOSE C1)
quit tSC
}
}

このコードに、エラーチェックをさらに改善したコードや、問題が発生した場合に中断したところから再開するコードなどを追加することもできます。

埋め込みSQLとEns.MessageHeaderメソッドのResendDuplicatedMessageのドキュメントは次からアクセスできます。

http://docs.intersystems.com/ens20161/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_esql#GSQL_esql_cursor

http://docs.intersystems.com/ens20152/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=ENSLIB&CLASSNAME=Ens.MessageHeader#METHOD_ResendDuplicatedMessage 

0
0 122
記事 Toshihiko Minamoto · 8月 18, 2021 13m read

PHP はその公開当初から、多くのライブラリや市場に出回っているほぼすべてのデータベースとの統合をサポートしていることでよく知られています(またそのことで批判を受けてもいます)。 にもかかわらず、何らかの不可解な理由により、グローバル変数については階層型データベースをサポートしませんでした。

グローバル変数は階層情報を格納するための構造です。 Key-Value型データベースにある程度似ていますが、キーを次のようにマルチレベルにできるという点で異なっています。


Set ^inn("1234567890", "city") = "Moscow"
Set ^inn("1234567890", "city", "street") = "Req Square"
Set ^inn("1234567890", "city", "street", "house") = 1
Set ^inn("1234567890", "year") = 1970
Set ^inn("1234567890", "name", "first") = "Vladimir"
Set ^inn("1234567890", "name", "last") = "Ivanov"

この例では、マルチレベルの情報は、ビルトインのObjectScript言語を使用して、グローバル変数の**^innに保存されています。 グローバル変数^inn**は、ハードドライブに保存されています(これは、最初の「^」記号で示されています)。

PHP からグローバル変数を操作するには、PHP モジュールによって追加される新しい関数が必要となります。これについて、以下で説明します。

グローバル変数は、階層を操作するための多数の関数をサポートしています。固定レベルと縦型のトラバーサルツリーでツリー全体と個別のノードの削除、コピー、および貼り付けを行えます。 また、質の高いデータベースと同様に、ACID トランザクションもサポートされています。 これらは次の2つの理由により、非常に迅速に行われます(一般的なPCで、1秒間に105~106の挿入が行われます)。

  1. グローバル変数は SQL に比べると、より低レベルの抽象化である。
  2. ベースは数十年もの間グローバルスコープで稼働しており、この間に洗練され、コードは完全に最適化されてきた。

グローバル変数について詳しくは、「グローバル変数: データ管理の魔法の剣」という連載記事をご覧ください。

パート1.
ツリー。 パート2.
スパースアレイ。 パート3.

この業界では、グローバル変数は主に、医療、個人データ、銀行などの構造化されていないまばらな情報のストレージシステムで使用されています。

私はPHPを気に入っており、開発作業でも使用しているため、グローバル変数を使って色々と試してみたいと思いました。 IRISとCaché用のPHPモジュールは存在しなかったため、 InterSystemsに問い合わせ、作成するよう依頼しました。 InterSystemsは教育助成金の一環として開発を後援してくれたおかげで、私の院生とともにモジュールを作成することになりました。

一般的に、InterSystems IRISはマルチモデルDBMSであるため、ODBCを通じてSQLを使ってPHPから操作することができますが、私が興味を持っていたのはグローバルであったため、それに使用できるコネクタは存在しなかったのです。

それはさておき、このモジュールはPHP 7.xで利用できます(7.0~7.2でテストしました)。 現在、同じホストにインストールされているInterSystems IRISとCachéでのみ動作します。

OpenExchangeのModuleページ(InterSystems IRISとCachéの開発者向けのプロジェクトとアドオンのディレクトリ)。

開発者同士で関連する体験をシェアできるDISCUSSセクションが設けられています。

こちらからダウンロードしてください。

https://github.com/intersystems-community/php_ext_iris コマンドラインからリポジトリをダウンロードする場合:

git clone https://github.com/intersystems-community/php_ext_iris

 

モジュールのインストール手順(英語・ロシア語)。

モジュールの関数:

<td>
  説明
</td>
<td>
   ノードの値を設定します。iris_set($global, $subscript1, ..., $subscriptN, $value); iris_set($global, $value); 戻り値: true または false(エラーの場合)。この関数のすべてのパラメーターは文字列か数値です。 最初のパラメーターはグローバルの名前で、次にインデックス、そして最後のパラメーターは値です。  iris_set('^time',1); iris_set('^time', 'tree', 1, 1, 'value'); ObjectScript equivalent:   Set ^time = 1 Set ^time("tree", 1, 1) = "value" iris_set($arrayGlobal, $value);パラメーターは2つしかありません。1つ目はグローバルの名前とすべてのインデックスを含む配列で、2つ目は値です。   $node = ['^time', 'tree', 1, 1]; iris_set($node,'value');
</td>
<td>
   ノードの値を取得します。 戻り値: 値(数値または行)、NULL(値が定義されていません)、またはFALSE(エラーの場合)。   iris_get($global, $subscript1, ..., $subscriptN); iris_get($global); この関数のすべてのパラメーターは行または数値です。 1つ目はグローバルの名前で、残りはサブスクリプトです。 グローバルにはサブスクリプトがない場合があります。   $res = iris_get('^time'); $res1 = iris_get('^time', 'tree', 1, 1); iris_get($arrayGlobal); 唯一のパラメーターは、グローバルの名前とそのすべてのサブスクリプトが格納されている配列です。   $node = ['^time', 'tree', 1, 1]; $res = iris_get($node);
</td>
<td>
   ノードの値を削除します。 戻り値: TRUEまたはFALSE(エラーの場合)。   この関数はノードの値のみを削除し、下位のブランチには影響しないことに注意してください。   iris_zkill($global, $subscript1, ..., $subscriptN); iris_zkill($global); この関数のすべてのパラメーターは行または数値です。 1つ目はグローバルの名前で、残りはサブスクリプトです。 グローバルにはサブスクリプトがない場合があります。   $res = iris_zkill('^time'); // 下位ブランチは削除されません。 $res1 = iris_zkill('^time', 'tree', 1, 1); iris_zkill($arrayGlobal); 唯一のパラメーターは、グローバルの名前とそのすべてのサブスクリプトが格納されている配列です。   $a = ['^time', 'tree', 1, 1]; $res = iris_zkill($a);
</td>
<td>
   ノードとすべての子孫ブランチを削除します。 戻り値: TRUEまたはFALSE(エラーの場合)。   iris_kill($global, $subscript1, ..., $subscriptN); iris_kill($global); この関数のすべてのパラメーターは行または数値です。 1つ目はグローバルの名前で、残りはインデックスです。 グローバルにはインデックスがない場合があり、その場合は完全に削除されます。   $res1 = iris_kill('^example', 'subscript1', 'subscript2'); $res = iris_kill('^time'); // グローバルは完全に削除されます。 iris_kill($arrayGlobal); 唯一のパラメーターは、グローバルの名前とそのすべてのサブスクリプトが格納されている配列です。   $a = ['^time', 'tree', 1, 1]; $res = iris_kill($a);
</td>
<td>
   指定されたレベルでグローバルのブランチをトラバースします。戻り値: 同じレベルのグローバルの前のノードのフルネームが格納されている配列、またはFALSE(エラーの場合)。   iris_order($global, $subscript1, ..., $subscriptN); この関数のすべてのパラメーターは文字列または数値です。 1つ目のパラメーターはグローバルの名前で、残りはサブスクリプトです。PHPとObjectScript相当の使用方法: iris_order('^ccc','new2','res2'); // $Order(^ccc("new2", "res2")) iris_order($arrayGlobal); 唯一のパラメーターは、グローバルの名前と最初のノードのサブスクリプトが格納されている配列です。 $node = ['^inn', '1234567890', 'city']; for (; $node !== NULL; $node = iris_order($node)) { echo join(', ', $node).'='.iris_get($node)."\n"; } Returns: ^inn, 1234567890, city=Moscow ^inn, 1234567890, year=1970
</td>
<td>
   指定されたレベルでグローバルのブランチを逆順にトラバースします。戻り値: 同じレベルのグローバルの前のノードのフルネームが格納されている配列、またはFALSE(エラーの場合)。   iris_order_rev($global, $subscript1, ..., $subscriptN); この関数のすべてのパラメーターは行または数値です。 1つ目はグローバルの名前で、残りはサブスクリプトです。 PHPとObjectScript相当の使用方法: iris_order_rev('^ccc','new2','res2'); // $Order(^ccc("new2", "res2"), -1) iris_order_rev($arrayGlobal); 唯一のパラメーターは、グローバルの名前と最初のノードのサブスクリプトが格納されている配列です。   $node = ['^inn', '1234567890', 'name', 'last']; for (; $node !== NULL; $node = iris_order_rev($node)) { echo join(', ', $node).'='.iris_get($node)."\n"; } 戻り値: ^inn, 1234567890, name, last=Ivanov ^inn, 1234567890, name, first=Vladimir
</td>
<td>
   グローバルの縦型トラバース 戻り値: 下位ノード(使用可能な場合)またはグローバルの次のノード(埋め込みノードがない場合)のフルネームが含まれている配列。   iris_query($global, $subscript1, ..., $subscriptN); この関数のすべてのパラメーターは文字列または数値です。 1つ目はグローバルの名前で、残りはサブスクリプトです。 PHPとObjectScript相当の使用方法: iris_query('^ccc', 'new2', 'res2'); // $Query(^ccc("new2", "res2")) iris_query($arrayGlobal); 唯一のパラメーターは、グローバルの名前と最初のノードのインデックスが格納されている配列です。   $node = ['^inn', 'city']; for (; $node !== NULL; $node = iris_query($node)) { echo join(', ', $node).'='.iris_get($node)."\n"; } 戻り値: ^inn, 1234567890, city=Moscow ^inn, 1234567890, city, street=Req Square ^inn, 1234567890, city, street, house=1 ^inn, 1234567890, name, first=Vladimir ^inn, 1234567890, name, last=Ivanov ^inn, 1234567890, year=1970 この順序は挿入時にグローバル内で自動的に昇順にソートされるため、設定した順序とは異なります。
</td>
<td>
   データベースのディレクトリをセットアップします。戻り値: TRUEまたはFALSE(エラーの場合)。   iris_set_dir('/InterSystems/Cache/mgr'); これはデータベースに接続する前に実行する必要があります。
</td>
<td>
   データベースコマンドを実行します。戻り値: TRUEまたはFALSE(エラーの場合)。 iris_exec('kill ^global(6)'); // グローバルを削除するObjectScriptコマンド
</td>
<td>
  データベースに接続します。
</td>
<td>
  DBとの接続を閉じます。
</td>
<td>
  エラーコードを取得します。
</td>
<td>
  エラーのテキストによる説明を取得します。
</td>
PHP関数
データの操作
iris_set($node, value)
iris_get($node)
iris_zkill($node)
iris_kill($node)
iris_order($node)
iris_order_rev($node)
iris_query($CmdLine)
サービス関数
iris_set_dir($FullPath)
iris_exec($CmdLine)
iris_connect($login, $pass)
iris_quit()
iris_errno()
iris_error()

 

 
モジュールを試してみたい場合は、dockerコンテナーの実装などを確認してください。
 

特にDCや使用したい方のために、Caché用php-moduleがセットアップされた仮想マシンを実行しています。

 
InterSystems Cachéにモジュールを自分でインストールする場合

単なる好奇心で、私のPC(AMD FX-9370@4700Mhz 32GB、LVM、SATA SSD)のdockerコンテナーで新しい値をデータベースに挿入する速度をチェックするプリミティブテストを2つ実行しました。

  • 100万個の新しいノードをグローバルに挿入するのに、1.81秒掛かりました(1秒あたり552Kの挿入)。
  • 同じグローバルの値を1,000,000回更新するのに、1.98秒掛かりました(1秒当たり505Kの更新)。 興味深かったのは、挿入が更新よりも早く行われたということです。 どうやらこれは、迅速な挿入を目的としたデータベースの初期最適化の結果のようです。

明らかに、これらのテストは原始的であり、コンテナー内で実行されるため、100%の正確性または有用性があるとは考えられません。 PCIe SSDにディスクシステムを備えたより強力なハードウェアでは、1秒あたり数千万の挿入を達成可能です。

作成中の機能とその状況

  1. トランザクションを操作するための便利な関数を追加できます(iris_execで使用できます)。
  2. グローバル構造全体を返す関数は実装されていません。PHPからグローバルをトラバースする関数についても同様です。
  3. PHP配列をサブツリーとして保存する関数は実装されていません。
  4. ローカルデータベース変数へのアクセスは実装されていません。 iris_setを使用した方が良いですが、iris_execのみを使用してください。
  5. 逆順での縦型グローバルトラバースは実装されていません。
  6. メソッドを使ったオブジェクト経由のデータベースアクセス(現在の関数に類似)は実装されていません。

現在のモジュールはまだ本番対応とは言えません。高負荷やメモリリークについてのテストは行われていません。 ただし、必要だという方がいらっしゃれば、いつでもご連絡ください(Sergey Kamenev宛: sukamenev@gmail.com)。

結論

グローバルは特定のデータタイプ(医療、個人データなど)に強力で高速な機能を提供しているにも関わらず、長い間、PHPの世界とグローバル変数での階層型データベースの世界では、実質的に重なることがありませんでした。

このモジュールをきっかけに、PHPプログラマーがグローバル変数を試すようになり、ObjectScriptプログラマーがPHPでウェブインターフェースを簡単に開発できるようになることを願っています。

追伸 最後までお読みいただき、ありがとうございました!

0
0 243
記事 Toshihiko Minamoto · 7月 1, 2021 5m read

クラスのコンパイル時に、定義済みのプロパティ、クエリ、またはインデックスごとに対応する複数のメソッドが自動的に生成されます。 これらのメソッドは非常に便利です。 この記事では、その一部について説明します。

プロパティ

「Property」というプロパティを定義したとしましょう。 次のメソッドが自動的に利用できるようになります(太字のPropertyは変化する部分でプロパティ名になります)。

ClassMethod PropertyGetStored(id)

このメソッドは、データ型プロパティの場合は論理値を返し、オブジェクトプロパティの場合はIDを返します。 これはクラスのデータグローバルへの、ラップされたグローバル参照であり、特異なプロパティ値を取得する上でもっとも手早い方法です。 このメソッドは、保存されたプロパティでのみ利用できます。

Method PropertyGet()

プロパティgetterである。 再定義可能です。

Method PropertySet(val) As %Status

プロパティsetterである。 再定義可能です。


オブジェクトプロパティ

オブジェクトプロパティの場合、IDとOIDアクセスに関連するいくつかの追加メソッドを利用できるようになります。

Method PropertySetObjectId(id)

このメソッドはIDでプロパティ値を設定するため、オブジェクトを開いてプロパティ値として設定する必要はありません。

Method PropertyGetObjectId()

このメソッドは、プロパティ値のIDを返します。

Method PropertySetObject(oid)

このメソッドは、OIDでプロパティ値を設定します。

Method PropertyGetObject()

このメソッドは、プロパティ値のOIDを返します。


データ型プロパティ

データ型プロパティの場合、異なる形式間で変換するためのほかのメソッドがいくつか利用できるようになります。

ClassMethod PropertyDisplayToLogical(val)
ClassMethod PropertyLogicalToDisplay(val)
ClassMethod PropertyOdbcToLogical(val)
ClassMethod PropertyLogicalToOdbc(val)
ClassMethod PropertyXSDToLogical(val)
ClassMethod PropertyLogicalToXSD(val)
ClassMethod PropertyIsValid(val) As %Status

valが有効なプロパティ値であるかどうかをチェックします。

ClassMethod PropertyNormalize(val)

正規化された論理値を返します。

注意事項

  • 関係はプロパティであり、これらのメソッドで取得/設定できます。
  • 入力値(val)は、形式変換メソッドを除き、必ず論理値です。

  • インデックス

    「Index」というインデックスの場合、次のメソッドを自動的に利用できるようになります。

    ClassMethod IndexExists(val) As %Boolean

    このvalを持つオブジェクトが存在するかどうかに応じて、1または0を返します。valはインデックス付きのプロパティの論理値です。


    一意のインデックス

    一意のインデックスの場合、追加メソッドを利用できるようになります。

    ClassMethod IndexExists(val, Output id) As %Boolean

    このvalを持つオブジェクトが存在するかどうかに応じて、1または0を返します。valはインデックス付きのプロパティの論理値です。 また、オブジェクトIDがある場合は、第2引数として返します。

    ClassMethod IndexDelete(val, concurrency = -1) As %Status

    インデックスの値がvalのエントリを削除します。

    ClassMethod IndexOpen(val, concurrency, sc As %Status)  

    インデックスの値がvalの既存のオブジェクトを返します。

    注意事項:

    a)インデックスは複数のプロパティに基づいている可能性があるため、メソッドシグネチャは、入力として複数の値を持つように変更されます。例として、次のインデックスを見てみましょう。

    Index MyIndex On (Prop1, Prop2);

    この場合、IndexExistsメソッドには次のシグネチャがあります。

    ClassMethod IndexExists(val1, val2) As %Boolean

    val1はProp1値に対応し、val2はProp2値に対応します。 その他のメソッドも同じロジックに従います。

    b)Cachéは、IDフィールド(RowID)にインデックスを作成するIDKEYインデックスを生成します。 ユーザーが再定義することが可能で、複数のプロパティを含むこともできます。 たとえば、クラスにプロパティが定義されているかをチェックするには、次を実行します。

    Write ##class(%Dictionary.PropertyDefinition).IDKEYExists(class, property)

    c)すべてのインデックスメソッドは、論理値をチェックします。

    d) ドキュメント

    クエリ

    「Query」というクエリの場合(単純なSQLクエリまたはカスタムクラスクエリのどちらでも構いません。これに関する記事をご覧ください)、Funcメソッドが生成されます。

    ClassMethod QueryFunc(Arg1, Arg2) As %SQL.StatementResult

    このメソッドは、クエリの反復処理に使用される%SQL.StatementResultを返します。 たとえば、SamplesネームスペースのSample.Personクラスには、1つのパラメーターを受け入れるByNameクエリがあり、 次のコードを使って、オブジェクトコンテキストから呼び出すことができます。

    Set ResultSet=##class(Sample.Person).ByNameFunc("A")
    While ResultSet.%Next() { Write ResultSet.Name,! }

    また、GitHubには、これらのメソッドを実演するデモクラスがあります。

    0
    1 201
    記事 Toshihiko Minamoto · 5月 4, 2021 2m read

    ObjectScriptには、エラー(ステータスコード、例外、SQLCODEなど)を処理する方法が少なくとも3つあります。 ほとんどのシステムコードにはステータスが使用されていますが、例外は、いくつかの理由により、より簡単に処理することができます。 レガシーコードを使用している場合、さまざまな手法の変換にいくらか時間が掛かりますが、 参考として、次のスニペットをよく使用しています。 皆さんのお役にも立てればと思います。

    ///SQLCODEのステータス:set st = $$$ERROR($$$SQLError, SQLCODE, $g(%msg))  //埋め込みSQLset st = $$$ERROR($$$SQLError, rs.%SQLCODE, $g(rs.%Message)) //動的SQL///SQLCODEの例外:throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) //埋め込みSQLthrow ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) //動的SQLthrow:(SQLCODE'=0)&&(SQLCODE'=100) ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) //クエリに成功する場合、またはデータがない場合はスローしない///ステータスの例外:$$$ThrowOnError(st)///例外のステータス:set st = err.AsStatus()///カスタムエラーステータスの作成:set st = $$$ERROR($$$GeneralError,"Custom error message")///カスタム例外をスロー:$$$ThrowStatus($$$ERROR($$$GeneralError,"Custom error message"))///ステータス付きのSOAPエラーの処理:try {  //SOAPリクエストコード} Catch err {  If err.Name["ZSOAP" {    Set st = %objlasterror  } Else {    Set st = err.AsStatus()  }}return st///カスタム例外クラスを定義Class App.Exceptions.SomeException Extends %Exception.AbstractException{Method OnAsStatus() As %Status{  return $$$ERROR($$$GeneralError,"Custom error message")}}///カスタム例外のスローとキャッチtry {  throw ##class(App.Exceptions.SomeException).%New()} catch err {  if err.%ClassName(1) = ##class(App.Exceptions.SomeException).%ClassName(1) {    //この種の例外に特有の処理  }}</pre></body></html>
    0
    0 588
    記事 Toshihiko Minamoto · 4月 28, 2021 5m read

    数年ほど前、Caché Foundationsの講座(現「Developing Using InterSystems Objects and SQL」)において、%UnitTestフレームワークの基礎を講義していたことがあります。 その時、ある受講者から、ユニットテストを実行している間に、パフォーマンス統計を収集できるかどうかを尋ねられました。 それから数週間後、この質問に答えるために、%UnitTestの例にコードを追加したのですが、 ようやく、このコミュニティでも共有することにしました。

    Processクラスの%SYSTEMには、プロセスについて収集可能なメトリクスが(所要時間以外に)いくつか提供されています。

    • 所要時間
    • 実行された行
    • グローバル参照
    • システムCPU時間
    • ユーザーCPU時間
    • ディスク読み取り時間
    上記の統計を収集する任意のユニットテストを有効にするには、%UnitTest.TestCaseのサブクラスを作成してプロパティを追加します。
     
    Class Performance.TestCase Extends %UnitTest.TestCase
    {
    Property Duration As %Time;
    Property Lines As %Integer;
    Property Globals As %Integer;
    Property SystemCPUTime As %Integer;
    Property UserCPUTime As %Integer;
    Property DiskReadTime As %Integer;
    }
     
    作成する特定のユニットテストは、%UnitTest.TestCaseではなく、新しいサブクラスから継承されている必要があります。
     
    サブクラスではOnBeforeOneTest()を使って、各ユニットテストの統計データを初期化します。 DiskReadTime以外、現在値でプロパティを初期化します。
     
    /// パフォーマンス統計を初期化します
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">Method OnBeforeOneTest(testname As %String) As %Status</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">{</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // 現在の値で初期化します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Duration = $zh</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Lines = $system.Process.LinesExecuted()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Globals = $system.Process.GlobalReferences()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><span style="">    set ..SystemCPUTime = $piece(CPUTime, ",", 1)</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..UserCPUTime = $piece(CPUTime, ",", 2)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ディスク時間を0にリセットし、カウントを開始します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.ResetDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.EnableDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    return $$$OK</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">}</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
     
    <div>
      <span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none">OnAfterOneTest() を使って、各ユニットテストの統計データを確定します。 DiskReadTime以外、現在の値から初期値を減算します。</span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
       
    </div>
    
    <div>
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">/// パフォーマンス統計を確定します </font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">/// ここに、分析用にカウンターを別のテーブルに保存するためのコードを追加できます。</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">Method OnAfterOneTest(testname As %String) As %Status</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">{</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><span style="">    set ..Duration = $zh - ..Duration</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Lines = $system.Process.LinesExecuted() - ..Lines</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..Globals = $system.Process.GlobalReferences() - ..Globals</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <div>
          <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set CPUTime = $system.Process.GetCPUTime()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
        </div>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..SystemCPUTime = $piece(CPUTime, ",", 1) - ..SystemCPUTime</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..UserCPUTime = $piece(CPUTime, ",", 2) - ..UserCPUTime</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ディスク読み取り時間を取得し、カウントを停止します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set ..DiskReadTime = $system.Process.DiskReadMilliseconds()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $system.Process.DisableDiskReadTiming()</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    // ユニットテストログにメッセージを追加します</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set msg = "Performance: " _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span> 
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              "Duration: " _           ..Duration _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Lines: " _            ..Lines _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Globals: " _          ..Globals _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", System CPU Time: " _ (..SystemCPUTime / 1000) _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", User CPU Time: " _   (..UserCPUTime / 1000) _</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">              ", Disk Read Time: " _  (..DiskReadTime / 1000)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do $$$LogMessage(msg)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    return $$$OK</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
      
      <div>
        <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">}</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
      </div>
    </div>
    
     
    ちょっとした技がもう1つあります。 統計を収集するかしないかを指定して、ユニットテストを実行したほうが良いかもしれません。 したがって、ユニットテストを呼び出すコードに引数(%Boolean 1または0)を追加し、何らかの方法で渡す必要があります。 テストを実際に実行するメソッド(RunTest() またはほかのRun*() メソッドの1つ)は、第3引数に配列を取り、参照形式で渡します。 次は、そのサンプルコードです。
     
        // logging引数(1または0)を保持する配列を作成し、それを参照で渡します
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    set p("logging") = logging</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
    <div>
      <span style="font-size:12px"><span style="caret-color:#000000"><span style="color:#000000"><span style=""><span style="font-style:normal"><span style="font-variant-caps:normal"><span style="font-weight:normal"><span style="letter-spacing:normal"><span style="orphans:auto"><span style="text-transform:none"><span style=""><span style="widows:auto"><span style="word-spacing:0px"><span style="-webkit-text-size-adjust:auto"><span style="text-decoration:none"><font><font face="Consolas">    do ##class(%UnitTest.Manager).RunTest(test, qualifiers, .p)</font></font></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
    </div>
    
     
    配列に渡す値は、OnBeforeOneTest() と OnAfterOneTest() でアクセスできます。 これを両方のメソッドの最初の行に追加します。
     
       if (..Manager.UserFields.GetAt("logging") = 0) { return $$$OK }
     
    以上です! 皆さんのご質問、コメント、その他のアイデアをぜひお聞かせください。
    0
    0 174
    記事 Toshihiko Minamoto · 4月 22, 2021 3m read

    先日、永続クラスとシリアルクラスからSwagger仕様を生成する必要がありました。そこで、その時のコードを公開することにします(完全なコードではないため、アプリケーション固有の部分を変更する必要がありますが、まずは出発点として使用できます)。 こちらからご利用ください。

    次のクラスがあるとしましょう。

    クラス

    このSwagger定義を自動的に生成できます。

     REST.Test.Person:
       type: "object"
       properties:
         Age:
           type: "integer"
         DOB:
           type: "string"
         FavoriteColors:
           type: "array"
           items:
             type: "string"
         FavoriteNumbers:
           type: "object"
         Home:
           $ref: "#/definitions/REST.Test.Address"
         Name:
           type: "string"
         Office:
           $ref: "#/definitions/REST.Test.Address"
         SSN:
           type: "string"
         Spouse:
           $ref: "#/definitions/REST.Test.Person"
     REST.Test.Address:
       type: "object"
       properties:
         City:
           type: "string"
         State:
           type: "string"
         Street:
           type: "string"
         Zip:
           type: "string"
    

    メインのメソッド: Utils.YAML:GenerateClasses

    テスト実行: do ##class(Utils.YAML).Test()

    0
    0 170
    記事 Toshihiko Minamoto · 12月 23, 2020 3m read

    新しい動的 SQL クラス(%SQL.Statement および %StatementResult)のパフォーマンスは %ResultSet より優れてはいますが、%ResultSet の使用方法をせっかく学習したので、しばらくの間新しい方を使用せずにいましたが、 やっとチートシートを作ったので、新しいコードを書いたり古いコードを書き直す際に役立てています。 皆さんのお役に立てればいいなと思っています。

    次に示すのは、私のチートシートの詳細版です。

    <th>
      %ResultSet::%New()
    </th>
    
    <th>
      %SQL.Statement::%New()
    </th>
    
    <td>
         Prepare() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         %Prepare() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         Execute() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         %Execute() インスタンスメソッドを呼び出す
    </td>
    
    <td>
         前のステップがステータスを返すので、それを確認
    </td>
    
    <td>
         前のステップが %SQL.StatementResult のインスタンスを返すので、次のステップでそれを使用
    </td>
    
    <td>
         Next() インスタンスメソッドを呼び出す(while ループでイテレートなど)
    </td>
    
    <td>
         %Next() インスタンスメソッドを呼び出す(while ループでイテレートなど)
    </td>
    
    <td>
         GetData() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    <td>
         %GetData() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    <td>
         %Get() インスタンスメソッドを呼び出して、列番号で列を取得
    </td>
    
    1
    2
    3
    4
    5
    6
    7
       Get() または Data() インスタンスメソッドを呼び出して、列番号で列を取得

     

    そして、これが私が実際に使用している簡易版チートシートです。

    <th>
      %ResultSet::%New()
    </th>
    
    <th>
      %SQL.Statement::%New()
    </th>
    
    <td>
         Prepare()
    </td>
    
    <td>
         %Prepare()
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         Execute()
    </td>
    
    <td>
         %Execute()
    </td>
    
    <td>
         ステータスを確認
    </td>
    
    <td>
         %Execute の戻り値を次のステップに使用
    </td>
    
    <td>
         Next()
    </td>
    
    <td>
         %Next()
    </td>
    
    <td>
         GetData()
    </td>
    
    <td>
         %GetData()
    </td>
    
    <td>
         %Get()
    </td>
    
    1
    2
    3
    4
    5
    6
    7
       Get() または Data()
    0
    0 226
    記事 Tomoko Furuzono · 11月 24, 2020 4m read

    最近、InterSystems 内で PHP から Caché ベースの Web サービスに接続が必要になる事例がいくつかありました。 これらの最初の事例とは、実はこの開発者コミュニティそのものであり、他の InterSystems サイト/アプリケーションとのシングルサインオンに Web サービスを使用しています。 次の例は、パスワード認証を使用して PHP から Caché ベースの Web サービス(具体的には SAMPLES ネームスペースの Web サービス)に接続する方法を示しています。


    (注意: この例は、/csp/samples に対してパスワード認証が有効になっていることを前提としています。) 

    0
    0 347
    記事 Toshihiko Minamoto · 11月 18, 2020 3m read

    FTP ファイルを Intersystems Caché からダウンロードするメソッドを以下に示します。ご質問がある場合はメッセージをお寄せください。

    ClassMethod FTPDownload(myFTP = "", myUserName = "", myPassword = "", sFileLocation = "", dLocation = "", noOfdownloadFile = 1, sourceFileDel = )
    {
     /*---------------------------------------------------------------------------------------------------------------------------
     要件に従ってファイルをダウンロードします  : FTP

    メソッド : 再利用可能 

    作成者 : Sanjib Raj Pandey、30/03/2018 に作成

     downLoadFile = ファイル数またはすべてのファイル  ...... ダウンロードしたいファイルを指定します、デフォルト値は 1 です。
       = 1,3,7,100 ファイルなどの値を指定します。
       = すべてのファイルをダウンロードするには "*" を指定します。
     
      SourceFileDel = ダウンロード後にソースフォルダーのファイルを削除したい場合は..... 
                      この値を 1 に設定します  -- ;   デフォルト値 :  0 
                      1= True (ダウンロード後にソースフォルダーを削除する)、 0 = False(コピーのみ)
     
      sFileLocation = ソースファイルの場所(フォルダー)
      dLocation = 宛先フォルダー
    例 :
      以下の内容は ..... すべてのファイルをソースから宛先フォルダーに移動します。
      w ##class(CW.COMMON).FTPDownload("IP アドレス","ユーザー名","パスワード","ソースフォルダー","宛先フォルダー,"*",1)
     
    以下の内容は...... 200 ファイルをソースから宛先フォルダーにコピーします。
    w ##class(CW.COMMON).FTPDownload("IP アドレス","ユーザー名","パスワード","ソースフォルダー","宛先フォルダー,200,0)

      -------------------------------------------------------------------------------------------------------------------------
     */
     // Try .. Catch のようなエラー制御をセットアップできます。 
     
     Set (count,fileNo,key,messge,fileStream,myFileName,myFile,fSave,eMessage,eSubject)=""
     Set fIp= myFTP 
     Set fUserName= myUserName 
     set fPassword=myPassword 
     set sFileLocaion=sFileLocation 
     set dLocation=dLocation 
     Set downloadFile=noOfdownloadFile 
     Set sourceFileDel=sourceFileDel
     
     If $Length(fIp)=0||($L(fUserName)=0)||($L(fPassword)=0) || ($L(downloadFile)=0) "資格情報が無効であるか、ダウンロードファイルが 0 です!IP、ユーザー名、パスワード、FTP または宛先の場所を確認してください!"      
     Set myFtp=##class(%Net.FtpSession).%New()
     Set eMessage="FTP 接続に失敗しました。"_fIp_" またはユーザー名、パスワードをチェックしてください!"
     Set eSubject ="FTP 警告メッセージ。"  
     Set myFtp.Timeout = 60
     If 'myFtp.Connect(fIp,fUserName,fPassword) Quit  $$EVEMAIL^CW.COMMON(eSubject,eMessage)
     Do myFtp.SetDirectory(sFileLocaion)
     If 'myFtp.NameList(" ",.x) Quit "ファイルが見つかりません "
     Set fileStream = ##class(%Stream.FileBinary).%New()
     Set message ="コピー"
     Set myFileName=""
     Set fileNo=0
     Set Key=""
     If (downloadFile = "*")
     {
       While (x.GetNext(.Key))'=""
     {
           Do StartCopy
     }
    Do myFtp.Logout()
    Quit fileNo_" ファイルが正常に"_message_"されました!"   
     }
     
     If (downloadFile >0)
     {
           Set count=1
           While ((count <= downloadFile) && (count<=x.Count()))
      {
       do StartCopy
      Set count=count +1
      }
      Do myFtp.Logout()
      Quit fileNo_" ファイルが正常に"_message_"されました!" 
     }
      
    StartCopy
      Set myFileName= x.GetNext(.fileNo)
       Do myFtp.Binary()
      Do myFtp.Retrieve(myFileName,.fileStream)
      Set myFile= ##class(%Library.FileBinaryStream).%New()
       Set myFile.Filename=dLocation_myFileName
      Do myFile.CopyFrom(fileStream)
      Set fSave=myFile.%Save()
       IF ((sourceFileDel=1) && (fSave = 1))
      {
         Do myFtp.Delete(myFileName)
          Set message="移動"
    }

    0
    0 357
    記事 Toshihiko Minamoto · 11月 10, 2020 8m read

    最近行われたディスカッションの中で、Caché ObjectScript における for/while loop のパフォーマンンスが話に出ましたので、意見やベストプラクティスをコミュニティの皆さんと共有したいと思います。 これ自体が基本的なトピックではありますが、他の点では合理的と言える方法のパフォーマンスが意味する内容を見逃してしまうことがよくあります。 つまり、$ListNext を使って$ListBuild リストをイテレートするループ、または $Order を使ってローカル配列をイテレートするループが最も高速な選択肢ということです。

    興味深い例として、コンマ区切りの文字列をループするコードについて考えます。

    そのようなループをできるだけ手短に書くと、次のようになります。

    For i=1:1:$Length(string,",") {
        Set piece = $Piece(string,",",i)
        //piece を使って何らかの処理を実行する...
    }
    

    とても分かりやすいですね。でも、多くのコーディングスタイルガイドラインは次のようなコードを提案するかもしれません。

    Set n = $Length(string,",")
    For i=1:1:n {
        Set piece = $Piece(string,",",i)
        //piece を使って何らかの処理を実行する...
    }
    

    各イテレーションで終了条件は評価されていないので、この 2 つのコードにパフォーマンス面での違いはありません。 (初めは誤解していましたが、これはパフォーマンスの問題ではなく、単にスタイルの違いだということを Mark が指摘してくれました。)

    コンマ区切りの文字列の場合は、このパフォーマンスを高めることが可能です。 文字列が長くなるにつれ、$Piece(string,",",i) は、string を i 個目の piece の終わりまで処理することになるので、どんどん重くなっていきます。 これを改善するには $ListBuild リストを使用します。 例えば、$ListFromString$ListLength、および $List を使うと、以下のようなコードを書けます。

    Set list = $ListFromString(string,",")
    Set n = $ListLength(list)
    For i=1:1:n {
        Set piece = $List(list,i)
        //piece を使って何らかの処理を実行する...
    }
    

    この方が、特に piece が長い場合は、$Length/$Piece を使うよりもパフォーマンスが良くなります。 $Length/$Piece を使った方法では、n の各イテレーションで piece の最初の i に渡される文字がスキャンされています。 一方の $ListFromString/$ListLength/$List を使った方法では、n の各イテレーションで $ListBuild 構造の i ポインターを追いかけています。 この方が高いパフォーマンスを得られますが、それでも実行時間は O(n2) のままです。 loop によりリストの内容が変更されないことを想定した場合、$ListNext を使えば O(n) を改善することができます。

    Set list = $ListFromString(string,",")
    Set pointer = 0
    While $ListNext(list,pointer,piece) {
        //piece を使って何らかの処理を実行する...
    }
    

    $List のように、毎回、次のポインタはリストの先頭から i個目の piece までを移動するのではなく、変数「pointer」がリスト内の現在位置を把握しています。 したがって、合計 n(n+1)/2 回 ($Listではn 回のイテレーションでそれぞれ i 回) の「次のポインタ」操作は行わずに、単純に操作を n 回 ($ListNextではイテレーションごとに 1 回) 実行するということになります。

    最後に、文字列を整数の添え字が付いた配列に変換すると良いかもしれません。一般的に、$Order を使ってローカル配列をイテレートすると、$ListNext を使った場合よりも処理速度が少し、または大幅に改善します (リスト要素の長さによる)。 もちろん、カンマ区切りの文字列の場合は、配列に変換するのに少し手間がかかります。 繰り返しイテレートする場合や、リストを部分的に変更する必要がある場合、または逆方向にイテレートする必要がある場合は、手間をかけてでも行う価値があるでしょう。

    以下は、異なる入力サイズごとに示した実行時間のサンプルです (必要な変換をすべて含む)。

    これらの数字は以下から得ています。

    USER>d ##class(DC.LoopPerformance).Run(10000,20,100)
    Iterating 10000 times over all the pieces of a string with 100 ,-delimited pieces of length 20:
    Using $Length/$Piece (hardcoded delimiter): .657383 seconds
    Using $Length/$Piece: 1.083932 seconds
    Using $ListFromString/$ListLength/$List (hardcoded delimiter): .189867 seconds
    Using $ListFromString/$ListLength/$List: .189938 seconds
    Using $ListFromString/$ListNext (hardcoded delimiter): .089618 seconds
    Using $ListFromString/$ListNext: .089242 seconds
    Using $Order over an equivalent local array with integer subscripts: .072485 seconds
    ****************************************************
    Using $ListFromString/$ListNext (not including conversion to $ListBuild list): .058329 seconds
    Using one-argument $Order over an equivalent local array with integer subscripts: .060327 seconds
    Using three-argument $Order over an equivalent local array with integer subscripts: .069508 seconds
     
    USER>d ##class(DC.LoopPerformance).Run(2,1000,2000)
    Iterating 2 times over all the pieces of a string with 2000 ,-delimited pieces of length 1000:
    Using $Length/$Piece (hardcoded delimiter): 3.372927 seconds
    Using $Length/$Piece: 11.739316 seconds
    Using $ListFromString/$ListLength/$List (hardcoded delimiter): 1.004757 seconds
    Using $ListFromString/$ListLength/$List: .997821 seconds
    Using $ListFromString/$ListNext (hardcoded delimiter): .010489 seconds
    Using $ListFromString/$ListNext: .010268 seconds
    Using $Order over an equivalent local array with integer subscripts: .000839 seconds
    ****************************************************
    Using $ListFromString/$ListNext (not including conversion to $ListBuild list): .003053 seconds
    Using one-argument $Order over an equivalent local array with integer subscripts: .000938 seconds
    Using three-argument $Order over an equivalent local array with integer subscripts: .000677 seconds
    

    コード (DC.LoopPerformance) を Gist で表示する

    追加

    ディスカッションの際に、他の方法でも良いパフォーマンスが得られることが判明しましたので、お互いを比較しておく価値があるでしょう。 RunLinearOnly メソッドとテストを実施した様々な実装を最新版の Gist でご覧ください。

    USER>d ##class(DC.LoopPerformance).RunLinearOnly(100000,20,100)
    Iterating 100000 times over all the pieces of a string with 100 ,-delimited pieces of length 20:
    Using $ListFromString/$ListNext (While): .781055 seconds
    Using $ListFromString/$ListNext (For/Quit): .8438 seconds
    Using $ListFromString/$ListNext (While, not including conversion to $ListBuild list): .37448 seconds
    Using $Find/$Extract (Do...While): .675877 seconds
    Using $Find/$Extract (For/Quit): .746064 seconds
    Using one-argument $Order (For): .589697 seconds
    Using one-argument $Order (While): .570996 seconds
    Using three-argument $Order (For): .688088 seconds
    Using three-argument $Order (While): .617205 seconds
     
    USER>d ##class(DC.LoopPerformance).RunLinearOnly(200,2000,1000)
    Iterating 200 times over all the pieces of a string with 1000 ,-delimited pieces of length 2000:
    Using $ListFromString/$ListNext (While): .913844 seconds
    Using $ListFromString/$ListNext (For/Quit): .925076 seconds
    Using $ListFromString/$ListNext (While, not including conversion to $ListBuild list): .21842 seconds
    Using $Find/$Extract (Do...While): .572115 seconds
    Using $Find/$Extract (For/Quit): .610531 seconds
    Using one-argument $Order (For): .044251 seconds
    Using one-argument $Order (While): .04467 seconds
    Using three-argument $Order (For): .043631 seconds
    Using three-argument $Order (While): .042568 seconds
    

    以下のチャートは、これらのメソッドの While/Do...While をそれぞれ比較したものです。 特に、$ListFromString/$ListNext と $Extract/$Find、および文字列から $ListBuild リストへの変換をせずに使用する場合の $ListNext と整数の添え字を付けたローカル配列で使用する $Order を比較した相対的なパフォーマンスに注目してください。

    まとめ

    • コンマ区切りの文字列から始める場合は、$ListFromString/$ListNext を使った方がわずかにより直感的なコードを書けますが、パフォーマンスの面では $Find/$Extract が最良な選択肢となります。
    • データ構造のチョイスを考慮した場合、 $ListBuild リストのトラバーサルは、入力の小さい同等のローカル配列よりもわずかにパフォーマンスが優れているように思える一方で、入力が大きい場合はローカル配列の方がかなり高いパフォーマンスを提供します。 パフォーマンスにそれほど大きな違いがある理由は分かっていません。 (これに関連して、整数の添え字を付けたローカル配列と$ListBuild リストにおけるランダムな挿入と削除のコストを比較する価値はあるでしょう。このような配列では、set $list を使った方が、わざわざ値を移動させるよりも処理が速くなると思います。)
    • 引数なしの同等の for loop と比較すると、While loop または Do...While loop の方が わずかに 高いパフォーマンスを発揮します。
    0
    1 564
    記事 Tomoko Furuzono · 9月 10, 2020 3m read

    日付範囲クエリが極端に遅くなっていませんか?  SQLのパフォーマンスが低下していませんか?

    日付範囲のサブクエリをまだご覧になっていない場合は、前回の投稿をご確認ください。
    https://jp.community.intersystems.com/post/日付範囲クエリのsqlパフォーマンスを改善する


    なぜ、こうも日付クエリに注目しているのでしょうか? それは、日付クエリが重要だからです。 それは報告であり、統計であり、自分の素晴らしい仕事を上司に証明するための数字です(もちろん、あなたが実際にそうしていればの話ですが )。では、前回と同じようなテーブルを見てみましょう。ただし、実際には MAXLEN と MINLEN を次のように適切に定義します。

    Class User.DateQ extends %Persistent 
    {
    Property DateSubmitted as %Date;
    Property Data as %String (MAXLEN=200, MINLEN=100);
    Index DateIdx on DateSubmitted;
    }

    では、先月のすべてのデータを取得したい場合を見てみましょう。 次のようなクエリを書き、「良い仕事」を考えます。

    0
    0 766