#フロントエンド

0 フォロワー · 16 投稿

フロントエンドWeb開発は、HTML、CSS、JavaScriptを使用してデータをグラフィカルインターフェースに変換し、ユーザーがそのデータを表示して操作できるようにする方法です。 詳細はこちら

記事 Toshihiko Minamoto · 5月 20, 2025 7m read

Django フレームワークは長年学習したいと思ってきましたが、いつも他の差し迫ったプロジェクトが優先されてきました。 多くの開発者と同様に、機械学習においては Python を使用していますが、初めてウェブプログラミングについて学習したころは、PHP がまだまだ優勢でした。そのため、機械学習の作品を公開する目的でウェブアプリケーションを作成するための新しい複雑なフレームワークを選択する機会が訪れても、私は依然として PHP に目を向けていました。 ウェブサイトの構築には Laravel と呼ばれるフレームワークを使用してきましたが、この PHP フレームワークから最新の MVC(モデルビューコントローラー)というウェブプログラミングのパターンに出会いました。 さらに複雑なことに、私は最新の JavaScript フレームワークを使用してフロントエンドを構築するのを好んでいます。 React を使用するのがより一般的のようですが、私は Vue.js に一番慣れているため、このプロジェクトではそれを使用することにしました。

なぜ複雑なフレームワークを使用するのでしょうか? Django、Laravel、React、または Vue などのフレームワークを学習する際の最大の難関は何でしょうか?

答えは人それぞれですが、私は MVC フレームワークがアプリの構造化に関するガイドを非常に多く提供してくれるため、気に入っています。毎回、作り直す必要がありません。 初めはこういったフレームワークは制約が多くて難解に思えるかもしれませんが、構造に慣れてしまえば、新しい機能をより追加しやすいと思います。

問題は、物事があまりにも単純になりすぎる可能性があることです。 Django のようなフレームワークは、よく知られた概念に基づいているかもしれませんが、Django では特に馴染みのない名前と構造を持つ多くの省略形や仮定に依存しています。 私のアプリケーションでは、Django は API とすべてのウェブルーティングを処理しています。 新しい API エンドポイントを追加する場合は、views.py のファイルに関数を追加してから、urls.py ファイルに移動して、その関数をインポートするステートメントと、API エンドポイントが提供されている URL を定義する別のステートメントを追加する必要があります。 その後で、データを取得してユーザーに表示するか操作するために、そのエンドポイントをクエリする JavaScript を使って、フロントエンドの Vue コンポーネントを編集する必要があります。

プロジェクトのセットアップが完了したら、このような機能の追加は迅速に行えます。 約 4 行のコードを追加するだけで、HTTP リクエストを処理し、必要なデータを JSON 形式で返すように、views.py ファイルの新しい関数に必要なロジックに集中できます。 難しいのは、それらのファイルが何であるか、そしてそれらがどのように連携してアプリケーション全体を作り上げるかを学ぶことです。

Django のようなフレームワークを学習するには、実際に動作する例を探して、データのフローを感じ取れる小さな変更を適用して見るのが最適な方法だと思います。 概念が明確になり始めて理解できるようになってきたら、ドキュメントを参考にしましょう。 AI モデルにコードを説明してもらい、様々な標準ファイルがフレームワークでどのように動作するかを尋ねましょう。 これらのツールが、長期的には時間を節約し、アプリケーションの保守と更新を容易にする方法として登場したことにすぐに気づくでしょう。 Django と Vue フレームワークには標準の構造があるため、後で戻ってきても、なぜ特定の方法でコーディングしたのかをすぐに理解でき、作業についての理解を再び深めやすくなっているでしょう。 また、アプリケーションの基本構造に慣れているため、他の人のアプリケーションを理解し、主な機能を把握するのもより簡単です。

では、これから始めようとしている人の支援となる Django の基礎とは何でしょうか? 私にとっては、最初に理解すべきことは、Django プロジェクトは Django の新規プロジェクトを作成するコマンドの実行によって 生成され、これによって構築を開始するために使用できる「基本プロジェクト」を構成する一連の基本ファイルとフォルダが生成されるということです。 プロジェクトフォルダには、プロジェクト全体に適用される設定を含むいくつかの Python ファイルがあります。 頻繫にアクセスする重要なフォルダは、すべての設定が含まれる settings.py と、urls.py です。 「Django はどのようにして静的ファイルを配置する場所を決定しているのか」といった疑問がある場合、その答えは通常 settings.py のどこかにあります。 アプリケーションに新しい URL を追加する場合は、urls.py ファイルを更新する必要があります。

これらのプロジェクトレベルのファイルと共に、プロジェクト内のアプリごとにフォルダを作成します。 これらのアプリは登録されている必要があります。つまり、 settings.py ファイルで名前を付ける必要があります。 プロジェクト内のメインのアプリフォルダはドキュメントと呼ばれます。 どのフォルダ内には、models.py ファイル、serializer.py ファイル、views.py ファイルがあります。 ファイルは他にもありますが、これらが重要な 3 つのファイルです。

models.py 内には、Document オブジェクトとそのフィールドを指定します。 Document オブジェクトに保管する予定の情報を保存するために必要なスキーマを使って IRIS データベースに Documents テーブルを作成するのは、Django に任せられます。 私の models.py ファイルでは、Documents には 255 文字以内の名前、大量のテキストであるコンテンツフィールド、ベクトルが補完されるデータベース名(別のテキストフィールド)、埋め込みタイプ(別のテキストフィールド)、および数値で表現されるベクトル埋め込みの次元が含まれることを指定しています。 これらの定義を使用することで、Fjango は必要な列タイプで必要なデータベーステーブルを作成します。 すると、データベースへのオブジェクトの保存は、Document.save() だけで完了です。

serializer.py ファイル内には、オブジェクトと JSON の変換方法に関する定義が含まれます。 基本的なユースケースでは、これを定義する標準的な方法があり、このプロジェクトで確認できます。

では、Django の核心である views.py ファイルを確認しましょう。 ここに、HTTP リクエストを受け取って、HTTP レスポンス全体、または JSON API の場合は JSON API レスポンスなどのデータを返す関数を定義します。 つまり、Django ではウェブページ全体を制作して、アプリのフロントエンドとしても使用することも、JSON データのみを提供して、フロントエンドを全く別のプラットフォームで構築することもできます。

最初は、一見恣意的なファイルや規則をすべて使用するのは面倒に感じるかもしれませんが、そうすることでアプリケーションが動作し始め、HTTP リクエストを処理して、レスポンスとして正しいデータを提供するようになることが分かれば、新しい機能を構築し続けるのが非常に楽しくなります。 HTTP リクエストを処理するオブジェクト、ウェブルート、および関数を 1 つ定義すれば、2 つ目、そして 3 つ目を簡単に定義してアプリケーションに機能を追加できるようになります。

私のプロジェクトは、github: https://github.com/grongierisc/iris-django-template にある @Guillaume Rongier が作成した Iris Django Template をフォークしたものです。

このテンプレートには Django のみが含まれており、Django フレームワークの学習に非常に役立ちました。私が行った主な追加項目の 1 つは、Tailwind CSS を使用した Vue.js の追加です。最新の JavaScript フレームワークをこのパッケージに統合して、IRIS で実行する単一ページのアプリケーションを作成できます。 単一ページのアプリケーションは、xhr リクエストを送信して JSON データを取得し、完全に再読み込みすることなく動的にページを更新する JavaScript アプリケーションです。 これには長所と短所がありますが、最新のウェブ開発の特徴です。

RAG と IRIS 上のベクトルストアの例としてだけでなく、Vue.js と Tailwind を使って IRIS 上に最新の柔軟なウェブアプリケーションを簡単に素早く作成する目的で Django を使用するためのテンプレートとして、私のプロジェクトを確認することをお勧めします。 リポジトリはこちらの GitHub にあります: https://github.com/mindfulcoder49/iris-django-template

ご質問があれば、ぜひお答えします。このプロジェクトを独自のユースケースに適合しようとするする際に問題が発生した場合は、私の洞察を提供いたします。

0
0 63
記事 Toshihiko Minamoto · 12月 10, 2024 9m read

コミュニティメンバーから、Python 2024 コンテストでの出品に対する非常に素晴らしいフィードバックが届きました。 ここで紹介させていただきます。

純粋な IRIS の 5 倍以上のサイズでコンテナーをビルドしているため、時間がかかっています

コンテナーの始動も時間はかかりますが、完了します

バックエンドは説明通りにアクセス可能です

プロダクションは稼動しています

フロントエンドは反応します

何を説明したいのかがよくわかりません

私以外のエキスパート向けに書かれた説明のようです

出品はこちら: https://openexchange.intersystems.com/package/IRIS-RAG-App

このようなフィードバックをいただけて、本当に感謝しています。プロジェクトに関する記事を書く素晴らしいきっかけとなりました。 このプロジェクトにはかなり包括的なドキュメントが含まれてはいますが、ベクトル埋め込み、RAG パイプライン、LLM テキスト生成のほか、Python や LLamaIndex などの人気の Python ライブラリに精通していることが前提です。

この記事は、IRIS での RAG ワークフローを実証するに当たって、上記の前提事項や、それらが IRIS で RAG ワークフローをこのプロジェクトにどのように適合するかについてを説明する試みです。AI をまったく使用せずに書かれています。

コンテナーが大きいのは、ベクトル埋め込みの作成に関わる Python パッケージに必要なライブラリ依存関係が非常に大きいためです。 より選択的にインポートすることで、サイズを大幅に縮小することが可能です。

コンテナーの初回ビルドには確かに時間がかかりますが、一度ビルドすれば起動時間は短くなります。 とはいえ、起動時間は確かに改善できるかもしれません。 起動にこれほどの時間がかかる主な理由は、アプリケーションのある個所が最後の起動から変更されていることを想定して entrypoint.sh が更新されているためです。これには、データベースの移行、CSS 構成、JavaScript 構成、Python バックエンドコードも含まれており、起動のたびにプロジェクト全体がリコンパイルされます。 これは、このプロジェクトを開発し始めやすくするためで、そうでない場合は、変更が適用されるたびに、フロントエンドとバックエンドのビルドを適切に実行するのが困難になってしまいます。 こうすることで、プロジェクトのコードを変更した場合はコンテナーを再起動し、場合によってはバックエンドのプロダクションを復旧すれば、アプリケーションのインターフェースと操作に変更が反映されます。

バックエンドのプロダクションは、HTTP リクエストを Django アプリケーションに渡すものであり、このパッケージの相互運用性にとって非常に重要であると確信しています。 ただし、私自身は IRIS プラットフォームの初心者であるため、プロダクションについてもっと学ぶ必要があります。

次に、ベクトル埋め込み、LLM、および RAG について、包括的に説明したいと思います。 この内最初に作られたのはベクトル埋め込みです。 まず、ベクトルについて説明します。 ほとんどのコンテキストにおいて、ベクトルは方向です。 空間のどこかを指す矢印です。 より正式には、ベクトルは「大きさだけでなく方向も持つ量」です。 これは、特定の方向へ移動し、空間内の特定の地点で爆発する花火によって例えることができます。 すべての花火が同じ中心点、つまり原点である [0,0,0] から発射され、その原点の周囲に雲となって飛び散るとします。 数学的には、3 つの座標系 [x,y,z] を使用して各花火の爆発の位置を表現することができ、これが花火の爆発の「ベクトル埋め込み」となります。 花火のビデオをたくさん撮影し、花火の爆発をすべてデータセットとして記録すると、花火の一種のベクトル埋め込みデータベース、つまりベクトルストアが作成されることになります。

花火に関する情報を使って何ができるでしょうか? 特定の花火を指して、花火全体の中から同じ点に最も近い位置で爆発した花火について尋ねると、空間の近くの点で爆発した他の花火を検索できます。 最もちかいものを見つけるだけですが、これを行うために数式があります。

花火ごとに、x、y、z の 3 つの数値のみを記録したことに注意してください。3 次元空間において、地上の花火発射台を [0,0,0] としています。

他の特定の花火に対して、距離と時間の両方の観点で最も近く爆発した花火も知りたい場合はどうでしょうか? それを知るには、花火の映像を確認して、各爆発の時間も記録しなければなりません。 これで、4 つの数値を持つ 4 次元ベクトルが取得されました。花火の爆発の 3 次元の位置に爆発の時間を加えたベクトルです。 ベクトル埋め込みにもう 1 つの次元を追加することで、花火の埋め込みがより記述的になりました。

これを機械学習に変換するとどうなるでしょうか? 手短に言えば、大量のテキストデータを処理することで、コンピューター科学者は、フレーズ、文章、段落、またはページなどのテキストを変換し、理論的な高次元空間の点を表現する非常に長い一連の数値に変換できる埋め込みモデルを作成することができました。

4 つの数字ではなく、300、700、さらには 1500 もの数字があります。 これらは、1 つのテキストが互いに「近い」か「遠い」かを 1500 通りまたは1500 次元の意味で表します。 テキストの意味を何らかの方法で表す数字を作成する手段があるというわけですから、多くの人にとって魅力的な概念と言えるでしょう。

数学を使用すると、これらの高次元テキストベクトル埋め込みのうち 2 つを比較して、同じモデルによって作成された場合に、それらが互いにどの程度類似しているか、つまり「近い」かを調べることができます。

このアプリで最初の行われているのが正にこれです。ユーザーはドキュメントを追加して名前を付け、埋め込みのタイプを選択する必要があります。 サーバーはそのドキュメントを受け取り、テキストのチャンクに分割してから、それぞれのチャンクをベクトル埋め込みに変換します。そのチャンクはそのドキュメントの専用のテーブルの行として保存されます。 各ドキュメントは、さまざまなテキスト埋め込みモデルによって作成されるベクトル埋め込みの可変長に対応できるように、それぞれの専用テーブルに保存されます。

ドキュメントがベクトル埋め込みとしてデータベースに保存されると、ユーザーはドキュメントに「尋ねる」クエリを入力できるようになります。 このクエリは 2 つの方法で使用されます。 1 つは、ドキュメントを検索するためです。 従来のテキスト検索は実行せずに、「ベクトル検索」を実行します。 アプリはクエリを受け取り、それをベクトル埋め込みに変換してから、クエリベクトル埋め込みに最も築地する埋め込みのあるドキュメントのセクションを検索します。 各ドキュメントセクションには 0 と 1 の間の類似性スコアが生成され、top_k_similarity と similarity_threshold に基づいて、ベクトルデータベースから複数のセクションが検索されます。 基本的に、取得するドキュメントのセクション数と取得の対象となるためにクエリとどの程度類似している必要があるかを指定することができます。

これが、検索拡張生成における取得です。 次は生成に移りましょう。

コンピューター科学者がテキストを意味的に重要な数値ベクトル埋め込みに変換する方法を見つけると、次に、テキストを生成するモデルの作成に移りました。 これは大きな成功を生み出し、現在では GPT-4、LLama3、Claude 3.5 などの大規模言語モデルとなっています。 これらの LLM はプロンプトまたはクエリを受け取り、補完または回答を提供します。これは LLM が提示されたテキストであるプロンプトから最も続行できる可能性があると考えるテキストです。

