キューバホームアカウンティング





この記事の目的は、小さな便利なアプリケーションを作成する例を使用して、 CUBAプラットフォームの機能について説明することです。

CUBAはJavaでのビジネスアプリケーションの高速開発を目的としていますが、Habré に関するいくつかの記事についてはすでに書いています。



通常、プラットフォームは、実在するが大きすぎる閉鎖的な情報システム、または「Hello World」スタイルのアプリケーション、またはWebサイトの「Libraries」などの人工的な例を構築します。 そのため、しばらく前に、私は2羽の鳥を一度に1石で殺そうとすることにしました-私たちのプラットフォームの使用例として、自分にとって便利なアプリケーションを書いてパブリックドメインに置きます。





結果として何が起こったのか



要するに、アプリケーションは2つの主なタスクを解決します。

  1. どの時点でも、現金、カード、預金、債務など、あらゆる種類の現金の現在の残高が表示されます。
  2. 収入と支出のカテゴリごとにレポートを生成し、特定の期間にお金が何に使われたのか、どこから来たのかを調べることができます。


もう少し詳しく:



いくつかのスクリーンショット



メインUI:操作のリスト






メインUI:収入/費用のカテゴリ別レポート






レスポンシブUI:操作のリスト






レスポンシブUI:現在のバランス








実行方法



プロジェクトのソースコードは次のとおりです。github.com / knstvk / akkount (KK-これらは私のイニシャルであり、私の頭に浮かんだものはありません)。

プラットフォーム自体は無料ではありませんが、無料ライセンスでの5つの同時接続は家庭での使用には十分であるため、誰かがそれを使用したい場合はお願いします。



JDK 7+とset JAVA_HOME環境変数のみが必要です。 ビルドするには、プロジェクトのルートでコマンドプロンプトを開き、実行します

gradlew setupTomcat deploy







Gradleがダウンロードされ、 インターネットプラットフォームと他のライブラリがダウンロードされ、build / tomcatサブディレクトリにアプリケーションがアセンブルされます。 アセンブリプロセス中に、CUBAプラットフォームのライセンス契約に同意するよう求められます。

その後、HSQLサーバーを起動し、プロジェクトデータサブディレクトリにデータベースを作成する必要があります。

gradlew startDb





gradlew createDb







Gradleコマンドを使用してTomcatを起動できます。

gradlew start





またはstartup.*



build/tomcat/bin



サブディレクトリ内のスクリプト。

アプリケーションのメインWebインターフェースは localhost:8080/app



localhost:8080/app



、レスポンシブUI-オン localhost:8080/app-portal



localhost:8080/app-portal



ユーザーはadmin、パスワードはadminです。



データベースは最初は空ですが、テストデータを入力するためのジェネレーターがあります。 管理メニュー-> JMXコンソール-> app-core.akkount-> app-core.akkount:type = SampleDataGeneratorから利用できます。 以下にgenerateSampleData()



メソッドを示します。このメソッドは、整数を入力として受け取ります。これは、操作を作成する必要がある現在の日付からの日数です。 たとえば、200と入力し、[実行]をクリックします。 操作が完了するのを待ってからログアウトし(右上隅のアイコン)、再度ログインします。 私のスクリーンショットとほぼ同じものが表示されます。



内部の見方



アプリケーションを調査および改良するには、CUBA Studio、IntelliJ IDEA、およびCUBAプラグインをダウンロードしてインストールすることをお勧めします



さらに、スタジオでどのように、何が行われているかについては触れません。 すべてが視覚的であり、コンテキストヘルプがあり、プラットフォーム上にビデオ資料ドキュメントがあります。 HSQLデータベースを使用する唯一のニュアンスについて説明します。HSQLDBを使用してプロジェクトを開くと、スタジオは独自のサーバーをポート9001で起動し、データベースを~/.haulmont/studio/hsqldb



保存します。 つまり、Gradleコマンドを使用してStudioとは別にHSQLサーバーを起動した場合は、停止する必要があります。 必要に応じて、データベースファイルをdata/akk



から~/.haulmont/studio/hsqldb/akk



簡単に転送できます。



一般に、アプリケーションは、より深刻なデータベース(PostgreSQL、Microsoft SQL Server、またはOracle)でも実行できます。 これを行うには、Studioでプロジェクトプロパティで必要なデータベースのタイプを選択し、[ エンティティ]-> [DBスクリプトの生成 ]を実行し、メインメニューの[ 実行 ] -> [データベースの作成 ]をクリックします