LLM は大量のテキストデータに対してトレーニングする必要があり、その回答または補完はそのトレーニングデータに制限されます。 トレーニングセットにないデータを含む可能性のある補完を LLM に提供させる場合、または補完を特定のナレッジセットに基づかせる場合は、1 つの方法として、プロンプトに追加のコンテキストデータを含めることができます。 基本的に、トレーニングされていない内容について LLM から回答を得たい場合、プロンプトに情報を提供する必要があるということです。

多くの人は、ChatGPT やローカルの LLama インストールが自分の個人文書に基づいて回答を提供してくれることを望む状況に陥っていました。 ドキュメント内でその情報を検索し、プロンプトに貼り付けて、質問を入力するだけの単純な操作であり、手作業で行っていました。 それ自体が検索拡張生成です。 RAG は、より正確または利便的な応答を得られるように、ユーザークエリに関連する情報を検索し、LLM にクエリを提供する操作を自動化したに過ぎません。

このアプリでは、ベクトル検索で取得したドキュメントセクションは、インターフェースでモデルとしてラベル付けされている選択された LLM にクエリとともに送信され、回答のコンテキストが提供されます。

このプロジェクト用に制作した動画の例では、シェイクスピアの 2 つの戯曲の全文を含むドキュメント「ハムレット」と「リア王」を使って、「この戯曲の悪役は誰ですか?」と尋ねています。 IRIS データベースには、ハムレットとリア王の 2 つのテーブルがすでに存在します。 各テーブルには、各戯曲のテキストをセクションに分割して作成されたベクトル埋め込みの行が入力されています。 これらの埋め込みは、一連の長い数値によって各ドキュメントセクションの多次元を表現しています。

サーバーは、「この戯曲の悪役は誰ですか」という質問を、リア王のベクトル埋め込みを生成した Text-to-Vector モデルを使用して数値ベクトルに変換し、リア王テーブル内でそれに最も類似するセクションを見つけます。 これらはおそらく悪役という語が言及されたセクションかもしれませんが、悪役が明示的に言及されていない場合でも、裏切り、裏切り、欺瞞などの他の悪役についても言及されている可能性があります。 こういったドキュメントのセクションは、クエリに追加され、合わせてプロンプトとして LLM に送信されます。LLM は提供されたドキュメントのセクションに基づいて質問に回答します。

これはドキュメントごとに個別に実行されるため、クエリの回答はクエリされているドキュメントに応じて異なります。 これにより頭字語が補完されます。ベクトル検索の力を使用して関連するコンテキスト情報を取得することで、LLM からの応答の生成を強化しているためです。

この記事をお読みいただきありがとうございました。このトピックについては今後の記事でも発展させたいと思います。 フィードバックをお待ちしています。

0
0 82
記事 Toshihiko Minamoto · 10月 5, 2023 4m read

コミュニティの皆さん、こんにちは!

DeepSee Web についてのパート 2 では、DSW のカスタマイズオプションについて説明します。  

カスタマイズには、ウィジェットのカスタマイズとダッシュボードパネルのカスタマイズの 2 種類があります。

開発者コミュニティ分析におけるダッシュボードのカスタマイズ例。

ウィジェットのカスタマイズ

DSW の各グラフウィジェットは、凡例、凡例要素、値、上位/全部の切り替えの要素によって調整することができます。

調整パネルは、グラフウィジェットの右上にあります。

ではその仕組みを見てみましょう。

凡例

小文字の「i」は凡例の切り替えを表します。 クリックすると凡例が表示/非表示になります。  また、凡例の特定の要素を表示/非表示にすることもできます。

上位行フィルタ

星ボタンは、上位/全部の切り替えボタンで、ピボットの最初の測定の上位 20 件フィルターを素早くオン/オフにできます。

ウィジェットに行数コントロールを導入すると、上位切り替えは上位/全部の値を自動的に使用するようになります。 当然、一般的な行カウント/列カウントコントロールを追加して、特殊な上位フィルタの動作を導入することも可能です。

値の切り替え

「V」は <s>vendetta</s> 値です。 クリックすると、行の値が表示/非表示になります。

ウィジェットのレイアウト

DSW は最初に DeepSee ダッシュボードリソースからウィジェットレイアウトを取得して、DSW 設定の列カウント(デフォルトは 12)とウィジェットの最小高さ(100)に合わせて拡大縮小されます。

ウィジェットのレイアウトは好みに合わせて移動して設定できます。 フィルタコントロールを配置する必要がある場合などには特に便利です(フィルタコントロールは、ウィジェットの代わりにダッシュボードにフィルタが配置される場合に自動的に表示されます)。

ダッシュボードパネルのカスタマイズ

DSW では、ダッシュボードパネルとタイル/カバーの外観をカスタマイズできます。

以下は、DeepSee ダッシュボードのクラシックパネルビューです。

DSW では、ダッシュボードパネルビューを 3 つの方法でカスタマイズできます(カスタマイズは設定で変更できます)。

Show images(画像を表示)をオンにすると、DeepSee の画像がダッシュボードカバーに表示されます。

すると、ダッシュボードパネルに画像が表示されます。

Show folders(フォルダを表示)を選択すると、同じダッシュボードパネルがフォルダで編成されます。

また、ダッシュボードカバーの色やサイズを調整し、対話型にすることができます。

ダッシュボードパネルの右下からタイルエディターを開きます。

ダッシュボード上の色、タイトル、画像を変更したり、ダッシュボードカバーの上にダッシュボードのウィジェットを配置したりできます。また、設定で、タイルの色テーマを確認できます。以下に例を示します。 これは、コミュニティ分析のコントラストテーマです。これは、同じダッシュボードパネルのメトロテーマです。

カスタマイズのデプロイ

デプロイについてはどうでしょうか。 設定やカスタマイズを開発からプロダクションソリューションに配布するには?

これを行うには、Settings(設定)メニューで現在のカスタマイズをエクスポートできます。

すべての設定が settings.json ファイルにエクスポートされるため、ターゲットプロダクションサーバーにデプロイすることができます。 DSW は、設定が含まれるファイルを /csp/dsw/configs/namespace.json ファイルで探します。

例: これは samples.json ファイルです。これを DSW インストールの /csp/dsw/config/samples.json に配置すれば、DSW がこのネームスペースのダッシュボードの設定を自動的に読み込むようになります。

尽きることのないカスタマイズ

デザイン、設定、調整などを行えるすべてのケースにおいて、カスタマイズが完成することは決してありません。 そのため、この分野において他の機能をご希望の場合は、課題をお送りいただくか、この機能を実装してプルリクエストをお送りください。

DSW の機能についてはさらに記事が追加されます。 パート 1 を読んで、今後の更新にご期待ください!

0
0 179
記事 Toshihiko Minamoto · 9月 15, 2023 5m read

DeepSee BI ソリューションのユーザーインターフェース(UI)を配布するにはいくつかのオプションがあります。 最も一般的には以下の手法があります。

  • ネイティブの DeepSee ダッシュボードを使用し、Zen で Web UI を取得して、Web アプリに配布する。
  • DeepSee REST API を使用して、独自の UI ウィジェットとダッシュボードを取得・構築する。

最初の手法はコーディングを行わずに比較的素早く BI ダッシュボードを構築できるためお勧めですが、事前設定のウィジェットライブラリに限られます。これを拡張することはできますが、大きな開発の手間がかかります。

2 つ目の手法には、任意の総合 js フレームワーク(D3,Highcharts など)を使用して DeepSee データを可視化する手段がありますが、ウィジェットとダッシュボードを独自にコーディングする必要があります。

今日は、上の 2 つを組み合わせて Angular ベースの DeepSee ダッシュボード用 Web UI を提供するもう 1 つの手法をご紹介します。DeepSee Web ライブラリです。

詳しい説明

DeepSee Web(DSW)は、DeepSee ダッシュボードを表示する Angular.js Web アプリで、Highcharts.js、OpenStreetMap、および一部の自作 js ウィジェットを使用する特定のネームスペースで使用できます。

仕組み

DSW は、MDX2JSON ライブラリを使用するネームスペースで使用できるダッシュボードとウィジェットのメタデータをリクエストします。 ダッシュボードを可視化するために、DSW はウィジェットとそのタイプのリストを読み取り、ほぼすべての DeepSee ウィジェットに実装されている js-widget アナログを使用します。 ダッシュボードにリストされる js ウィジェットをインスタンス化し、DeepSee REST API と MDX2JSON を使用してウィジェットのデータソースに従ってデータをリクエストし、JSON 形式でデータを取得して可視化を実行します。

そのメリットは?

DSW は以下の理由により優れています。

  • 標準のエディターを使用して DeepSee ダッシュボードを作成し、コーディングを一切行わずに Angular で UI を使用できます。
  • 任意の js ライブラリまたは自作ウィジェットで簡単にウィジェットのライブラリを拡張できます。
  • DSW カバーにスマートタイルを追加できます。
  • 地図にデータを表示できます。

モバイルデバイスは?

DSW は、モバイルブラウザ(Safari、Chrome)で非常によく動作し、スタンドアロンの DeepSight iOS アプリもあります。以下はそのスクリーンショットです。

いくらですか?

無料です。 最新リリースのダウンロード、課題の送信、フォークの作成、修正や新機能を含むプルリクエストを自由に行えます。

プロダクションでの使用に適していますか?

はい。 DSW は 2014 年にリリースされ、今日まで 60 回リリースされてきました。 DSW は数十社の DeepSee ソリューションで正常に使用されています。

たとえば、開発者コミュニティでも DSW を使用しています。

ただし、DSW ライセンス(MIT ライセンス)に従い、ご自身のリスクで使用していただきます。

 

サポートされていますか?

コミュニティによってサポートされています。 ぜひ課題を開き、フォークを作成して、修正と新機能が含まれるプルリクエストを送信してください。

主要貢献者は [@Anton Gnibeda​]、[@Nikita Savchenko]、[@Eduard Lebedyuk] です。 

インストール方法は?

簡単です。 まず、MDX2JSON をインストールします。 リリースから最新のインストーラーをダウンロードして、USER ネームスペースで installer.xml をインポート/コンパイルし、以下を実行します。

USER> D ##class(MDX2JSON.Installer).setup()

GitHub から最新のリリースがダウンロードされ、MDX2JSON データベースとネームスペースの作成と %All へのマッピングが行われ、/MDX2JSON Web アプリが作成されます。

すべてがうまくインストールされたことを確認するには、localhost:57772/MDX2JSON/Test を開きます。  すべてが正常である場合は、以下のように返されます。

{
"DefaultApp":"\/dsw",
"Mappings": {
"Mapped":["%SYS","MDX2JSON","SAMPLES","USER"
],
"Unmapped":["DOCBOOK"
]
},
"Parent":"18872dc518d8009bdc105425f541953d2c9d461e",
"ParentTS":"2017-05-26 16:20:17.307",
"Status":"OK",
"User":"",
"Version":2.2
}​

 

次に DSW をインストールします。 最新の release xml をダウンロードします。

それを USER ネームスペースにインポート/コンパイルします。 以下を実行してください。

USER> Do ##class(DSW.Installer).setup()

/dsw アプリが作成され、すべての js ファイルが /csp/dsw フォルダにインストールされます。

localhost:57772/dsw/index.html#!/?ns=SAMPLES を開き、動作することを確認してください。

既知の問題:

一部のシステムでは、CSP ゲートウェイで CSP 以外のファイルに UTF8 サポートを設定する必要があります。 ターミナルで以下を実行してオンにしてください。

USER> set ^%SYS("CSP","DefaultFileCharset")="utf-8"

 

いつインストールすべきですか?

今すぐインストールしましょう! )

続きにご期待ください!

0
0 177
記事 Toshihiko Minamoto · 12月 12, 2022 16m read

こんにちは! 今日は、Angular で最も重要なアーキテクチャパターンの 1 つについてお話しします。

パターン自体は直接 Angular に関連していませんが、Angular はコンポーネント駆動のフレームワークであるため、このパターンは最新の Angular アプリケーションを構築するために最も不可欠なものの 1 つです。

「コンテナ・プレゼンテーション」パターン

コンポーネントは、小さい、目的集中型、独立型、テスト可能性、そして一番重要と言える再利用可能性という特性が備わっているのが最適だと考えられています。

コンポーネントに、サーバー呼び出しを行う、ビジネスロジックが含まれている、他のコンポーネントと密に連携している、他のコンポーネントまたはサービスの内部を過度に知っている、という特徴が備わっていれば、より大きく、テスト、拡張、再利用、変更が行いにくいものになってしまします。 「コンテナ・プレゼンテーション」パターンは、これらの問題を解決するために存在します。

一般的にすべてのコンポーネントは、コンテナコンポーネント(スマート)とプレゼンテーションコンポーネント(ダム)の 2 つのグループに分けられます。

コンテナコンポーネントは、サービスからデータを取得し(ただし、サーバー API を直接呼び出さない)、何らかのビジネスロジックを含み、サービスまたは子コンポーネントにデータを配信することができます。 通常、コンテナコンポーネントは、ルーティング構成でルートが指定されたコンポーネントとして指定されるものです(もちろん、必ずしもそうとは限りません)。

プレゼンテーションコンポーネントは、入力としてデータを取り、何らかの方法で画面に表示することのみを行います。 これらのコンポーネントはユーザー入力に反応しますが、それはローカルで独立した状態を変更することでのみ発生するものです。 アプリの残りの部分とのすべてのやり取りは、カスタムイベントを発行することで行われます。 これらのコンポーネントには非常に高い再利用可能性が備わっている必要があります。

もう少し分かりやすいように、コンテナコンポーネントとプレゼンテーションコンポーネントの例をいくつか挙げましょう。

コンテナ: About ページ、ユーザーページ、管理者パネル、注文ページなど。

プレゼンテーション: ボタン、カレンダー、テーブル、モーダルダイアログ、テーブルビューなど。

めちゃくちゃなボタンの例

コンポーネントアプローチが誤用されている例として、実際のプロジェクトにおいて私自身が体験した非常に悪い例を見てみましょう。

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() action = '';
  @Input() className = '';
  @Input() label = '';

  constructor(
    private router: Router,
    private orderService: OrderService,
    private scanService: ScanService,
    private userService: UserService
  ) {}

  onClick() {
    if (this.action === 'registerUser') {
      const userFormData = this.userService.form.value;
      // some validation of user data
      // ...
      this.userService.registerUser(userFormData);
    } else if (this.action === 'scanDocument') {
      this.scanService.scanDocuments();
    } else if (this.action === 'placeOrder') {
      const orderForm = this.orderService.form.values;
      // some validation and business logic related to order form
      // ...
      this.orderService.placeOrder(orderForm);
    } else if (this.action === 'gotoUserAccount') {
      this.router.navigate('user-account');
    } // else if ...
  }
}

読みやすいように単純化してありますが、実際にはもっとひどいものです。 これは、ユーザーがクリックして呼び出せるすべての可能なアクションが含まれたボタンコンポーネントです。API 呼び出し、フォームの検証、サービスからの情報取得などを行います。 このようなコンポーネントは、比較的小さなアプリケーションで使用されていたとしても、あっと言う間に地獄入りしてしまうのが想像できるでしょう。 私が見つけて(後でリファクタリングした)このようなボタンコンポーネントのコードは、2000 行以上にも及ぶものでした。 あり得ません!

それを書いた開発者に、すべてのロジックを 1 つのコンポーネント収めた理由を尋ねたところ、本人が行ったのは「カプセル化」だという答えが返ってきました。🙀

理想的なコンポーネントに備わっているべき特性について思い出しましょう。

小さい - 2000 行以上のコードでできたこのボタンは小さくありません。 また、他のアクション用に別のボタンが必要という人が現れるたびに、コンポーネントが膨れ上がってしまうでしょう。

目的集中型 - このボタンはまったく無関係な多くのアクションを実行するため、目的集中型とは呼べません。

独立型 - このボタンはいくつかのサービスとフォームと密に連携しているため、いずれかを変更すると、ボタンに影響があります。

テスト可能性 - ノーコメントです。

再利用可能性 - まったく再利用できません。 使用するたびに、含まれていないアクションを追加するためにコンポーネントのコードを変更する必要があります。さらに、このボタンの不要なアクションや依存関係もすべて含まれることになります。

さらに、このボタンコンポーネントは、ネイティブの HTML ボタンを内部的に非表示にするため、開発者はそのプロパティにアクセスできません。 これが、コンポーネントの悪い書き方としての良い例です。

このボタンコンポーネントを使用する非常に単純なコンポーネントの例を示し、コンテナ・プレゼンテーションパターンでリファクタリングしてみましょう。

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" action="registerUser"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}
}

このコンポーネントの中には何もロジックがありません。フォームはサービスに格納されており、ボタンにはクリックによるすべてのロジックの呼び出しが含まれます。 現時点では、ボタンにその動作に関係のないすべてのロジックが含まれており、ボタンが処理するアクションに直接関係している親コンポーネントよりスマートです。

コンテナ・プレゼンテーションパターンでボタンコンポーネントをリファクタリングする

これらの 2 つのコンポーネントの関数を分離しましょう。 ボタンはプレゼンテーションコンポーネント、つまり小さく再利用可能である必要があります。 このボタンを含む登録フォームは、ビジネスロジックやサーバーレイヤーとのやり取りを保持するコンテナコンポーネントとすることができます。

ネイティブボタンを表示する部分は説明せずに(これについては、おそらく今後の記事で話すことにします)、主に 2 つのコンポーネントのアーキテクチャ上の関係に焦点を当てます。

リファクタリングしたボタンコンポーネント(プレゼンテーション)

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() className = '';
  @Input() label = '';

  @Output() click: EventEmitter = new EventEmitter();

  onClick() {
     this.click.emit();
  }
}

ご覧のとおり、リファクタリングしたボタンは非常に簡潔です。 2 つの入力と 1 つの出力しか含まれません。 入力は、親コンポーネントからデータを取ってユーザーにそれを表示するために使用します(ボタンの外観はクラスと表示ボタンのラベルで変更します)。 出力は、ユーザーがボタンをクリックするたびに発行されるカスタムイベントに使用されます。

このコンポーネントは小さく、目的集中型、独立型、テスト可能性、および再利用可能性の特性を備えています。 コンポーネント自体の動作に無関係なロジックは含まれていません。 アプリケーションの残りの内部動作についてまったく認識しておらず、アプリケーションのあらゆる部分または他のアプリケーションにも安全にインポートして使用することが可能です。

リファクタリングした登録フォームコンポーネント(コンテナ)

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" (click)="registerUser()"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}

  registerUser() {
    const userFormData = this.userService.form.value;
    // some validation of user data
    // ...
    this.userService.registerUser(userFormData);
  }
}

この登録フォームは、ボタンを使用し、ボタンクリックに反応して registerUser メソッドを呼び出すようになりました。 このメソッドのロジックはこのフォームに密に関連しているため、それを配置する最適な場所と言えます。

これは非常に単純な例であり、コンポーネントツリーにあるレベルは 2 つだけです。 このパターンには、コンポーネントツリーのレベルが多い場合に落とし穴があります。

より高度な例

これは実際の例ではありませんが、このパターンに潜在する問題を理解する上で役立つと思います。

以下のようなコンポーネントツリーがあるとします(上から下)。

user-orders - トップレベルのコンポーネント。 サービスレイヤーに話しかけ、ユーザーとその注文に関するデータを受け取り、それを後続のツリーに渡して注文のリストをレンダリングするコンテナコンポーネントです。

user-orders-summary - 中間レベルのコンポーネント ユーザーの注文リストの上に合計注文数を表示するバーをレンダリングするプレゼンテーションコンポーネントです。

cashback - 最下レベル(リーフ/葉)コンポーネント。 ユーザーの合計キャッシュバック額を表示し、銀行口座に引き落とすためのボタンを持つプレゼンテーションコンポーネントです。

トップレベルのコンテナコンポーネント

トップレベルの user-orders コンテナコンポーネントを見てみましょう。

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  user$: Observable<User>;
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
    private userService: UserService
  ) {}

  ngOnInit() {
      this.user$ = this.userService.user$;
      this.orders$ = this.ordersService.getUserOrders();
  }

onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
}
<div class="orders-container">
    <user-orders-summary
        [orders]="orders$ | async"
        [cashbackBalanace]="(user$  | async).cashBackBalance"
        (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"
    >
    </user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

ご覧のとおり、user-orders コンポーネントは、user$orders$ の 2 つの Obeservable を定義し、非同期パイプをテンプレートに使用してそれらを購読しています。 このコンポーネントはデータをプレゼンテーションコンポーネントの user-orders-summary に渡し、注文リストをレンダリングします。 また、user-orders-summary から発行される requestCashbackWithdrawal カスタムイベントに反応してサービスレイヤーに話しかけます。

中間レベルのプレゼンテーションコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback [balance]="cashbackBalanace" (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
    @Input() cashbackBalanace: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

このコンポーネントは、上記でリファクタリングしたボタンコンポーネントに非常に似ています。 その入力から受け取ったデータをレンダリングし、何らかのユーザーアクションでカスタムイベントを発行します。 サービスの呼び出しは行わず、ビジネスロジックも含まれません。 そのため、これは別のプレゼンテーション cashback コンポーネントを使用する純粋なプレゼンテーションコンポーネントです。

最下レベルのプレゼンテーションコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{balance}}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent {
    @Input() balance: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

これは、入力からデータを受け取って出力でイベントをスローするだけの、もう 1 つのプレゼンテーションコンポーネントです。 非常に単純で再利用可能ですが、コンポーネントツリーに問題があります

user-orders-summary コンポーネントと cashback コンポーネントには、類似する入力(cashbackBalanacebalance)と同じ出力(requestCashbackWithdrawal)があることに気づいたことでしょう。 これは、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるためです。 この設計ではツリーのレベル数が増えるほど、問題が悪化してしまいます。 この問題について詳しく見てみましょう。

問題 1 - 中間レベルのプレゼンテーションコンポーネントにある無関係なプロパティ

user-orders-summarycashbackBalanace の入力を受け取って、さらにツリーの下の方に渡すだけであり、それ自体がその入力を使用することはありません。 このような状況に直面した場合、コンポーネントツリーの設計に欠陥がある可能性があります。 実際の状況で使用するコンポーネントには多数の入力と出力があるため、この設計では多数の「プロキシ」入力が含まれてしまい、中間レベルのコンポーネントの再利用可能性が薄れてしまいます(子コンポーネントに密に連携させる必要があるため)。また、繰り返し可能なコードが多数含まれてしまいます。

問題 2 - 下方レベルからトップレベルのコンポーネントに湧き上がるカスタムイベント

この問題は、前の問題に非常に似ていますが、コンポーネントの出力に関連しています。 ご覧のとおり、requestCashbackWithdrawal カスタムイベントは、cashbackuser-orders-summary コンポーネントで繰り返されています。 これもやはり、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるために起きていることです。 また、中間コンポーネント自体を再利用できないものにしてしまっています。

これらの問題に対する潜在的な解決策は少なくとも 2 つあります。

1 - ngTemplateOutlet を使用して、中間レベルのコンポーネントをよりコンテンツに依存しないコンポーネントにし、より深いコンポーネントをコンテナコンポーネントに直接公開する。 この解決策には専用の記事にするだけの価値があるため、今回はこれについて説明しないでおきます。

2 - コンポーネントツリーを再設計する。

コンポーネントツリーをリファクタリングする

作成したコードをリファクタリングして、無関係なプロパティと中間レベルのコンポーネントで湧いているイベントの問題を解決できるか見てみましょう。

リファクタリングしたトップレベルのコンポーネント

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
  ) {}

  ngOnInit() {
      this.orders$ = this.ordersService.getUserOrders();
  }
}
<div class="orders-container">
    <user-orders-summary [orders]="orders$ | async"></user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

トップレベルのコンテナコンポーネントから user$ Observable と onRequestCashbackWithdrawal() メソッドを削除しました。 非常に単純化され、user-orders-summary コンポーネント自体をレンダリングするために必要なデータのみを渡し、その子コンポーネントの cashback には渡さないようになっています。

リファクタリングした中間レベルのコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
}

これもかなり単純化されています。 入力は 1 つだけで、合計注文数をレンダリングするようになっています。

リファクタリングした最下レベルのコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{ (user$ | async).cashbackBalance }}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent implements OnInit {
  user$: Observable<User>;

  constructor(
     private ordersService: OrdersService,
     private userService: UserService
  ) {}

  ngOnInit() {
    this.user$ = this.userService.user$;
  }

  onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
  }
}

すごいです。 ご覧のとおり、プレゼンテーションコンポーネントではなくなっています。 トップレベルのコンポーネントに非常に似ており、コンポーネントツリーの下にあるコンテナコンポーネントになりました。 このようにリファクタリングすることで、コンポーネントツリー全体の設計と API、およびコンポーネントツリーの上位にある 2 つのコンポーネントのロジックを単純化できました。

新しい cashback コンポーネントの再利用可能性についてはどうでしょうか? まだ、再利用可能なままです。それには、コンポーネント自体に関連するロジックのみが含まれているため、以前として独立性を維持しています。

新しいコンポーネントツリーの設計は、管理性、合理性、および細分化がさらに改善されているようです。 湧き上がってくるイベントが無くなり、コンポーネントツリー全体で繰り返し可能な入力もありません。総合的な設計もはるかに単純になっています。 これは、コンポーネントツリーの下にコンテナコンポーネントを追加することで達成しました。 この手法は、コンポーネントツリーの設計を単純化するために使用できますが、再利用可能性を大幅に失うことなく、ツリー内のどのコンポーネントをコンテナにするのが適しており、どれが純粋なプレゼンテーションコンポーネントであるべきかを十分に理解しておくことが必要です。 これはバランスと設計の選択に関する課題であり、アプリのアーキテクチャを作成するときには必ず発生するものです。

「コンテナ・プレゼンテーション」パターンは非常に誤解しやすいもので、コンテナコンポーネントは必ずトップレベルコンポーネントであるべきだと考えがちです(直感的に、コンテナはローカルコンポーネントツリー内のその他すべてのコンポーネントを格納するものです)。 しかし、そうではありません。コンテナコンポーネントはコンポーネントツリーのあらゆるレベルに配置することができます。上記で見たように、リーフレベルにも配置可能です。 私はコンテナコンポーネントをスマート(賢い)コンポーネントと呼んでいます。私にとって、これらのコンポーネントがビジネスロジックを含み、コンポーネントツリーのあらゆる場所に配置できるということが非常に明白であるためです。

後書き

ここまでで、「コンテナ・プレゼンテーション」パターンの概要とその実装における潜在的な問題について理解できたことと思います。

できるだけ単純に維持するよう努めましたが、このパターンに関しては非常に多くの情報が存在します。

ご質問やメッセージがございましたら、お気軽にコメント欄に投稿してください。

次回の記事では、Angular における changeDetectionStrategy についてお話しします(この記事に非常に関連する内容です)。

それではまた!

0
0 764
記事 Toshihiko Minamoto · 11月 29, 2022 11m read

中・上級トピックに進む前に、より一般的なポイントについてまとめておきたいと思います。 これはもちろん主観的な内容であるため、他の意見やさらに良い根拠をお持ちであれば、それについて喜んでお聞きします。

このリストは包括的ではありません。一部のトピックは今後の記事で対応するつもりなので、意図的にそうしています。

ヒント 1. 公式スタイルガイドに従う

Angular は、アプリケーションに使用可能なアーキテクチャを制限するという点で非常に厳格ですが、それでも独自の方法で行えることはたくさんあります。 開発者の想像力は無限ではありますが、そのために、あなたと、またはあなたを引き継いでプロジェクトに携わる人の作業が困難になってしまうことがあります。

Angular チームは、Angular アーキテクチャ自体とそのライブラリをうまく管理しているため、安定した対応可能なコードベースを作成する方法をよく理解しています。

したがって、公式スタイルガイドに従い、そのとおりに動作しない場合にのみ他の方法を取るようにすることをお勧めします。 こうすることで、新しいプロジェクトに参加する際や、他の人がプロジェクトに参加する際に、事がより簡単に進められるようになります。

コードとアプリアーキテクチャが対応可能で安定しており、理解しやすい方が、賢明でありながらも暗号的なソリューションを作るよりも重要です。誰も追いつけなくなってしまいます(開発者自身が後に追いつけなくなる可能性もあります)。

Angular 公式スタイルガイド: https://angular.io/guide/styleguide

ヒント 2. Angular の Ninja 本を購入する

宣伝と思われるかもしれませんが、つまりこういうことです。Angular のすべての主要概念がカバーされた非常に良い本で、価格はあなたが決められます。 また、価格のどれくらいが作者に支払われ、どれくらいが慈善活動に募金されるかを決めることもできます。

この本の作者の 1 人は Angular チームのメンバーであるため、公式ドキュメントの次に最も信頼できる Angular の情報源であることは間違いありません。 本の章構成は、本のページで確認できるようになっているため、サンプルページを読んでから購入する価値があるかを決めることができます。

さらに、この本は Angular の新しいバージョンのリリースで更新されており、本のすべての更新内容を無料で得られます。

Ninja Squad ブログ自体も Angular に関する非常に優れた情報源で、新しいバージョンに関する記事や最新情報、ベストプラクティス、実験的機能などが掲載されています。

Angular の Ninja 本: https://books.ninja-squad.com/angular

ヒント 3. 公式ドキュメントを読む

アプリのコードを書き始める前、特に、以前に使用したことのないバージョンの Angular でプロジェクトに取り組む場合には、公式ドキュメントとガイドに目を通すことをお勧めします。 廃止機能は常に増えており、インターネット上のチュートリアルとガイドが古くなっている可能性があります。そのため、新しいベストプラクティスや機能を使用する代わりに、技術的負債が増え続けてしまうことになりかねません。

ガイドがどのバージョンを対象にして書かれているのかを確認するのも良い習慣です。 使用しているのが 2 つ以上前のバージョンであれば、それが書かれた当時から変更されたことがないかを確認することをお勧めします。

公式ドキュメント: https://angular.io

ヒント 4. Angular Material を使用しなくても、Angular CDK の使用を検討する

これについては今後の記事に取り上げるのが良いと思いますが、私は、多くの Angular 開発者は Angular CDK の存在すらも知らないことを知っています。

Angular CDK は、より優れたアプリケーションの開発を支援できる便利なディレクティブと基底クラスが集められたライブラリです。 たとえば、FocusTrap、Drag & Drop、VirtualScroll などが含まれており、コンポーネントに簡単に追加することができます。

Angular CDK: https://material.angular.io/cdk/categories

ヒント 5. package.json 内の依存関係を修正する