この記事の主な目的は、Studioのインターフェースには表示されず、何を探すべきか事前にわからない場合はドキュメントで見つけるのが難しいプラットフォームでの開発手法を示すことです。 したがって、プロジェクトの説明は断片的であり、非自明で非標準的なものに重点が置かれます。



データモデル







エンティティクラスはglobal



モジュールにあり、中間層とWebクライアントの両方からアクセスできます。



これらは基本的に通常のJPAエンティティであり、適切に注釈が付けられ、 persistence.xml



登録されます。 それらのほとんどには、「インスタンス名」を設定するCUBA固有のアノテーション@NamePattern



もあります。これは、 toString()



ようなUIで特定のエンティティインスタンスを表示する方法です。 そのような注釈が指定されていない場合、インスタンス名としてtoString()



使用され、クラス名とオブジェクト識別子が返されます。 別の特定のアノテーション@Listeners



、オブジェクトを作成/変更するためのリスナーオブジェクトのクラスを定義します。 エンティティリスナーについては、以下で詳しく説明します。



JPAエンティティに加えて、プロジェクトには非永続的なCategoryAmount



エンティティがあります。 非永続エンティティのインスタンスはデータベースに保存されませんが、アプリケーションレイヤー間でデータを転送し、標準UIコンポーネントで表示するためにのみ使用されます。 この場合、そのようなエンティティを使用して、 CategoryAmount



ごとにレポートを生成します:中間層では、データが抽出され、 CategoryAmount



インスタンスが作成および入力され、Webクライアントでは、これらのインスタンスがデータソース(データソース)に配置され、テーブルに表示されます。 標準のTable



コンポーネントは、エンティティの起源については何も知りません-それらにとって、これらはアプリケーションのメタデータで既知のオブジェクトにすぎません。 また、メタデータに非永続エンティティを含めるには、クラスに@MetaClass



アノテーションを追加し、 @MetaClass



アノテーションを追加して、 metadata.xml



ファイルにクラスを登録する必要があります。 もちろん、永続エンティティもメタデータで説明されています。このため、アプリケーションの開始時にメタデータローダーがpersistence.xml



ファイルも解析します。



Enumは、 OperationType



などのエンティティの隣にもあります。 エンティティ属性のデータモデルで使用される列挙体はあまり一般的ではありません。それらはEnumClass



インターフェイスを実装し、 id



フィールドを持っています。 したがって、データベースに格納されている値はJava値から分離されています。 これにより、アプリケーションコードの任意のリファクタリング中に、運用データベースのデータとの互換性を確保できます。



エンティティパッケージのmessages.properties



およびmessages_ru.properties



ファイルには、エンティティのローカライズされた名前とその属性が含まれています。 これらの名前は、ビジュアルコンポーネントがそれらのレベルで再定義しない場合にUIで使用されます。 メッセージファイルは、一般的なUTF-8エンコードのキーと値のセットです。 特定のロケールのメッセージの検索は、 PropertyResourceBundle



ルールに似ています-最初に、ロケールに対応するサフィックスを持つファイルで、見つからない場合、サフィックスのないファイルでキーが検索されます。



モデルの本質を考慮してください。





エンティティリスナー







JPAを使用している場合は、おそらくエンティティリスナーも使用したでしょう。 これは、データベースのエンティティへの変更を保存するときにアクションを実行するための便利なメカニズムです。 最も重要なことは、リスナーによって行われたすべての変更は、データベーストリガーと同様に同じトランザクションで行われます。 したがって、リスナーのデータモデルの一貫性を維持するためのロジックを編成すると便利です。



CUBAのエンティティリスナーは、実装がJPAとわずかに異なります。 リスナークラスは、1つ以上の特別なインターフェイス( BeforeInsertEntityListener



BeforeUpdateEntityListener



など)を実装する必要があります。 リスナーは、文字列の配列にクラス名をリストすること@Listeners



@Listeners



アノテーションのエンティティクラスに登録されます。 エンティティは中間層とクライアントの両方にアクセス可能なグローバルオブジェクトであり、リスナーはクライアントにアクセスできない中間層のみのオブジェクトであるため、リスナークラスのリテラルをエンティティクラスで直接使用することはできません。 リスナーは、 EntityManager



およびデータベースを操作する他の手段にアクセスする必要があるため、中間層にのみ存在します。



このアプリケーションでは、エンティティリスナーは2つの機能を実行します。1つ目は非正規化フィールドを更新し、2つ目は月の初めに口座残高を再計算することです。

最初のタスクは簡単ですonBeforeInsert()



メソッドのAccountEntityListener AccountEntityListener



onBeforeInsert()