特に Angular に関連することではありませんし、どんなプロジェクトでも重要なことかもしれません。 npm install --save <something> を実行すると、パッケージバージョンの先頭に ^ または ~ 付きで package.json に追加されます。 つまり、プロジェクトはその依存関係と同じメジャーバージョンのあらゆるマイナー/パッチバージョンを使用できるということになります。 この場合、将来的にほぼ 100% の確率で問題となります。 私はさまざまなプロジェクトにおいて何度もこの問題に直面しました。 時間が経過し、ある依存関係の新しいマイナーバージョンが作成されれば、アプリはビルドできなくなります。 この依存関係の作成者がそれをマイナー(またはパッチ)バージョンで更新したために、ビルドと競合するようになってしまうこともあるでしょう。 また、依存関係の新しいマイナーバージョンで新しいバグが見つかることもあります。自分のコードでは問題がなくても(新しいバージョンがリリースされる前に依存関係をインストールしたため)、リポジトリから自分のプロジェクトをインストールしてビルドしようとしている人たちが、自分が気付いていないバグに直面することもあります(また、自分のマシンでそのバグを再現することもできません)。

package-lock.json は、この問題を解決し、複数の開発者がプロジェクト全体で同じ依存関係セットを使用できるようにするために存在します。 リポジトリにコミットすることが理想的ですが、このファイルを .gitignore に追加している開発者がたくさんいます。 それでは意味がなく、結局、上記と同じ問題が起きてしまいます。 そのため、依存関係の解決を盲目的に信頼して特定のバージョンに固定するのではなく、定期的に脆弱性をスキャンして(npm audit を使用)、手動で更新してテストすることをお勧めします。

ヒント 6. できるだけ早期にアプリを新しい Angular バージョンにアップグレードするようにする

Angular は絶えず進化しており、約半年ごとに新しいバージョンがリリースされています。 アプリを最新状態に維持することで、ビルドの高速化やその他の最適化を含むすべての新機能を使用することができます。 この場合には、Angular の次期メジャーバージョンへのアップグレードは大した問題ではないでしょう。 ただし、Angular には、Angular の中間バージョンをスキップしてアプリをアップグレードするためのスケマティックがないため、5 バージョン前の Angular を使用しており、アプリケーションの規模が中~大である場合には、最新バージョンへの更新プロセスが困難になる可能性があります。 各バージョンでアプリケーションが動作することを確認しながら、すべての中間バージョンを 1 つずつアップグレードすることが必要になるでしょう。 Angular CLI のスケマティックを使わずに直接アップデートすることは可能ですが、コツが必要となるため、常にアプリを最新状態に維持しておくことをお勧めします。

ヒント 7. 依存関係リストをできるだけ短くする

新機能が必要となるたびに新しい依存関係をアプリに採り入れるという習慣に陥りやすいものですが、 依存関係ごとに、アプリの複雑性が増し、技術的負債が突然雪崩れてしまうリスクが高まります。

アプリに新しい依存関係を追加すると決めた場合は、以下のことを検討してください。

  • どれほど十分に依存関係がサポートされているか?
  • どれくらいの人が使用しているか?
  • 開発チームはどれくらいの規模か?
  • どれくらい迅速に GitHub 課題をクローズしているか?
  • どれくらい十分に依存関係のドキュメントが書かれているか?
  • Angular の新しいメジャーバージョンがリリースされてからどれくらい迅速に更新されているか?
  • この依存関係によるアプリケーションのパフォーマンスとバンドルサイズへのインパクトはどれくらいか?
  • 将来的に放置または廃止された場合に、この依存関係を入れ替えるのがどれくらい困難になるか?

この質問のいくつかに否定的な回答がある場合、その依存関係を排除するか、少なくとももっと成熟して十分にサポートされているものに交換することを検討してください。

ヒント 8. 「暫定的な」型として <any> を使用しない

繰り返しますが、適切な型を書くには時間がかかるものであり、このスプリントでタスクを完了させる必要があるため、ビジネスロジックを書く際に簡単に any を使いがちです。 もちろん、型は後で追加できますが、 技術的負債がすぐに積もる危険があります。

アプリと型のアーキテクチャは、ビジネスロジックを書く前に定義しておく必要があります。 どのようなオブジェクトがあり、どこで使用されるかを明確に理解しておくかなければなりません。 コードの前に仕様書、ですよね? (Tom Demarco は、この考えが主流になる前にこれについての本を書いています: https://www.amazon.com/Deadline-Novel-About-Project-Management/dp/0932633390

事前に定義された型を使わずにコードを書いている場合、非常に似ているようで異なるオブジェクトを使用する、質の悪いアプリアーキテクチャと関数が出来上がってしまう可能性があります。 そのため、関数ごとに異なる型を作成する(余計に悪くなってしまいます)か、仕様書、型、およびリファクタリングを書くことに時間を費やす必要があるでしょう。後者は、前もってやっていれば時間の無駄になりません。

ヒント 9. ビルドプロセスを理解するのに時間をかける

Angular は、プロジェクトの構築に関する開発者の作業をうまく容易にしていますが、 デフォルトのオプションは、すべての場合に必ずしも最適ではありません。

Angular のビルドプロセスの仕組み、開発ビルドと本番ビルドの違い、Angular で提供されているビルドのオプション(最適化、ソースマップ、バンドル化など)を理解する時間を取りましょう。

ヒント 10. バンドル内のものを調べる

すべてのライブラリがツリーシェイキングを提供しているわけでも、必ずしもすぐにインポートするわけでもありません。そのため、冗長するものがアプリにバンドルされる可能性が必ずあります。

したがって、たまにバンドルの中身を調べる習慣をもつことをお勧めします。

webpack-bundle-analyzer を使ったプロセスをうまく説明した記事がいくつかあるため、ここでは説明しませんが、その中の 1 つのリンクをご覧ください: https://www.digitalocean.com/community/tutorials/angular-angular-webpack-bundle-analyzer

このトピックについては、連載の後の方でより詳しく説明します。

ヒント 11. モジュールにサービスをインポートする代わりに、サービスの providedIn プロパティを使用する

インターネットに公開されている Angular コードサンプルの多くは、モジュールへのサービスのインポートを使用しています。 ただし、Angular 6 または 7 以降では、これはサービスを宣言する方法として推奨されていません。 ツリーシェイキングを有効にし、アプリケーションのバンドル化を改善するには、@Injectable デコレータのプロパティ providedIn を使用する必要があります。 Angular は、どのバンドルにサービスを含める必要があるのか、いつ初期化すべきなのか、サービスのインスタンスをいくつ作成する必要があるのかを理解できるほどスマートなフレームワークです。

providedIn が受け入れる値には 3 つあります。 ほとんどの場合は root で十分ですが、そのほかに 2 つあります。

  • root: サービスアプリケーションの範囲でシングルトンになります。
  • any: すべての Eager ロードされたモジュールにサービスのインスタンスが 1 つ作成され、遅延モジュールごとに別のインスタンスが作成されます。
  • platform: 同じページで実行するすべてのアプリケーションに対し、サービスはシングルトンになります。

ヒント 12. 主要なパフォーマンスルールを忘れないこと

  • 再描画操作と JavaScript 実行を減らすには、コレクションに trackBy を使用する
  • 使用できるすべての場所に onPush 変更検出ストラテジーを使用する(これについては、専用の記事で説明します)
  • 高い処理能力を必要とする計算は、ngZone の外で実行する
  • イベントにスロットル・デバウンスパターンを使用して、不要なサーバー呼び出しとイベントの大量送信を防止する
  • ビッグデータセットの表示には仮想スクロールを使用する
  • テンプレートにデータ変換用の純粋なパイプを使用する
  • AoT コンパイルを使用する
  • アプリケーションの起動には必要ないアプリの部分を遅延読み込みする
  • テンプレートでは計算と条件付き関数呼び出しを使わない(関数呼び出しはイベントのみに使用します)

お読みいただきありがとうございました! いくつかのヒントがお役に立ちますように。 コメントやご意見がございましたら、コメント欄でお知らせください。喜んでお伺いします 😃

それではまた!

0
0 141
記事 Toshihiko Minamoto · 11月 17, 2022 10m read

こんにちは! Sergei Sakisian と申します。InterSystems で 7 年以上、Angular フロントエンドを作成しています。 Angular は非常に人気のあるフレームワークであるため、開発者、お客様、そしてパートナーの皆さんは、アプリケーションのスタックの 1 つとして Angular を選択することがよくあります。

概念、ハウツー、ベストプラクティス、高度なトピックなど、Angular のさまざまな側面を網羅する記事の連載を始めたいと思います。 この連載は、すでに Angular に精通しており、基本概念の説明がいらない方が対象となります。 連載記事のロードマップを作成しているところであるため、まずは、一番新しい Angular リリースの重要な機能をいくつか紹介することから始めることにします。

厳格な型指定のフォーム

これはおそらく、過去 2 年間で最も要望の多かった Angular 機能です。 Angular 14 では、Angular リアクティブフォームを使って、TypeScript のすべての厳格な型チェック機能を使用できるようになりました。

FormControl クラスはジェネリクスになったため、それが保持する値の型を取ることができます。

/* Angular 14 より前*/
const untypedControl = new FormControl(true);
untypedControl.setValue(100); // 値を設定、エラーなし

// 現在
const strictlyTypedControl = new FormControl<boolean>(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます

// Angular 14
const strictlyTypedControl = new FormControl(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます

ご覧のとおり、最初の最後の例はほぼ同じですが、結果が異なります。 これは、Angular 14 では、新しい FormControl クラスが、開発者が指定した初期値から型を推論しているためです。 したがって、true が指定された場合、Angular はこの FormControl の型を boolean | null に設定します。 .reset() メソッドには、値が指定されていない場合に値を null にする Nullable 値が必要です。

以前の型なしの FormControl クラスは、UntypedFormControl に変換されています(UntypedFormGroupUntypedFormArray、および UntypedFormBuilder についても同様)が、実質的に FormControl<any> のエイリアスです。 以前のバージョンの Angular からアップグレードしている場合、FormControl クラスのすべてのメンションは、Angular CLI によって UntypedFormControl クラスに置き換えられます。

Untyped* のクラスは、以下のような特定の目的に使用されます。

  1. アプリを、以前のバージョンから移行される前とまったく同じように動作させる(新しい FormControl は、初期値から型を推論することに注意してください)。
  2. すべての FormControl<any> を意図的に使用する。 そのため、すべての UntypedFormControl を手動で FormControl<any> に変更する必要があります。
  3. 開発者にもっと自由度を与える(これについては、後の方で説明します)。

初期値が null である場合、FormControl の型を明示的に指定する必要があることに注意してください。 また、TypeScript には、初期値が false の場合に同じことを行う必要のあるバグが存在します。

フォームの Group については、インターフェースを定義することも可能です。このインターフェースを FormGroup の型として渡すだけです。 この場合、TypeScript は FormGroup 内のすべての型を推論します。

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

手動で FormGroup を作成した上記の例のように、FormBuilder の .group() メソッドに、事前に定義されたインターフェースを受け入れられるジェネリクス属性が追加されました。

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const fb = new FormBuilder();
const login = fb.group<LoginForm>({
    email: '',
    password: '',
});

このインターフェースにはプリミティブな非 nullable 型しかないため、新しい nonNullable FormBuilder プロパティ(NonNullableFormBuilder クラスインスタンスを含み、直接作成することも可能)を使って以下のように単純化できます。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

❗ 非 nullable 型の FormBuilder を使用する場合、または FormControl に非 nullable 型のオプションを設定する場合、.reset() メソッドを呼び出す際に、リセット値として初期の FormControl 値が使用されることに注意してください。

また、this.form.value のすべてのプロパティがオプションとしてマークされることに注意することも非常に重要です。 以下に例を示します。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

// login.value
// {
//   email?: string;
//   password?: string;
// }

これは、FormGroup 内のいずれかの FormControl を無効にする際に、この FormControl の値が form.value から削除されるために発生します。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

login.get('email').disable();
console.log(login.value);

// {
//   password: ''
// }

フォームオブジェクト全体を取得するには、.getRawValue() メソッドを使用する必要があります。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

login.get('email').disable();
console.log(login.getRawValue());

// {
//   email: '',
//   password: ''
// }

厳格に型付けされたフォームのメリット:

  1. FormControl / FormGroup の値を返すすべてのプロパティとメソッドが厳格に型付けされるようになった。 例: valuegetRawValue()valueChanges
  2. FormControl 値を変更するすべてのメソッドが型安全になった。setValue()patchValue()updateValue()
  3. FormControl が厳格に型付けされた。 このことは、FormGroup の .get() メソッドにも適用されます。 これにより、コンパイル時に存在しない FormControl へのアクセスも防止されます。

新しい FormRecord クラス

新しい FormGroup クラスの欠点は、その動的な性質が失われたことです。 一度定義されると、オンザフライで FormControl を追加または削除することはできません。

この問題を解決するために、Angular は新たに FormRecord クラスを追加しました。 FormRecord は実質的に FormGroup と同じですが、動的であり、そのすべての FormControl に同じ型が使用されます。

folders: new FormRecord({
  home: new FormControl(true, { nonNullable: true }),
  music: new FormControl(false, { nonNullable: true })
});

// グループに新しい FormContol を追加する 
this.foldersForm.get('folders').addControl('videos', new FormControl(false, { nonNullable: true }));

// コントロールの型が異なるため、これにより、コンパイルエラーが発生する
this.foldersForm.get('folders').addControl('books', new FormControl('Some string', { nonNullable: true }));

ご覧のとおり、これには別の制限があります。すべての FormControl は同じ型でなければなりません。 動的と異種の両方を兼ね備えた FormGroup がどうしても必要な場合は、UntypedFormGroup クラスを使用してフォームを定義することをお勧めします。

モジュールレス(スタンドアロン)コンポーネント

これは未だ実験的とされている機能ではありますが、興味深い機能です。 コンポーネント、ディレクティブ、およびパイプをモジュールに含めることなく、これらを定義することができます。

この概念はまだ完全に練られてはいませんが、すでに ngModule を使用せずにアプリケーションをビルドすることができるようになっています。

スタンドアロンコンポーネントを定義するには、Component/Pipe/Directive デコレーターで新しい standalone プロパティを使用する必要があります。

@Component({
  selector: 'app-table',
  standalone: true,
  templateUrl: './table.component.html'
})
export class TableComponent {
}

この場合、このコンポーネントはどの ngModule にも宣言されませんが、 ngModule やその他のスタンドアロンコンポーネントにインポートすることは可能です。

各スタンドアロンコンポーネント/パイプ/ディレクティブには、その依存関係を直接デコレーターにインポートするメカニズムが備えられています。

@Component({
  standalone: true,
  selector: 'photo-gallery',
  // 既存のモジュールは直接スタンドアロンコンポーネントにインポートされる
  // CommonModuleは、*ngIf などの標準の Angular ディレクティブを使用するために直接インポートされる
  // 上記に宣言されるスタンドアロンコンポーネントも直接インポートされる
  imports: [CommonModule, MatButtonModule, TableComponent],
  template: `
    ...
    <button mat-button>Next Page</button>
    <app-table *ngIf="expression"></app-table>
  `,
})
export class PhotoGalleryComponent {
}

前述のとおり、スタンドアロンコンポーネントは、既存の ngModule にインポート可能です。 sharedModule 全体をインポートする必要がなく、本当に必要な物だけをインポートできます。 新しいスタンドアロンコンポーネントを使用し始めるのに適したストラテジーでもあります。

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, TableComponent], // import our standalone TableComponent
  bootstrap: [AppComponent]
})
export class AppModule {}