は通貨コードの値を更新します。 これを行うには、 Currency



関連インスタンスを参照するだけで十分です。

2番目のタスクは、基本的にアプリケーションのビジネスロジックの主要なタスクの1つです。 OperationEntityListener



は、このonBeforeInsert()



onBeforeUpdate()



onBeforeDelete()



ます。 バランスの再計算に加えて、このリスナーはUserData



オブジェクトで最後に使用されたアカウントも記憶します。



Before-listenerでは、 EntityManager



の使用に制限がなく、エンティティのインスタンスをロードおよび変更することに注意してください。 たとえば、 addOperation()



では、 Query



を使用してQuery



Balance



インスタンスがロードおよび変更されます。 これらは、1つのトランザクションの操作と同時にデータベースに保存されます。



リスナーでは、永続コンテキストにあるオブジェクトの「前の」状態、つまりデータベースにある状態を取得する必要がある場合があります。 たとえば、この場合、 onBeforeUpdate()



場合、最初に残高から取引金額の前の値を差し引いてから、新しい値を追加する必要があります。 これを行うには、 getOldOperation()



を使用してgetOldOperation()



メソッドで新しいトランザクションが開始され、そのコンテキストでEntityManager



別のインスタンスEntityManager



取得され、同じ識別子を持つ以前の操作状態がデータベースからロードされます。 その後、リスナーが動作している現在のトランザクションに影響を与えることなく、新しいトランザクションが完了します。



中間層コンポーネント







クライアントレベルへのデータのロードとデータベースへのユーザーによる変更の保存に関する主な作業は、プラットフォームに実装されている標準のDataServiceによって実行されます。 これにより、視覚コンポーネントのデータソースが機能します。 これはアプリケーションでは十分ではないため、いくつかの特定のサービスが作成されています。



まず、 UserDataService



であり、これによりUserDataService



のキーと値のストレージを操作し、エンティティ識別子の読み取りと書き込みのための型付きインターフェイスを提供できます。 サービスインターフェイスは、クライアントレベルからアクセスできる必要があるため、 global



モジュールにあります。 サービスの実装は、 UserDataServiceBean



クラスのコアモジュールにあります。 UserDataWorker



への呼び出しを委任します。このUserDataWorker



には、有用な作業を行うコードが集中しています。 これが行われるのは、 OperationEntityListener



もこの機能が必要なためです。つまり、中間層の「内側から」です。 このサービスは「ミドルウェア境界」を形成し、クライアントブロックからの呼び出しのみを目的としています。 中間層コンポーネント内から呼び出すことはできません。これにより、インターセプターの操作が繰り返され、認証を確認して特別な方法で例外を処理します。 また、順序を復元するために、ミドルウェアの外部から呼び出されるサービスを、内部から呼び出される残りのBeanから分離する価値があります。 少なくとも、トランザクションの外部から呼び出す場合は常に存在せず、ミドルウェアコードから呼び出す場合は、トランザクションを既に開くことができるためです。



次のサービスはBalanceService



です。 任意の日に口座残高の値を取得できます。 この機能はUIの顧客と中間層(テストデータジェネレーター)の両方に必要なので、別のBalanceWorker



も配置されBalanceWorker







最後のサービスはReportService



。 カテゴリごとにレポートのデータを取得し、非永続的なCategoryAmount



エンティティのインスタンスのリストとして返します。



中間層には、テストデータを生成するために設計されたSampleDataGenerator



ビンも実装されています。 この種の機能では、通常、複雑なUIは必要ありません-単純なパラメーターの転送を呼び出しに提供するだけで十分な場合があり、属性のセットの形式で状態を表示する必要がある場合があります。 さらに、システムのユーザーではなく、管理者のみがこれを操作します。 この場合、BeanにJMXインターフェースを与え、Webクライアントに組み込まれたJMXコンソールから、または任意の外部JMXツールに接続して、そのメソッドを呼び出すと便利です。 この場合、BeanにはSampleDataGeneratorMBean



インターフェースがあり、コアモジュールのspring.xml



登録されています。



BeanのgenerateSampleData()



メソッドには@Authenticated



として注釈が付けられていることに注意してください。 つまり、このメソッドが呼び出されると、特別なシステムログインが実行され、実行スレッドにユーザーセッションが存在します。 この場合、メソッドはEntityManager



を介してエンティティを作成および変更するため、このメソッドが必要です。これらのエンティティは、保存時に属性createdBy



updatedBy



createdBy



必要とします。 一方、 removeAllData()



メソッドもJMXインターフェース経由で呼び出されますが、 QueryRunner