スタンドアロンコンポーネントは、Angular CLI を使って以下を入力すると作成できます。

ng g component --standalone user

モジュールレスアプリケーションをブートストラップ

アプリケーションにあるすべての ngModule を排除する場合は、別の方法でアプリをブートストラップする必要があります。 Angular にはこのための新しい関数があり、それを main.ts ファイルで呼び出す必要があります。

bootstrapApplication(AppComponent);

この関数の 2 つ目のパラメーターを使って、アプリ全体で必要なプロバイダーを定義できます。 通常プロバイダーのほとんどはモジュール内に存在するため、Angular は(現時点では)それに新しい importProvidersFrom 抽出関数を使用する必要があります。

bootstrapApplication(AppComponent, { providers: [importProvidersFrom(HttpClientModule)] });

スタンドアロンコンポーネントの遅延読み込みルート:

Angular には、loadComponent という新しい遅延読み込みルート関数があります。これは、スタンドアロンコンポーネントを読み込むためだけに存在する関数です。

{ 
  path: 'home',
  loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
}

loadChildren は、ngModule を遅延読み込みできるようにするだけでなく、ルートファイルから直接、子ルートも読み込めるようになっています。

{ 
  path: 'home',
  loadChildren: () => import('./home/home.routes').then(c => c.HomeRoutes)
}

記事の執筆時点におけるいくつかの注意事項

  • スタンドアロンコンポーネント機能は、現在も実験的段階にあります。 将来的に、Webpack の代わりに Vite ビルダーに移行し、ツーリングの改善、ビルド時間の高速化、アプリアーキテクチャの強化、テスト方法の改善などを通じて、機能が大幅に改善されるでしょう。 現時点では、こういったものが多数欠けているため、全パッケージを受け取っていません。いずれにせよ、少なくともこの新しい Angular パラダイムを念頭に、アプリを開発し始めることは可能です。
  • IDE と Angular ツールはまだ、新しいスタンドアロンエンティティを静的に解析する準備を整えていません。 すべての依存関係を各スタンドアロンエンティティにインポートする必要があるため、何かを見逃した場合、コンパイラーもそれを見逃し、ランタイム時に失敗する可能性があります。 これは今後改善されていきますが、現時点ではインポートの際に開発者側の注意が必要です。
  • 現時点では Angular にグローバルインポート機能がないため(Vue などで行われるように)、各スタンドアロンエンティティで、依存関係を確実に 1 つずつインポートする必要があります。 この機能の主な目標は、私が思うところ、ボイラープレートを減らして物事を簡単に実行できるようにすることにあるため、今後のバージョンで解決されることを期待しています。

今日は、これで以上です。 それではまた!

0
0 1049
お知らせ Mihoko Iijima · 6月 19, 2022

開発者の皆さん、こんにちは!21回目の InterSystems プログラミングコンテストを開催します!

お題は👉 🏆 InterSystems Full Stack コンテスト 2022 🏆 です!

期間: 2022年6月27日~7月17日まで(詳細は末尾をご参照ください)

💰賞金総額: $10,000 💰


0
0 171
記事 Toshihiko Minamoto · 4月 7, 2022 4m read

皆さん、こんにちは!

@Henrique.GonçalvesDias と私は、MessageViewer の最近のアップデートにおいて、IRIS Interoperability でメッセージを可視化する新しい方法を提案しました。 そのアップデートでは、UML シーケンス図に基づく可視化を提供しようとしました。 詳細は、の記事をご覧ください。

このようなダイアグラムを描画するために必要な困難な幾何学計算を行えるように、素晴らしい mermaid JS オープンソースライブラリを使用しました。 この記事で紹介したいのは、このライブラリの使用方法です。 ここでは、シーケンス図のみに焦点を当てますが、このライブラリでは非常にたくさんのことを行えることを忘れないでください。

mermaid では Markdown に着想を得た構文を使用して、ダイアグラムを定義します。 非常に直感的であるため、退屈な文章を長々と書くのではなく、例を紹介したいと思います。

sequenceDiagram
Alice->>John: Hello John, how are you?loopHealthcheck    John->>John: Fight against hypochondriaendNoterightofJohn: Rational thoughts!John-->>Alice: Great!John->>Bob: How about you?

mermaid エンジンはこの定義に基づいて、SVG を使って直接 Web ページに以下のダイアグラムをレンダリングします。

この例は mermaid のドキュメントから得たもので、このオンラインエディターで試すことができます。 色々試すことのできる構成がたくさんあります。

ご覧の通り、ダイアグラムの定義では、アクターと参加者、相互に送信しているイベントとメッセージを指定するだけです。

また、Web ページにダイアグラムを表示するには、div コンテナにダイアグラムを指定し、mermaid エンジンを初期化してダイアグラムをレンダリングする JS コードのみが必要です。 

<divclass="mermaid">
sequenceDiagramAlice->>John: Hello John, how are you?loopHealthcheck    John->>John: Fight against hypochondriaendNoterightofJohn: Rational thoughts!John-->>Alice: Great!John->>Bob: How about you?</div>
mermaid.initialize({
    startOnLoad:true,    theme:'forest'});

この例は、こちらのフィドルにあります。

これは、提案された作業のフロントエンドベースです。 バックエンドでは、IRIS 相互運用性セッションからメッセージを取得し、適切な JSON オブジェクトにフォーマットしてフロントエンドに送り返す REST エンドポイントを設定する必要があります。 この記事の焦点はフロントエンドのコードであるため、バックエンドの実装には触れませんが、dispatchservice クラスでこれを確認できます。

バックエンドは、以下のような JSON を送り返します。

{
    "participants":[        "diashenrique.messageviewer.Service.SendMessage",        "diashenrique.messageviewer.Operation.ConsumeMessageClass"    ],    "messages":[        {            "id":"1182",            "from":"diashenrique.messageviewer.Service.SendMessage",            "to":"diashenrique.messageviewer.Operation.ConsumeMessageClass",            "message":"2021-10-05 03:16:56.059 SimpleMessage"        },        {            "id":"1183",            "from":"diashenrique.messageviewer.Operation.ConsumeMessageClass",            "to":"diashenrique.messageviewer.Service.SendMessage",            "message":"2021-10-05 03:16:56.06 NULL"        }    ]}

最後に、簡単な JS 関数を使用して、この JSON を mermaid シーケンス図の仕様に合わせて以下のように変換します。

sequenceDiagram
autonumberparticipantP0asdiashenrique.messageviewer.Service.SendMessageparticipantP1asdiashenrique.messageviewer.Operation.ConsumeMessageClassP0->>P1: 2021-10-05 03:16:56.059 SimpleMessageP1->>P0: 2021-10-05 03:16:56.06 NULL

そして、これがレンダリングされたシーケンス図です。

完全な JS コードはこちらで確認できます。

以上です。 この記事があなたの素晴らしいプロジェクトにいくらかでも役立てられれば幸いです。

それではまた!

0
0 1094
記事 Toshihiko Minamoto · 8月 31, 2021 12m read

最初の記事では、Caché Webアプリケーションのテストとデバッグを外部ツールを用いて行うことについて説明しました。 2回目となるこの記事では、Cachéツールの使用について説明します。

以下について説明します。

  • CSP GatewayとWebappの構成
  • CSP Gatewayのロギング
  • CSP Gatewayのトレース
  • ISCLOG
  • カスタムロギング
  • セッションイベント
  • デバイスへの出力

CSP GatewayとWebappの構成

まず初めに、フロントエンドアプリケーションをデバッグする場合、特にそれを開発している場合は、キャッシュは必要ありません。 本番システムでは役立ちますが、開発中には不要です。 Webアプリケーションのロギングを無効にするには、システム管理ポータル → メニュー → Webアプリケーションの管理 → <あなたのWebアプリ>に移動して、Serve Files Timeoutを0に設定します。 そして[保存]ボタンを押します。

次に、webアプリケーションの既存のキャッシュを消去する必要があります。 これを行うには、システム管理ポータル → システム管理 → 構成 → CSP Gatewayの管理 → システムステータスに移動します。 「Cached Forms」テーブルを探し、その最後の行にある「Total」の消去ボタン(ドット)を押してWebアプリケーションのキャッシュを消去します。

CSP Gatewayのロギング

CSP Gatewayに関しては、受信リクエストのロギングがサポートされています(ドキュメント)。 [デフォルトのパラメーター]タブで、希望するログレベル(v9aなど)を指定して、変更を保存します。 v9aは、すべてのHTTPリクエストをGatewayホームディレクトリのhttp.logに記録します(ほかのオプションについてはドキュメントをご覧ください)。 リクエストは次のようにキャプチャされます。

GET /forms/form/info/Form.Test.Person HTTP/1.1
Host: localhost:57772
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:57772/formsui/index.html
Cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000yxiocLLb8bbc9SVXQJC5WMU831n2sENf4OGeGa; CSPWSERVERID=144zwBTP
Dnt: 1
Connection: keep-alive
Cache-Control: max-age=0
<>

また、パフォーマンスを記録するオプションもあります。 結果はファイルに出力されるか、[イベントログの表示]タブで閲覧することができます。

CSP Gatewayのトレース

そして最後に、CSP Gatewayの[HTTPトレースの表示]タブでリクエストとレスポンスをトレースすることができます。 トレースを有効にすると、リクエストのキャプチャがすぐに開始します。 デバッグを終えたら、忘れずに無効にしてください。 デバッグセッションは次のように行われます。

注意: トレースのやり方がわかっている場合は、エラーを簡単に理解し、問題を再現することができます。 ロギングは統計収集、パフォーマンスプロファイリングなどに使用します。

また、ほとんどのWebサーバーにも、ロギングとパフォーマンス追跡ツールが備わっています。

ISCLOG

CSP Gatewayは、ネットワークの問題の特定とパフォーマンスの追跡には役立ちますが、Caché内で起きていることをログするにほかのツールが必要となります。  そのようなツールの中でも汎用性に優れているのがISCLOGです。 ドキュメントをご覧ください。

これは、現在のリクエスト処理に関する情報を保存できるグローバル変数です。 ロギングを開始するには、以下を実行します。

set ^%ISCLOG = 2

ロギングを終了するには、以下を実行します。

set ^%ISCLOG = 0

リクエストは次のようにロギングされます。

^%ISCLOG=0
^%ISCLOG("Data")=24
^%ISCLOG("Data",1)=$lb(2,"CSPServer","Header from CSP Size:3744 CMD:h IdSource:3","4664","FORMS","2017-06-07 10:49:21.341","%SYS.cspServer2","","")
^%ISCLOG("Data",1,0)="^h30000 "_$c(14,0,0)_"A"
^%ISCLOG("Data",2)=$lb(2,"CSPServer","[UpdateURL] Looking up: //localhost/forms/form/info path found: //localhost/forms/ Appl= "_$c(2,1,3,4)_"@"_$c(3,4,1,2,1,9,1)_"/forms/"_$c(2,1,3,4,1,2,1,2,1,2,4,3,4,1,2,1,9,1,7,1)_":%All"_$c(8,1)_"/forms"_$c(7,1)_"FORMS"_$c(2,1,2,1,3,4,1,2,1,2,1,3,4,1,2,1,4,4,132,3,3,4,2,3,4,2,2,1,4,4,16,14,2,4,3,4,1,3,4,1,2,1,3,4,1,2,1,16,1)_"Form.REST.Main"_$c(2,4,2,4),"4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE")
^%ISCLOG("Data",3)=$lb(2,"CSPServer","[UpdateURL] Found cls: Form.REST.Main nocharsetconvert:  charset:UTF-8 convert charset:UTF8","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE")
^%ISCLOG("Data",4)=$lb(2,"CSPServer","[HTML] Determined request type","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer2","124","L3DfNILTaE")
^%ISCLOG("Data",4,0)=$lb("usesession",1,"i%Class","Form.REST.Main","i%Service","REST","NOLOCKITEM","","i%GatewayError","")
^%ISCLOG("Data",5)=$lb(2,"CSPSession","[%LoadData] Loading CSP session, nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Session.1","","L3DfNILTaE")
^%ISCLOG("Data",5,0)=$lb(900,,0,5567742244,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:04","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",4,"","","","","http://localhost:57772/formsui/index.html")
^%ISCLOG("Data",6)=$lb(2,"CSPServer","[CSPDispatch]Requested GET /forms/form/info","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",7)=$lb(2,"CSPServer","[CSPDispatch] ** Start processing request newSes=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",7,0)="/forms/form/info"
^%ISCLOG("Data",8)=$lb(2,"CSPServer","[CSPDispatch] Service type is REST has-soapaction=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",9)=$lb(2,"CSPServer","[CSPDispatch]About to run page: Form.REST.Main","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",9,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737)
^%ISCLOG("Data",10)=$lb(2,"CSPServer","[callPage] url=/forms/form/info ; Appl: /forms/ newsession=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",11)=$lb(2,"CSPServer","[callPage]Imported security context ; User: UnknownUser ; Roles: %All,%Developer","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",12)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE")
^%ISCLOG("Data",13)=$lb(2,"CSPResponse","[WriteHTTPHeaderCookies] Session cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic; path=/forms/;  httpOnly;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","124","L3DfNILTaE")
^%ISCLOG("Data",14)=$lb(2,"CSPServer","[callPage] Return Status","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",14,0)=1
^%ISCLOG("Data",15)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE")
^%ISCLOG("Data",16)=$lb(2,"CSPServer","[Cleanup]Page EndSession=0; needToGetALicense=-1; nosave=0; loginredirect=0; sessionContext=1","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",17)=$lb(2,"CSPSession","[Cleanup] EndSession=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",18)=$lb(2,"CSPSession","[%SaveData] Saved: ","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Session.1","","L3DfNILTaE")
^%ISCLOG("Data",18,0)=$lb(900,,0,5567742261,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:21","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",5,"","","","","http://localhost:57772/formsui/index.html")
^%ISCLOG("Data",19)=$lb(2,"CSPServer","[Cleanup] Restoring roles before running destructor","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE")
^%ISCLOG("Data",19,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737)
^%ISCLOG("Data",20)=$lb(2,"CSPServer","[Cleanup] End","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE")
^%ISCLOG("Data",20,0)="<-Finish processing request->"
^%ISCLOG("Data",21)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: ed-pc:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","")
^%ISCLOG("Data",22)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: 127.0.0.1:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","")
^%ISCLOG("Data",23)=$lb(2,"GatewayRequest","[SendSimpleCmd:Server:Failed] WebServer: 127.0.0.1:57772; Gateway Server Request Failed","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","")
^%ISCLOG("Data",23,0)=0
^%ISCLOG("Data",24)=$lb(2,"GatewayRequest","[GetMetrics]","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","")
^%ISCLOG("Data",24,0)="<-End Request Client->"

また、以下のような簡単なスクリプトを使用して、グローバルをファイルに出力することができます。

set p="c:\temp\isclog.txt"
open p:"NW"
use p zw ^%ISCLOG
close p

 

カスタムロギング

デフォルトのロギングツールも非常に優れていますが、いくつかの問題があります。

  • 内容が一般的であり、アプリケーションが認識されない
  • より詳細なオプションを使うと、パフォーマンスに影響する
  • 構造化されていないため、情報の抽出が困難な場合がある

そのため、より具体的なケースを網羅するために、独自のカスタムロギングシステムを作成することができます。 以下は、%requestオブジェクトの一部を記録する永続クラスのサンプルです。

/// Incoming request
Class Log.Request Extends %Persistent
{
/// A string indicating HTTP method used for this request.
Property method As %String;
/// A string containing the URL up to and including the page name
/// and extension, but not including the query string.
Property url As %String(MAXLEN = "");
/// A string indicating the type of browser from which the request
/// originated, as determined from the HTTP_USER_AGENT header.
Property userAgent As %String(MAXLEN = "");
/// A string indicating the MIME Content-Type of the request.
Property contentType As %String(MAXLEN = "");
/// Character set this request was send in, if not specified in the HTTP headers
/// it defaults to the character set of the page it is being submitted to.
Property charSet As %String(MAXLEN = "");
/// A %CSP.Stream containing the content submitted
/// with this request.
Property content As %Stream.GlobalBinary;
/// True if the communication between the browser and the web server was using
/// the secure https protocol. False for a normal http connection.
Property secure As %Boolean;
Property cgiEnvs As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column");
Property data As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column");
ClassMethod add() As %Status
{
    set request = ..%New()
    quit request.%Save()
}
Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
    #dim %request As %CSP.Request
    #dim sc As %Status = $$$OK
    quit:'$isObject($g(%request)) $$$ERROR($$$GeneralError, "Not a web context")
    set ..charSet = %request.CharSet
    if $isObject(%request.Content) {
        do ..content.CopyFromAndSave(%request.Content)
    } else {
        set ..content = ""
    }
    set ..contentType = %request.ContentType
    set ..method = %request.Method
    set ..secure = %request.Secure
    set ..url = %request.URL
    set ..userAgent = %request.UserAgent
    set cgi = ""
    for {
        set cgi=$order(%request.CgiEnvs(cgi))
        quit:cgi=""
        do ..cgiEnvs.SetAt(%request.CgiEnvs(cgi), cgi)
    }
    // Only gets first data if more than one data with the same name is present
    set data = ""
    for {
        set data=$order(%request.Data(data))
        quit:data=""
        do ..data.SetAt(%request.Get(data), data)
    }
    quit sc
}
}

Log.Request テーブルに新しいレコードを追加するには、コードに次の呼び出しを追加します。

do ##class(Log.Request).add()

これは非常に基本的なサンプルであり、必要に応じてコメント、変数、またはほかの様々なものを記録するように拡張することができ、そうすることが推奨されます。 このアプローチの主なメリットは、記録されたデータに対してSQLクエリを実行できることにあります。  独自のロギングシステムの構築に関する詳細は、こちらの記事をご覧ください。

セッションイベント

イベントクラスは、%CSP.Sessionオブジェクトの寿命中に呼び出されるインターフェースを定義するクラスです。 これを使用するには、 %CSP.SessionEventsをサブクラス化して、実行するメソッドコードを実装する必要があります。 次に、CSPアプリケーション構成内で、イベントクラスを作成したクラスに設定します。

次のコールバックを利用できます。

  • OnApplicationChange
  • OnEndRequest
  • OnEndSession
  • OnLogin
  • OnLogout
  • OnStartRequest
  • OnStartSession
    • OnTimeout

    たとえば上記のカスタムロギングは、これらのメソッドから呼び出すことができます。

    デバイスへの出力

    最も単純なオプションの1つは、すべてのオブジェクトをレスポンスとして出力するCSPユーティリティメソッドです。 これをコードの任意の場所に追加するだけで出力できます。

    set %response.ContentType = "html"
    do ##class(%CSP.Utils).DisplayAllObjects()
    return $$$OK

     

    まとめ

    Webアプリケーションのデバッグにはさまざまなツールを使用することができます。 手元のタスクに最適なものを選択するようにしてください。

    皆さんは、CachéからWebアプリケーションをデバッグする際には、どのようなヒントやトリックを使用していますか?

    0
    0 440
    記事 Toshihiko Minamoto · 8月 25, 2021 7m read

    この記事では、Caché Webアプリケーション(主にREST)のテストとデバッグを外部ツールを用いて行うことについて説明します。 パート2では、Cachéツールの使用について説明します。

    サーバー側のコードを作成したのでクライアントからテストしたい、またはすでにWebアプリケーションが存在するが機能していない― そういったときに使用できるのがデバッグです。 この記事では、最も使いやすいツール(ブラウザ)から最も包括的なツール(パケットアナライザー)までを説明しますが、まずは、最も一般的なエラーとその解決方法について少し説明します。

    エラー

    401 Unauthorized

    これは、本番環境へのデプロイ中に最も頻繁に発生するエラーだと思います。 ローカル開発サーバーには通常、最小限のセキュリティ設定か、バニラを除く通常セキュリティ設定が構成されています。 一方の本番サーバーには、より制限的なスキームが適用されます。 つまり、以下を確認してください。

    • ログイン済みであること
    • アクセスするdatabase/table/procedure/row/columnへのアクセス権があること
    • 許可されていないユーザーがOPTIONSリクエストを実行できること

    404 Not Found

    以下を確認してください。

    • URLが正しいこと
    • 新しいアプリケーションであり、外部Webサーバーを使用している場合は、Webサーバーをリロードしてください。

    アプリケーションエラー

    最も簡単に見つけるには、スタックトレースを使用できます。 解決策は、完全にアプリケーション固有です。

    デバッグツール

    Webブラウザ

    必ず最初に利用できるデバッグツールはWebブラウザです。Chromeが推奨されますが、Firefoxでも十分にデバッグできます。 GETリクエストについては、URLをアドレスバーに入力すればテストできますが、その他すべてのリクエストには、Webアプリケーションかjsコードの記述が必要です。 一般的には以下のように行います。

    • F12キーを押して、デベロッパーツールを開きます。
    • [Network]タブに移動します。
    • [Preserve Log]チェックボックスがオンになっていない場合は、それをオンにします。
    • XHRリクエストのみを表示します。
    • Webアプリケーションでバグのあるアクションを実行します。

    ここから、リクエストを調べて再送信できます。 Firefoxでは、リクエストを繰り返す前にリクエストを編集することも可能です。

    メリット:

    • いつでも利用可能
    • 使いやすい(エンドユーザーは[Network]と[Console]タブのスクリーンショットを送信できます)
    • エンドユーザー環境

    デメリット:

    • 部分送信/破損などのレスポンスを表示しない
    • 大規模なレスポンスでは速度が低下する
    • 大量のレスポンスでは速度が低下する
    • すべて手動で行われる

    RESTクライアント

    RESTクライアントは、Webアプリケーションのテスト向けに特別に作成されたスタンドアロンのWebアプリケーションまたはWebブラウザアドオンです。 私はPostmanを使用していますが、似たようなものはたくさん存在します。 Postmanでのデバッグは次のようになります。

    Postmanはリクエストをコレクションにグループ化して処理します。 リクエストは環境に送信可能です。 環境は変数のコレクションです。 たとえば、私のCACHE@localhost環境ホスト変数は、localhostに設定されており、userは_SYSTEMに設定されています。 リクエストが送信される際は、変数は選択した環境の値に置き換えられてリクエストが送信されます。

    MDX2JSONプロジェクトのサンプルのcollectionenvironmentはこちらにあります。

    メリット:

    • 一度作成すれば、どこででも使用できる
    • リクエストの制御に優れている
    • レスポンスの「Pretty」表示

    デメリット:

    • 連鎖リクエスト(request1に対するレスポンスがrequest2またはrequest2Bを強制できる)のデバッグは依然として手動
    • 部分送信/破損などのレスポンスに失敗することがある

    HTTPデバッグプロキシ

    HTTP(S) トラフィックをログに記録するスタンドアロンアプリケーション。 ログに記録されたリクエストを変更して再送信することができます。 私はCharlesFiddlerを使用しています。

    メリット:

    • 部分送信/破損などのレスポンスを処理する
    • レスポンスの「Pretty」表示
    • HTTPSトラフィックのサポートに優れている(パケットアナライザーより)
    • キャプチャセッションを保存できる

    デメリット:

    • リクエストを送信するために何か(Webアプリケーション/RESTクライアント/JSコード)が必要

    パケットアナライザー

    ネットワークを通過するトラフィックを傍受してログに記録できるコンピュータープログラム。 データストリームがネットワークを流れる際に、スニファーが各パケットをキャプチャし、必要に応じてパケットの生データをデコードします。 これが最も包括的なオプションですが、適切に動作させるには、ある程度のスキルも必要となります。 私はWireSharkを使用しています。 以下にインストールと使用方法を簡単に説明します。

    1. ローカルパケットをキャプチャする場合は、ループバックについて読み、前提条件のソフトウェア(Windows用npcap)をインストールします。
    2. WireSharkをインストールします。
    3. キャプチャフィルタを構成します(たとえば57772のトラフィックのみをキャプチャするファイルはport 57772とします)。
    4. キャプチャを開始します。
    5. 表示フィルタを構成します(たとえば特定のIPへのhttpトラフィックのみを表示する場合は、ip.addr == 1.2.3.4 && httpとします)。

    以下は、ポート57772(キャプチャフィルタ)のhttpトラフィック(表示フィルタ)をキャプチャした例です。

      メリット:

    • 部分送信/破損などのレスポンスを処理する
    • 大量のトラフィックをキャプチャできる
    • 何でもキャプチャできる
    • キャプチャセッションを保存できる

    デメリット:

    • リクエストを送信するために何か(Webアプリケーション/RESTクライアント/JSコード)が必要

    どれを使用するか

    それは目的によって異なります。 まず、リクエストをログに記録する(デバッグプロキシ、パケットアナライザー)かリクエストを生成(ブラウザ、RESTクライアント)することを目的とすることができます。

    REST Web APIを開発している場合は、RESTクライアントを使用するのが、動作をテストする最速の方法です。

    ただし、RESTクライアントからのリクエストが機能してもクライアントWebアプリケーションが機能しない場合は、httpデバッグプロキシとパケットアナライザーが必要となることがあります。

    クライアントがあり、サーバー側APIを開発してそれを操作する場合は、httpデバッグプロキシかパケットアナライザーが必要となります。

    4種類すべてのツールを理解し、使用中のものが作業に不十分となった場合に素早く切り替えられるようにしておくことをお勧めします。

    適切なツールが明確である場合もあります。

    私は最近、人気のあるhttp拡張プロトコル用のサーバー側APIを開発しましたが、その際の要件は次のとおりでした。

    • クライアントがすでに作成済みであり、コードを変更できない。
    • クライアントごとに動作が異なる。
    • httpとhttpsでの動作が異なる。
    • 認証タイプごとに動作が異なる。
    • クライアントごとの1秒当たりのリクエスト数は最大100件である。
    • 全員がRFCを無視する。

    この要件で使用できるソリューションは1つしかありません。パケットアナライザーです。

    または、JS消費用のREST APIを開発しているのであれば、テストに最適なツールはRESTクライアントです。

    Webアプリケーションをデバッグする場合は、Webブラウザでデバッグを開始しましょう。

    パート2では、Webデバッグに関してCaché側でできること(たくさんあります)について説明します。

    皆さんは、クライアント側通信をデバッグする際にどのようなアプローチを採用していますか?

    0
    0 1721
    記事 Toshihiko Minamoto · 4月 5, 2021 11m read

    はじめに

    Webで行われるサーバーとクライアント間のほとんどの通信は、リクエストとレスポンスの構造に基づいており、 クライアントがサーバーにリクエストを送信すると、サーバーがそのリクエストに対するレスポンスを送信します。 WebSocketプロトコルは、サーバーとクライアント間の双方向通信チャンネルを提供するプロトコルで、サーバーがリクエストを受信しなくても、クライアントにメッセージを送信することができます。 WebSocketプロトコルと、InterSystems IRISでの実装についての詳細は、以下のリンクをご覧ください。

    このチュートリアルは、「非同期WebSocket -- クイックチュートリアル」を、Caché 2016.2以上とInterSystems IRIS 2018.1以上向けに更新したものです。

    非同期動作と同期動作

    InterSystems IRISでは、WebSocket接続を同期的または非同期的に実装することができます。 クライアントとサーバー間のWebSocket接続がどのように動作するかは、%CSP.WebSocketクラスの「SharedConnection」プロパティによって決まります。

    • SharedConnection=1: 非同期動作

    • SharedConnection=0: 同期動作

    クライアントとInterSystems IRISインスタンスがホスティングされているサーバーとの間のWebSocket接続には、IRISインスタンスとWebゲートウェイとの間のコネクションが含まれます。 WebSocketの同期動作では、そのコネクションはプライベートチャンネルが使用されますが、 非同期動作では、複数のWebSocketクライアントで、IRISインスタンスとWebゲートウェイとの間のコネクションが共有されます。 WebSocketの非同期動作は、各クライアントを処理するWebゲートウェイとIRISインスタンス間のコネクションを排他的に行う必要がないため、同一のサーバーに多数のクライアントが接続する場合に、そのメリットを発揮します。

    このチュートリアルでは、WebSocketの非同期動作を行います。 したがって、開いているすべてのチャットウィンドウは、Webゲートウェイと、WebSocketサーバークラスをホストするIRISインスタンス間のコネクションを共有することになります。

    チャットアプリケーションの概要

    WebSocketの「hello world」は、ユーザーがそのアプリケーションにログインしているすべてのユーザーにブロードキャストされるメッセージを送信できるチャットアプリケーションです。 このチュートリアルでは、チャットアプリケーションには次のコンポーネントが含まれます。

    • サーバー: %CSP.WebSocketを継承したクラスに実装されています。

    • クライアント: CSPページで実装されています。

    このチャットアプリケーションの実装では、次の内容を実現します。

    • ユーザーは、開いているすべてのチャットウィンドウにメッセージをブロードキャストできます。

    • オンラインユーザーは、開いているすべてのチャットウィンドウの「オンラインユーザー」リストに表示されます。

    • ユーザーは、「alias」キーワードで始まるメッセージを作成することで、ユーザー名を変更できます。このメッセージはブロードキャストされませんが、「オンラインユーザー」リストの内容を更新します。

    • ユーザーがチャットウィンドウを閉じると、「オンラインユーザー」リストから削除されます。

    チャットアプリケーションのソースコードについては、こちらのGitHubリポジトリをご覧ください。

    クライアント

    チャットアプリケーションのクライアント側は、チャットウィンドウのスタイル定義、WebSocket接続の宣言、サーバー間との通信を処理するWebSocketのイベントとメソッド、およびサーバーに送信されるメッセージをパッケージ化し、受信メッセージを処理するヘルパー関数を含むCSPページによって実装されます。

    まず、アプリケーションが、JavaScript WebSocketライブラリを使用して、WebSocket接続をどのように初期化するのか確認しましょう。

        ws = new WebSocket(((window.location.protocol === "https:")? "wss:":"ws:")
                        + "//"+ window.location.host + "/csp/user/Chat.Server.cls");
    

    new は、WebSocketクラスの新しいインスタンスを作成します。 これにより、「wss」(WebSocket通信チャンネルにTLSを使用することを示します)または「ws」プロトコルを使って、サーバーにWebSocket接続が開きます。 サーバーは、Chat.Server クラスを定義するウェブサーバーのポート番号とインスタンスのホスト名で指定されてます(この情報は window.location.host 変数に格納されます)。 サーバークラスの名前(Chat.Server.cls)は、サーバー上のリソースのGETリクエストとして、WebSocketの開始URIに含まれます。

    WebSocket接続の確立に成功すると ws.onopen イベントが発生し、**接続中から接続**に状態が移行します。

        ws.onopen = function(event){
            document.getElementById("headline").innerHTML = "CHAT - CONNECTED";
        };
    

    このイベントにより、チャットウィンドウの見出しが変更され、クライアントとサーバーが接続されていることが示されるようになります。

    メッセージの送信

    ユーザーがメッセージを送信するアクションによって、send 関数がトリガーされます。 この関数は ws.send メソッドを囲むラッパーとして機能し、クライアントのメッセージをWebSocket接続を介してサーバーに送信する仕組みが含まれます。

    function send() {
        var line=$("#inputline").val();
        if (line.substr(0,5)=="alias"){
            alias=line.split(" ")[1];
            if (alias==""){
                alias="default";
            }
            var data = {}
            data.User = alias
            ws.send(JSON.stringify(data));
            } else {
            var msg=btoa(line);
            var data={};
            data.Message=msg;
            data.Author=alias;
            if (ws && msg!="") {
                ws.send(JSON.stringify(data));
            }
        }
        $("#inputline").val("");
    }
    

    send は、サーバーに送信される情報(エイリアスの更新または一般的なメッセージ)をJSONオブジェクトにパッケージ化し、送信される情報の種類に応じたキー/値ペアを定義します。 btoa は、一般メッセージの内容を、base-64でエンコードされたASCII文字列に変換します。

    メッセージの受信

    クライアントがサーバーからメッセージを受信すると、ws.onmessage イベントがトリガーされます。

    ws.onmessage = function(event) {
        var d=JSON.parse(event.data);
        if (d.Type=="Chat") {
            $("#chat").append(wrapmessage(d));
                $("#chatdiv").animate({ scrollTop: $('#chatdiv').prop("scrollHeight")}, 1000);
        } else if(d.Type=="userlist") {
            var ul = document.getElementById("userlist");
            while(ul.firstChild){ul.removeChild(ul.firstChild)};
            $("#userlist").append(wrapuser(d.Users));
        } else if(d.Type=="Status"){
            document.getElementById("headline").innerHTML = "CHAT - connected - "+d.WSID;
        }
    };
    

    クライアントが受信するメッセージの種類(「チャット」、「ユーザーリスト」、「ステータス」)に応じて、onmessage イベントによって、wrapmessage または wrapuser が呼び出され、チャットウィンドウの該当するセクションに受信データを取り込みます。 受信メッセージがステータス更新であれば、チャットウィンドウのステータスの見出しがWebSocket IDで更新されます。このIDは、チャットウィンドウに関連付けられた双方向WebSocket接続を識別するものです。

    その他のクライアントコンポーネント

    クライアントとサーバー間の通信でエラーが発生すると、WebSocketの onerror メソッドがトリガーされ、エラーの発生を通知するアラートの発行とページのステータス見出しの更新が行われます。

    ws.onerror = function(event) {
        document.GetElementById("headline").innerHTML = "CHAT - error";
        alert("Received error"); 
    };
    

    クライアントとサーバー間のWebSocket接続が閉じると、onclose メソッドがトリガーされ、ステータスの見出しが更新されます。

    ws.onclose = function(event) {
        ws = null;
        document.getElementById("headline").innerHTML = "CHAT - disconnected";
    }
    

    サーバー

    チャットアプリケーションのサーバー側は、%CSP.WebSocket を拡張した Chat.Server クラスで実装されます。 サーバークラスは、%CSP.WebSocketのプロパティとメソッドを継承しています。これについては以下の方で説明します。 Chat.Server は、クライアントからのメッセージを処理するカスタムメソッドと、クライアントにメッセージをブロードキャストするカスタムメソッドも実装します。

    サーバーの起動前処理

    OnPreServer() は、WebSocketサーバーが作成されて %CSP.WebSocket クラスから継承される前に実行されます。

    Method OnPreServer() As %Status
    {
        set ..SharedConnection=1
        if (..WebSocketID '= ""){ 
            set ^Chat.WebSocketConnections(..WebSocketID)=""
        } else {
            set ^Chat.Errors($INCREMENT(^Chat.Errors),"no websocketid defined")=$HOROLOG 
        }
        Quit $$$OK
    }
    

    このメソッドは、SharedConnection クラスのパラメーターを1に設定し、WebSocket接続が非同期であり、InterSystems IRISインスタンスとWebゲートウェイ間の接続を定義する複数のプロセスでサポートされることを示します。 SharedConnection パラメーターは、OnPreServer() でしか変更できません。 また、OnPreServer() は、クライアントに関連付けられているWebSocket IDを ^Chat.WebSocketConnections グローバルに格納します。

    Serverメソッド

    サーバーが実行するロジックの本文は、Server() メソッドに含まれます。

    Method Server() As %Status
    {
        do ..StatusUpdate(..WebSocketID)
        for {       
            set data=..Read(.size,.sc,1) 
            if ($$$ISERR(sc)){
                if ($$$GETERRORCODE(sc)=$$$CSPWebSocketTimeout) {
                    //$$$DEBUG("no data")
                }
                if ($$$GETERRORCODE(sc)=$$$CSPWebSocketClosed){
                    kill ^Chat.WebSocketConnections(..WebSocketID)
                    do ..RemoveUser($g(^Chat.Users(..WebSocketID))) 
                    kill ^Chat.Users(..WebSocketID)
                    quit  // Client closed WebSocket
                }
            } else{
                if data["User"{
                    do ..AddUser(data,..WebSocketID)
                } else {
                    set mid=$INCREMENT(^Chat.Messages)
                    set ^Chat.Messages(mid)=data
                    do ..ProcessMessage(mid)
                }
            }
        }
        Quit $$$OK
    }
    

    このメソッドは、クライアントからの受信メッセージを読み取り(%CSP.WebSockets クラスの Read メソッド)、取得したJSONオブジェクトを ^Chat.Messages グローバルに追加し、接続されているその他すべてのチャットクライアントにメッセージを転送するための ProcessMessage() を呼び出します。 ユーザーがチャットウィンドウを閉じると(つまり、サーバーへのWebSocket接続が終了すると)、Server() メソッドの Read の呼び出しによって、それが評価するエラーコードが $$$CSPWebSocketClosed マクロに返され、メソッドはそれに応じて閉鎖の処理に進みます。

    メッセージの処理と配信

    ProcessMessage() は、受信チャットメッセージにメタデータを追加して SendData() を呼び出し、メッセージをパラメーターとして渡します。

    ClassMethod ProcessMessage(mid As %String)
    {
        set msg = ##class(%DynamicObject).%FromJSON($GET(^Chat.Messages(mid)))
        set msg.Type="Chat"
        set msg.Sent=$ZDATETIME($HOROLOG,3)
        do ..SendData(msg)
    }
    

    ProcessMessage() は、^Chat.Messages グローバルからJSONの書式付きメッセージを取得し、%DynamicObject クラスの %FromJSON メソッドを使って、そのメッセージをInterSystems IRIS オブジェクトに変換します。 この変換により、メッセージが接続されているすべてのチャットクライアントに転送される前に、データを編集しやすくなります。 Type 属性を値「Chat」で追加し、クライアントはこれを使用して、受信メッセージの処理方法を決定しアンス。 SendData() は、接続されているその他すべてのチャットクライアントにメッセージを送信します。

    ClassMethod SendData(data As %DynamicObject)
    {
        set c = ""
        for {
            set c = $order(^Chat.WebSocketConnections(c))
            if c="" Quit
            set ws = ..%New()
            set sc = ws.OpenServer(c)
            if $$$ISERR(sc) { do ..HandleError(c,"open") } 
            set sc = ws.Write(data.%ToJSON())
            if $$$ISERR(sc) { do ..HandleError(c,"write") }
        }
    }
    

    SendData() は、InterSystems IRISオブジェクトをJSON文字列に変換し直し(data.%ToJSON())、すべてのチャットクライアントにメッセージをプッシュします。 SendData() は、クライアントとサーバー間のそれぞれの接続に関連付けられたWebSocket IDを ^Chat.WebSocketConnections グローバルから取得し、そのIDを使って、%CSP.WebSocket クラスの OpenServer メソッドで、WebSocket接続を開きます。 OpenServer メソッドを使ってこの処理を実行できるのは、WebSocket接続が非同期であるからです。IRIS-Webゲートウェイの既存のプールからプロセスをプルし、特定のチャットクライアントへのサーバー接続を識別するWebSocket IDを割り当てています。 最後に、 Write()%CSP.WebSocket メソッドによって、JSON 文字列に変換されたメッセージがクライアントにプッシュされます。

    まとめ

    このチャットアプリケーションでは、クライアントとInterSystems IRISをホストするサーバー間にWebScocket接続を確立する方法が示されています。 InterSystems IRISにおけるプロコルとその実装については、引き続き、「はじめに」セクションに記載されているリンクをご覧ください。

    0
    0 3627
    記事 Tomoko Furuzono · 11月 24, 2020 4m read

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


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

    0
    0 347
    記事 Henrique Dias · 10月 8, 2020 2m read

    npm-iris とは何ですか? 

    N.P.Mは "No Project Mess "の略です。

    N.P.M.は、InterSystems IRISとBootstrap 4を使用したプロジェクト&タスク管理アプリです。

    No Project Messは、シンプルで直感的なプロジェクトとタスクの管理ソフトウェアで、開発者や中小企業が日々の複雑な問題を軽減できるように作成されています。 
    スプレッドシート、カンバン、カレンダー、ガントチャートなど、タスクのためのさまざまなビューを提供しています。

    2
    0 175
    記事 Shintaro Kaminaka · 8月 12, 2020 6m read

    最初の記事では、RESTForms(永続クラス用のREST API)について説明をしました。 基本的な機能についてはすでに説明しましたが、ここではクエリ機能を中心とする高度な機能について説明します。

    * 基本クエリ * クエリ引数 * カスタムクエリ ### クエリ

    クエリを使用すると、任意の条件に基づいてデータの一部を取得できます。 RESTFormsには、2種類のクエリがあります。

    * 基本クエリは一度定義すればすべてのRESTFormsクラスに対して機能します。異なっているのはフィールドリストのみです。 * カスタムクエリはそれが指定され、使用できるクラスに対してのみ機能しますが、開発者はクエリの本文に完全にアクセスできます。 ### 基本クエリ

    一度定義すると、すべてのクラスか一部のクラスですぐに使用できます。 基本クエリはシステムによって定義されているものもありますが、開発者が追加することもできます。これらのクエリはすべて、SELECTのフィールドリストのみを定義します。 その他すべて(絞り込み、ページネーションなど)はRESTFormsによって行われます。 form/objects/:class/:query を呼び出すと、単純なクエリを実行できます。 2番目の :query パラメーターはクエリ名(クエリの SELECT と FROM の間の内容)を定義します。 デフォルトのクエリタイプは次のとおりです。

    クエリ説明
    allすべての情報
    infodisplayName と id
    infoclassdisplayName、id、class
    count行数

    例えば、Form.Test.Personオブジェクトに関する基本的な情報を取得するには、infoclassクエリを実行できます。 form/objects/Form.Test.Person/infoclass

     {"children": [
        {"_id":"1", "displayName":"Alice",   "_class":"Form.Test.Person"},
        {"_id":"2", "displayName":"Charlie", "_class":"Form.Test.Person"},
        {"_id":"3", "displayName":"William", "_class":"Form.Test.Person"}
    ]}

    RESTFormsは次の場所で myq という名前のクエリを探します(最初にヒットするまで)。

    1. フォームクラス内のqueryMYQクラスメソッド
    2. クエリクラス内のMYQパラメーター
    3. クエリクラス内のqueryMYQクラスメソッド
    4. Form.REST.Objectsクラス内のMYQパラメーター
    5. Form.REST.Objectsクラス内のqueryMYQクラスメソッド

    独自のクエリクラスを定義できます(上記リストの項目2、3用)。このクエリクラスは、すべてのクラスで使用可能なクエリの定義を保持する特別なクラスです。 そのクラスで myq という名前の独自クエリを定義する手順は以下のとおりです。

    1. (1回だけ)YourClassName クラスを定義します。
    2. そのクラスで MYQ パラメーターか queryMYQ クラスメソッドを定義します。 パラメーターはメソッドよりも優先されます。
    3. メソッドまたはパラメーターは、SQLクエリのSELECTとFROMの間の部分を返す必要があります。
    4. (1回だけ)ターミナルで以下を実行します。 Do ##class(For.Settings).setSetting("queryclass", YourClassName)

    メソッドの署名は以下のとおりです。

    ClassMethod queryMYQ(class As %String) As %String

    クラス固有のクエリを定義することもできます。 myq という名前の独自クラスクエリを定義する手順は以下のとおりです。

    1. フォームクラスで queryMYQ クラスメソッドを定義します。
    2. メソッドの署名は以下のとおりです。ClassMethod queryMYQ() As %String
    3. メソッドは、SQLクエリのSELECTとFROMの間の部分を返す必要があります。

    URL 引数

    フィルターやその他のパラメーターをURLで指定できます。 すべての引数は省略可能です。
    引数サンプル値説明
    size2ページサイズ
    page1ページ番号
    filter値+contains+WWHERE句
    orderby値+descORDER BY句
    collationUPPERCOLLATION句
    nocount1レコード数を削除(クエリを高速化します)

    これらの引数に関するいくつかの情報を次に示します。

    ORDER BY句

    結果の順序を変更します。 値は、カラム名またはカラム名+desc です。 カラム名は、SQLテーブルのカラム名またはカラム番号です。

    WHERE句

    絞り込み条件の書式は、カラム名+条件+値です。 カラム名+条件+値+カラム名2+条件2+値2のように、複数の条件を指定できます。 矢印構文とシリアルオブジェクトもサポートされています: Column_ColumnField+条件+値 Valueに空白が含まれている場合、サーバーに送信する前にタブに置き換えます。

    URLSQL
    neq!=
    eq=
    gte>=
    gt>
    lte<=
    lt<
    startswith%STARTSWITH
    contains[
    doesnotcontain'[
    inIN
    likeLIKE

    リクエストの例:

    form/objects/Form.Test.Simple/info?size=2&page=1&orderby=text
    form/objects/Form.Test.Simple/all?orderby=text+desc
    form/objects/Form.Test.Simple/all?filter=text+eq+Hello
    form/objects/Form.Test.Person/infoclass?filter=company_name+contains+a
    form/objects/Form.Test.Simple/all?filter=text+in+A9044~B5920

    SQLアクセスには、ユーザーに適切なSQL権限(フォームテーブルに対するSELECT)を付与する必要があることに注意してください。

    COLLATION句

    書式は、collation=UPPER または collation=EXACT です。 指定した照合順序をWHERE句で強制します。 省略した場合、デフォルトの照合順序が使用されます。

    ページネーション

    ページネーションは、デフォルトで1ページあたり25レコードになります。 ページサイズと現在のページを変更するには、size 引数と page 引数を(1を基準として)指定します。

    カスタムクエリ

    form/objects/:class/custom/:query を呼び出すと、カスタムクエリを実行できます。 カスタムクエリを使用すると、開発者はクエリの内容全体を決めることができます。 size および page 以外のURLパラメーターは指定できません。 メソッドは他のすべてのURLパラメーターを解析する必要があります(またはForm.JSON.SQLからデフォルトのパーサーを呼び出す必要があります)。 myq という名前のカスタムクエリを定義する手順は以下のとおりです。

    1. フォームクラスで customqueryMYQ クラスメソッドを定義します。
    2. メソッドの署名は以下のとおりです。ClassMethod customqueryMYQ() As %String
    3. メソッドは有効なSQLクエリを返す必要があります。

    デモ

    [現在デモ環境はお試しいただくことができません。] こちらでRESTFormsをオンラインで試すことができます(ユーザー名:Demo、パスワード:Demo)。 また、RESTFormsUIアプリケーション(RESTFormsデータエディタ)もあります。こちらをご確認ください(ユーザー名:Demo、パスワード:Demo)。 クラスリストのスクリーンショットを以下に掲載しています。

    まとめ

    RESTFormsは、幅広くカスタマイズ可能なクエリ機能を提供します。

    次の内容

    次の記事では、いくつかの高度な機能について説明します。

    • メタデータの解釈
    • セキュリティと権限
    • オブジェクト名

    リンク

  • RESTForms UI GitHubリポジトリ
  • 0
    0 144
    記事 Shintaro Kaminaka · 7月 30, 2020 11m read

    この記事では、RESTFormsプロジェクト(モダンなWebアプリケーション用の汎用REST APIバックエンド)を紹介します。

    プロジェクトの背後にあるアイデアは単純です。私はいくつかのREST APIを書いた後、REST APIが一般的に次の2つの部分で構成されていることに気付きました。

    • 永続クラスの操作
    • カスタムビジネスロジック

    また、独自のカスタムビジネスロジックを書く必要はありますが、RESTFormsには永続クラスの操作に関連するすべての機能を提供しています。

    使用例

    • Cachéにすでにデータモデルがあり、REST API形式で情報の一部(またはすべて)を公開したい
    • 新しいCachéアプリケーションを開発しており、REST APIを提供したい

    クライアントサイド

    このプロジェクトはWebアプリケーションのバックエンドとして開発されているため、JSだけで事足ります。 形式の変換は必要ありません。

    補足:CRUD

    オブジェクトまたはコレクションに対し、次の4つの操作を実行できます。

    • Create(作成)
    • Read(読み込み)
    • Update(更新)
    • Delete(削除)

    機能

    RESTFormsを使用して以下を実行できます。

    • 公開されたクラスに対するCRUD - クラスのメタデータを取得し、クラスのプロパティを作成 / 更新 / 削除できます。
    • オブジェクトに対するCRUD - オブジェクトを取得 / 作成 / 更新 / 削除できます。
    • オブジェクトコレクションに対するRead(SQL経由) - SQLインジェクションから保護します。
    • 自己検出 – 最初に使用可能なクラスのリストを取得し、その後でクラスのメタデータを取得し、そのメタデータを基にしてオブジェクトに対するCRUDを実行できます。

    パス

    以下の表には、主なパスとRESTFormsを使用して実行できる操作を掲載しています。

    <td>
      説明
    </td>
    
    <td>
      利用可能なすべてのクラスを一覧表示します
    </td>
    
    <td>
      すべてのクラスのメタデータを取得します
    </td>
    
    <td>
      クラスのメタデータ
    </td>
    
    <td>
      プロパティをクラスに追加します
    </td>
    
    <td>
      クラスのプロパティを変更します
    </td>
    
    <td>
      クラスのプロパティを削除します
    </td>
    
    <td>
      オブジェクトを取得します
    </td>
    
    <td>
      オブジェクトの1つのプロパティを取得します
    </td>
    
    <td>
      オブジェクトを作成します
    </td>
    
    <td>
      動的オブジェクトからオブジェクトを更新します
    </td>
    
    <td>
      オブジェクトからオブジェクトを更新します
    </td>
    
    <td>
      オブジェクトを削除します
    </td>
    
    <td>
      (SQL)クエリでクラスのオブジェクトを取得します
    </td>
    
    <td>
      (SQL)カスタムクエリでクラスのオブジェクトを取得します
    </td>
    
    URL
    info
    info/all
    info/:class
    field/:class
    field/:class
    field/:class/:property
    object/:class/:id
    object/:class/:id/:property
    object/:class
    object/:class/:id
    object/:class
    object/:class/:id
    objects/:class/:query
    objects/:class/custom/:query

    **RESTFormsを使い始めるには?**
    1. GitHubからプロジェクトをインポートします(お勧めの方法は独自リポジトリにサブモジュールとして追加する方法ですが、単にリリースをダウンロードしても良いです)。
    2. RESTFormsを介して公開したい各クラスについて以下を実施します。
    • アダプタクラスから継承する
    • 権限を指定します(一部のクラスを読み取り専用として公開する場合などに実施)。
    • オブジェクトの表示値として使用されるプロパティを指定します。
    • 表示したいプロパティの表示名を指定します。

    セットアップ

    1. [リリースページ](https://github.com/intersystems-ru/RESTForms/releases/tag/v1.0)で最新リリースである20161.xml( Caché 2016.1用)または201162.xml(Caché 2016.2以降用)をダウンロードして任意のネームスペースにインポートします。
    2. 新しいWebアプリケーション /forms をDispatchクラス Form.REST.Main を使用して作成します。
    3. http://localhost:57772/forms/test?Debug をブラウザで開き、インストールを検証します({"Status": "OK"} が出力され、場合によってはパスワードの入力が求められます)。
    4. テストデータが必要な場合は、次を呼び出します:

    do ##class(Form.Util.Init).populateTestForms()

    最初に、利用可能なクラスを知る必要があります。 この情報を取得するには、次を呼び出します。

    http://localhost:57772/forms/form/info
    

    次のような応答が返されます。

    [
       { "name":"Company",     "class":"Form.Test.Company" },
       { "name":"Person",      "class":"Form.Test.Person"  },
       { "name":"Simple form", "class":"Form.Test.Simple"  }
    ]

    現在3つのサンプルクラス(RESTFormで提供)があります。Person(Form.Test.Personクラス)のメタデータを見てみましょう。 この情報を取得するには、次を呼び出します。

    http://localhost:57772/forms/form/info/Form.Test.Person
    

    次のように、クラスのメタデータが応答として返されます。

    {  
       "name":"Person",
       "class":"Form.Test.Person",
       "displayProperty":"name",
       "objpermissions":"CRUD",
       "fields":[  
          { "name":"name",     "type":"%Library.String",    "collection":"", "displayName":"Name",          "required":0, "category":"datatype" },
          { "name":"dob",      "type":"%Library.Date",      "collection":"", "displayName":"Date of Birth", "required":0, "category":"datatype" },
          { "name":"ts",       "type":"%Library.TimeStamp", "collection":"", "displayName":"Timestamp",     "required":0, "category":"datatype" },
          { "name":"num",      "type":"%Library.Numeric",   "collection":"", "displayName":"Number",        "required":0, "category":"datatype" },
          { "name":"аge",      "type":"%Library.Integer",   "collection":"", "displayName":"Age",           "required":0, "category":"datatype" },
          { "name":"relative", "type":"Form.Test.Person",   "collection":"", "displayName":"Relative",      "required":0, "category":"form"     },
          { "name":"Home",     "type":"Form.Test.Address",  "collection":"", "displayName":"House",         "required":0, "category":"serial"   },
          { "name":"company",  "type":"Form.Test.Company",  "collection":"", "displayName":"Company",       "required":0, "category":"form"     }
       ]
    }

    これらの情報は次のような意味を持ちます。

    クラスのメタデータ:

    • name - クラスの表示名。
    • class - 基本となる永続クラス。
    • displayProperty - オブジェクトを表示するときに使用するオブジェクトのプロパティ。
    • objpermissions - ユーザーがオブジェクトを使用して実行できる操作。 この例では、ユーザーは新しいオブジェクトを作成し、既存のオブジェクトを変更し、既存のオブジェクトを削除し、次を取得できます。

    プロパティのメタデータ:

    • name - プロパティ名 - クラスの定義と同じです。
  • type - プロパティのクラス。
  • * コレクション - リスト/配列のコレクションです。 * displayName - 表示プロパティ名。 * required - このプロパティが必須であるかどうか。 * category - プロパティのタイプクラスのカテゴリ。 RESTForms対応のすべてのクラスが「form」として表示されることを除き、通常のCachéクラスのカテゴリに従います。

    クラス定義では次のようになります。

    /// テストフォーム: Person
    Class Form.Test.Person Extends (%Persistent, Form.Adaptor, %Populate)
    {
    
    /// フォーム名。グローバルキーではないため、何でもかまいません。
    /// クラスをフォームとして持たないようにするには(ここのように)空の文字列に設定します。 
    Parameter FORMNAME = "Person";
    
    /// デフォルトの権限
    /// このフォームのオブジェクトは、作成、読み取り、更新、削除できます。
    /// すべてのユーザーの権限を変更するには、このパラメーターを再定義します。
    /// このクラスのcheckPermissionメソッドを再定義します(Form.Securityを参照してください)。
    /// ユーザーやロールなどに基づいて独自のセキュリティを追加します。
    Parameter OBJPERMISSIONS As %String = "CRUD";
    
    /// オブジェクトの基本情報に使用されるプロパティ
    /// デフォルトでは、getObjectDisplayNameメソッドはここから値を取得します。
    Parameter DISPLAYPROPERTY As %String = "name";
    
    /// このパラメーターの値をSQLでORDER BY句の値として使用します。 
    Parameter FORMORDERBY As %String = "dob";
    
    /// Personの名前。
    Property name As %String(COLLATION = "TRUNCATE(250)", DISPLAYNAME = "Name", MAXLEN = 2000);
    
    /// Personの生年月日。
    Property dob As %Date(DISPLAYNAME = "Date of Birth", POPSPEC = "Date()");
    
    Property ts As %TimeStamp(DISPLAYNAME = "Timestamp") [ InitialExpression = {$ZDATETIME($ZTIMESTAMP, 3, 1, 3)} ];
    
    Property num As %Numeric(DISPLAYNAME = "Number") [ InitialExpression = "2.15" ];
    
    /// Personの年齢。<br>
    /// これは、 <property>DOB</property> から派生した値を持つ計算されたフィールドです。
    Property аge As %Integer(DISPLAYNAME = "Age") [ Calculated, SqlComputeCode = { set {*}=##class(Form.Test.Person).currentAge({dob})}, SqlComputed, SqlComputeOnChange = dob ];
    
    /// このクラスメソッドは、誕生日 <var>date</var> が与えられた場合に現在の年齢を計算します。
    ClassMethod currentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
    {
    $Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
    }
    
    /// Personの配偶者。
    /// これは別の永続オブジェクトへの参照です。
    Property relative As Form.Test.Person(DISPLAYNAME = "Relative");
    
    /// Personの自宅住所。 埋め込みオブジェクトを使用します。
    Property Home As Form.Test.Address(DISPLAYNAME = "House");
    
    /// このPersonが働いている会社。
    Relationship company As Form.Test.Company(DISPLAYNAME = "Company") [ Cardinality = one, Inverse = employees ];
    }

    クラスでRESTFormsを有効にする

    そして、このクラスでRESTFormsを有効にするため、通常の永続クラスから始めて次のことを行いました。

    1. Form.Adaptor から拡張しました。
    2. 値を含むパラメーター FORMNAME(クラス名)を追加しました。
    3. OBJPERMISSIONS パラメーター(すべての権限のCRUD)を追加しました。
    4. DISPLAYPROPERTY パラメーター(オブジェクト名の表示に使用されるプロパティ名)を追加しました。
    5. FORMORDERBY パラメーター(RESTFormsを使用するクエリでソートするデフォルトのプロパティ)を追加しました。
    6. メタデータで確認したいプロパティごとに DISPLAYNAME プロパティのパラメーターを追加しました。

    以上です。 コンパイル後、RESTFormsを含むクラスを使用できるようになります。

    いくつかのテストデータを生成しましたので(インストールのステップ4を参照)、IDが1のPersonを取得してみましょう。 オブジェクトを取得するには、次を呼び出します。

    http://localhost:57772/forms/form/object/Form.Test.Person/1

    その応答は以下のとおりです(生成されるデータは異なる場合があります)。

    {
       "_class":"Form.Test.Person",
       "_id":1,
       "name":"Klingman,Rhonda H.",
       "dob":"1996-10-18",
       "ts":"2016-09-20T10:51:31.375Z",
       "num":2.15,
       "аge":20,
       "relative":null,
       "Home":{
          "_class":"Form.Test.Address",
          "House":430,
          "Street":"5337 Second Place",
          "City":"Jackson"
       },
       "company":{
          "_class":"Form.Test.Company",
          "_id":60,
          "name":"XenaSys.com",
          "employees":[
             null
          ]
       }
    }

    オブジェクト(具体的にはnumプロパティ)を変更するには、次を呼び出します。

    PUT http://localhost:57772/forms/form/object/Form.Test.Person

    このボディを使用します。

    {
       "_class":"Form.Test.Person",
       "_id":1,
       "num":3.15
    }

    速度を上げるには、_class_id、および変更対象のプロパティのみをリクエストのボディに含める必要があります。

    では、新しいオブジェクトを作成しましょう。 以下を呼び出します。

    POST http://localhost:57772/forms/form/object/Form.Test.Person

    このボディを使用します。

    {
       "_class":"Form.Test.Person",
        "name":"Test person",
        "dob":"2000-01-18",
        "ts":"2016-09-20T10:51:31.375Z",
        "num":2.15,
        "company":{ "_class":"Form.Test.Company", "_id":1 }
    }

    オブジェクトの作成が成功した場合、RESTFormsは以下のようにIDを返します。

    {"Id": "101"}

    成功しなかった場合、エラーがJSON形式で返されます。 すべての永続オブジェクトのプロパティは、 _class および _id プロパティによってのみ参照する必要があります。

    そして最後に、新しいオブジェクトを削除しましょう。 以下を呼び出します。

    DELETE http://localhost:57772/forms/form/object/Form.Test.Person/101

    これがForm.Test.Personクラスに対する完全なCRUDです。

    デモ

    [現在デモ環境はお試しいただくことができません。] こちらでRESTFormsをオンラインで試すことができます(ユーザー名:Demo、パスワード:Demo)。

    また、RESTFormsUIアプリケーション(RESTFormsデータエディタ)もあります。こちらをご確認ください(ユーザー名:Demo、パスワード:Demo)。 クラスリストのスクリーンショットを以下に掲載しています。

    まとめ

    RESTFormsは永続クラスに関する、REST APIから要求されるほとんどの機能を提供します。

    次の内容

    この記事では、RESTFormsの機能について説明しました。 次回の記事では、いくつかの高度な機能(クライアントからSQLインジェクションのリスクを冒さずにデータの一部を安全に取得できるクエリなど)についてお話ししたいと思います。 この記事のパート2でクエリに関する情報をお読みください

    RESTFormsUI(RESTFormsデータエディタ)もあります。

    リンク

    0
    0 344