を介したSQLクエリを使用してデータを削除し、ユーザーセッションにどこからもアクセスしないため、認証は必要ありません。



一般に、ユーザーセッションの存在の必須チェックは、クライアントレベルから中間層への入り口(サービスインターセプター)でのみ実行されます。 ミドルウェアレベルでセッションの存在とユーザー権限を確認するかどうか-アプリケーション開発者が決定しますが、エンティティ監査の属性にユーザー名を入れる必要があるため、セッションの存在が必要な場合があります。 さらに、 DataService



がエンティティを使用したCRUD操作の実行を委任するDataWorker



であるDataWorker



でユーザー権限が常にチェックされます。



メインアプリケーションウィンドウ



CUBA Webクライアントの標準機能は、アプリケーションウィンドウの左側にある非表示のパネルで、通常はいわゆる「アプリケーションフォルダー」と「検索フォルダー」が表示されます。これらのフォルダーは、情報にすばやくアクセスするために使用されます。フォルダーをクリックすると、エンティティのリストとフィルターが適用された特定の画面が開きます。



メインウィンドウの左側に現在の残高に関する情報を表示するのは論理的に思えました。そこで、フォルダパネルの上部にバランスパネルを埋め込みました。





これは次のように行われます。







画面記述子はファイルにありますoperation-browse.xml



ここではすべてが標準です。ただし、操作テーブルで日付と金額を表すためのフォーマッタクラスの使用は除きます。



日付を表示するにはDateFormatter



、ローカライズされたメッセージのパッケージからキーによってフォーマットが送信されるプラットフォームが使用されます。したがって、フォーマット文字列は言語によって異なります。ロシア語の場合、日付はドットで区切られ、英語の場合は/で区切られます。

金額が小数部なしで表示され、0がまったく表示されないようにするために、プロジェクトでクラスが作成されましたDecimalFormatter



-金額の列で使用されます。



操作エディター



ここでより興味深いのは、操作が3つのタイプ(収入、費用、振替)のいずれかであり、編集画面が異なるように見えることです。









一見すると、最初の2つの画面は同じように見えますが、実際はそうではありません。ビジュアルコンポーネントは、エンティティのさまざまな属性Operation



-との費用、acc1



- amount1



との収入、acc2



およびとの収入amount2



です。この可変性は、コントローラーコードで完全に実装できますが、画面のさまざまな部分を別々のフレームに分割することで、より宣言的に行うことにしました。



3つのフレーム-操作の種類の数。それらはすべて、操作編集画面自体と同じパッケージにあります。ほとんどの場合、フレームは静的に接続されます-コンポーネントを使用してiframe



XML画面記述子内。操作のタイプに応じて目的のフレームを選択する必要があるため、これは私たちには適していません。したがって、画面のXML記述子ではoperation-edit.xml



、フレームのコンテナのみが定義されますgroupBox



。識別子を持つコンポーネントframeContainer



、および画面へのフレームの実際の作成と挿入は、コントローラーで実行されますOperationEdit





  @Inject private GroupBoxLayout frameContainer; private OperationFrame operationFrame; @Override public void init(Map<String, Object> params) { ... String frameId = operation.getOpType().name().toLowerCase() + "-frame"; operationFrame = openFrame(frameContainer, frameId, params);
      
      





ここでOperationFrame



-フレームコントローラ操作の種類によって実装されるインタフェース。それを介して、3つのフレームすべてを均一に管理すること、つまりそれらを初期化および検証することが便利です。コントローラー



メソッドには別の興味深いポイントがあります。操作がコミットされた後に起動するリスナーが登録されます。init()



OperationEdit





  @Override public void init(Map<String, Object> params) { ... getDsContext().addListener(new DsContext.CommitListenerAdapter() { @Override public void afterCommit(CommitContext context, Set<Entity> result) { LeftPanel leftPanel = App.getLeftPanel(); if (leftPanel != null) leftPanel.refreshBalance(); } }); }
      
      





このリスナーは、現在のバランスを表示する左パネルのコンテンツを更新します。



操作タイプフレームには、次の共通機能があります。合計で機能するテキストフィールドは、データソースに添付されません。これは、フィールドに算術式を入力できるように行われ、システムが金額を計算します。



検討してくださいexpense-frame.xml



textField



識別子を使用してコンポーネント宣言しますamountField



。コントローラーExpenseFrame



AmountCalculator



、合計計算ロジックがカプセル化されたビン使用します。

  @Inject private TextField amountField; @Inject private AmountCalculator amountCalculator; @Override public void postInit(Operation item) { amountCalculator.initAmount(amountField, item.getAmount1()); … } @Override public void postValidate(ValidationErrors errors) { BigDecimal value = amountCalculator.calculateAmount(amountField, errors); … }
      
      





Webクライアント層で定義された同じBeanは、他の2つのフレームコントローラーでも使用されます。initAmount()



Bean メソッドは、テキストボックスのデータ型でフォーマットされた現在の量を設定しますBigDecimal



datatype = decimal



コンポーネントに指定することは単純に不可能です。この場合、数字のみを入力することが可能であり、算術式を入力できる必要があるためです。メソッドcalculateAmount()



は、正規表現を使用して式の正確性をチェックし、インターフェイスを介してGroovyの式として実行しますScripting



結果は数値になり、操作を設定するためにスクリーンコントローラーに返されます。



カテゴリーレポート







この対話型レポートは画面によって実装されますcategories-report.xml



。このタイプの2つのカスタムデータソースが含まれているため、主に興味深いものCategoryAmountDatasource



です。データソースクラスはdatasourceClass



element 属性で指定されますcollectionDatasource



。これらのデータソースにはJPQL演算子も指定されますが、使用されず、指定されていない場合はStudioがクエリテキストを自動的に生成するためにのみ存在します。実際、データソースCategoryAmountDatasource



はメソッドloadData()



オーバーライドDataService



、JPQLクエリを介しデータをロードする代わりに、serviceを呼び出しReportService



、必要なパラメーターを渡します。

 public class CategoryAmountDatasource extends CollectionDatasourceImpl<CategoryAmount, UUID> { private ReportService service = AppBeans.get(ReportService.NAME); @Override protected void loadData(Map<String, Object> params) { ... Date fromDate = (Date) params.get("from"); Date toDate = (Date) params.get("to"); ... List<CategoryAmount> list = service.getTurnoverByCategories(fromDate, toDate, categoryType, currency.getCode(), ids); for (CategoryAmount categoryAmount : list) { data.put(categoryAmount.getId(), categoryAmount); } ... }
      
      





パラメータは、refresh()



データソースメソッドのスクリーンコントローラーによって設定されますメソッドrefreshDs1()



refreshDs2()



クラスを参照してくださいCategoriesReport



。サービスは非永続エンティティインスタンスのリストを返しCategoryAmount



、データソースはそれらをデータコレクションに保存します。したがって、これらのデータソースに関連付けられたテーブルCategoryAmount



は、通常の方法でデータベースからロードされた他のエンティティとしてインスタンス表示します[除外



]ボタンの機能は興味深いように設計されており、選択したカテゴリを検討対象から除外できます。このような2つのボタンは



、記述子でcategories-report.xml



宣言されています-左右のテーブル用です。各ボタンはアクションに関連付けられています。excludeCategory



あなたのテーブル。ただし、XML記述子のテーブルに対してアクションは宣言されていません。どのように機能しますか?実際、この場合のテーブルのアクションはinit()



スクリーンコントローラーメソッドに追加されますinitExcludedCategories()



メソッドを参照してください。このメソッドは、サービスを使用して記憶されていた以前に除外されたカテゴリのリストも「リコール」しますUserDataService







型アクションはExcludeCategoryAction



、トリガーされると、除外されたカテゴリに対応するリンクボタンを持つコンテナと碑文を作成し、ハンドルで事前に宣言されたコンテナ内に新しいコンテナを配置するメソッドexcludeCategory()



呼び出しComponentsFactory



ますexcludedBox



ボタンごとにリスナーが作成され、トリガーされると、ボタンがラベルとともに配置されているコンテナ全体が親コンテナから削除されます。さらに、データソースはカテゴリリストを再編成することにより更新されます。



一般に、カテゴリレポート画面はプラットフォームを使用するためのかなり非標準のオプションであるため、多くの手動で記述されたロジックがあり、通常はコンポーネントの相互作用の標準オプション内に隠されています。



謝辞



しばらく使っていた素晴らしいzenmoney.ruサービスからアイデアを得ました。プラットフォームに含まれるすべてのオープンソースライブラリとフレームワークは、[ヘルプ]-> [バージョン情報]-> [クレジット]ウィンドウにリストされています



続く



同じアプリケーションに関する次の記事では、Backbone.js + Bootstrapで記述され、REST APIを介して中間層と対話するデバイスブロックレスポンシブUIについて説明する予定です。さらに、メインUIのテーマをわずかに変更し、新しいUIコンポーネントを追加して、プロジェクトでインターフェイスをカスタマイズする可能性を示します。



All Articles