37

私は新しい Java Web アプリケーションを開発しており、データを永続化するための新しい方法 (私にとっては新しい!) を模索しています。私は主にJPAとHibernateの経験がありますが、単純なケースを除いて、この種の完全なORMは非常に複雑になる可能性があると思います. さらに、私は彼らと一緒に仕事をするのがあまり好きではありません。おそらくSQLに近い新しいソリューションを探しています。

私が現在調査している解決策:

しかし、Hibernate と比較して、これらのソリューションには 2 つのユース ケースについて懸念があります。これらのユースケースで推奨されるパターンを知りたいです。


ユース ケース 1 - エンティティを取得し、関連付けられた子エンティティと孫エンティティの一部にアクセスします。

  • Person私がエンティティ を持っているとしましょう。
    • これPersonには関連付けられたAddressエンティティがあります。
      • これAddressには関連付けられたCityエンティティがあります。
        • このCityエンティティにはnameプロパティがあります。

person エンティティから始まる都市の名前にアクセスするためのフル パスは、次のようになります。

person.address.city.name

PersonServiceここで、このメソッドを使用して、から Person エンティティをロードするとします。

public Person findPersonById(long id)
{
    // ...
}

Hibernate を使用すると、に関連付けられたエンティティPersonをオンデマンドで遅延ロードできるためperson.address.city.name、このプロパティにアクセスして、確実にアクセスできるようになります (そのチェーン内のすべてのエンティティが null 許容でない限り)。

しかし、私が調査している 3 つのソリューションのいずれかを使用すると、より複雑になります。これらのソリューションでは、このユース ケースを処理するために推奨されるパターンは何ですか? 前もって、3 つのパターンが考えられます。

  1. 必要なすべての関連付けられた子エンティティと孫エンティティは、使用された SQL クエリによって熱心に読み込まれます。

    しかし、このソリューションで見られる問題は、エンティティから他のエンティティ/プロパティ パスにアクセスする必要がある他のコードが存在する可能性があることPersonです。たとえば、一部のコードは へのアクセスが必要になる場合がありますperson.job.salary.currency。既に持っているメソッドを再利用したい場合はfindPersonById()、SQL クエリでさらに情報をロードする必要があります。address->city関連エンティティだけでなく、関連エンティティもjob->salary

    person エンティティから始まる他の情報にアクセスする必要がある場所が他に10か所ある場合はどうなるでしょうか。潜在的に必要なすべての情報を常に熱心にロードする必要がありますか? または、個人エンティティをロードするために 12 の異なるサービス メソッドを使用することはありますか? :

    findPersonById_simple(long id)
    
    findPersonById_withAdressCity(long id)
    
    findPersonById_withJob(long id)
    
    findPersonById_withAdressCityAndJob(long id)
    
    ...
    

    しかし、Personエンティティを使用するたびに、何がロードされていて何がロードされていないかを知る必要があります...それはかなり面倒ですよね?

  2. エンティティのgetAddress()getter メソッドでPerson、アドレスが既に読み込まれているかどうかを確認し、読み込まれていない場合は遅延読み込みを行うことができますか? これは、実際のアプリケーションで頻繁に使用されるパターンですか?

  3. ロードされたモデルから必要なエンティティ/プロパティに確実にアクセスできるようにするために使用できる他のパターンはありますか?


ユース ケース 2 - エンティティを保存し、関連付けられたエンティティと変更されたエンティティも保存されることを確認します。

thisのメソッドPersonを使用してエンティティを保存できるようにしたい:PersonService

public void savePerson(Person person)
{
    // ...
}

Personエンティティがあり、別のものに変更した場合、エンティティの変更が保存されてperson.address.city.nameいることを確認するにはどうすればよいですか? Hibernate を使用すると、保存操作を関連付けられたエンティティに簡単にカスケードできます。私が調査しているソリューションはどうですか?CityPerson

  1. 人を保存するときに、関連付けられているエンティティも保存する必要があることを知るために、何らかのダーティフラグを使用する必要がありますか?

  2. このユースケースに対処するのに役立つ既知のパターンは他にありますか?


更新:JOOQ フォーラムでこの質問についての議論があります。

4

8 に答える 8

30

多くの質問に対する答えは簡単です。3 つの選択肢があります。

  1. あなたが言及した3つのSQL中心のツール(MyBatisjOOQDbUtils)のいずれかを使用してください。これは、オブジェクト指向ドメイン モデルとオブジェクト リレーショナル マッピング (つまり、エンティティと遅延読み込み) の観点から考えるのをやめるべきであることを意味します。SQL はリレーショナル データに関するものであり、RBDMS は複数の結合の結果を「熱心にフェッチ」するための実行計画を計算するのに非常に優れています。通常、時期尚早のキャッシュはあまり必要ありません。たまにデータ要素をキャッシュする必要がある場合でも、EhCacheのようなものを使用できます。

  2. これらの SQL 中心のツールは使用せず、Hibernate / JPA を使用してください。Hibernate が嫌いだと言ったとしても、「Hibernate を考えている」からです。Hibernate は、オブジェクト グラフをデータベースに永続化するのに非常に優れています。これらのツールはどれも、Hibernate のように機能するように強制することはできません。その使命は別のものだからです。彼らの使命は、SQL を操作することです。

  3. まったく別の方法で、リレーショナル データ モデルを使用しないことを選択します。他のデータ モデル (たとえばグラフ) の方が適している場合があります。私はこれを 3 番目の選択肢としています。実際にはその選択肢がないかもしれませんし、私は代替モデルについて個人的な経験があまりないからです。

あなたの質問は特にjOOQに関するものではないことに注意してください。それにもかかわらず、jOOQ を使用すると、 ModelMapperなどの外部ツールを使用して、(結合されたテーブル ソースから生成された) フラットなクエリ結果のオブジェクト グラフへのマッピングを外部化できます。ModelMapper User Groupには、このような統合に関する興味深い進行中のスレッドがあります。

(免責事項:私はjOOQの背後にある会社で働いています)

于 2013-07-25T18:27:49.527 に答える
30

この種の問題は、実際の ORM を使用していない場合に典型的なものであり、特効薬はありません。iBatis(myBatis)を使用した(それほど大きくない)Webアプリケーションでうまくいった単純な設計アプローチは、永続化のために2つのレイヤーを使用することです。

  • 単純な低レベル層: 各テーブルには、テーブルの列に直接マップされるフィールドを持つ Java クラス (POJO または DTO) があります。テーブルを指すフィールドを持つPERSONテーブルがあるとします。次に、(整数) フィールドだけを持つクラスを作成します。メソッドはなく 、単純な. したがって、これらの Java クラスは非常に馬鹿げています (永続性や関連するクラスについては知りません)。対応するクラスは、このオブジェクトをロード/永続化する方法を知っています。このレイヤーは、iBatis + iBator (または MyBatis + MYBatisGenerator ) などのツールを使用して簡単に作成および維持できます。ADDRESS_IDADRESSPersonDbaddressIdpersonDb.getAdress()personDb.getAdressId()PersonDao

  • 豊富なドメイン オブジェクトを含むより高いレベルのレイヤー: これらのそれぞれは通常、上記の POJOのグラフです。これらのクラスには、それぞれの DAO を呼び出すことによって、グラフを (おそらく遅延して、おそらくいくつかのダーティ フラグを使用して) ロード/保存するためのインテリジェンスもあります。ただし、重要なことは、これらの豊富なドメイン オブジェクトが POJO オブジェクト (または DB テーブル) に 1 対 1 でマッピングされるのではなく、ドメイン ユース ケースにマッピングされるということです。各グラフの「サイズ」は決定され (無限に大きくなるわけではありません)、特定のクラスのように外部から使用されます。したがって、Person複数のユース ケースまたはサービス メソッドで使用される 1 つの豊富なクラス (関連するオブジェクトの不確定なグラフを含む) があるわけではありません。代わりに、いくつかの豊富なクラスがありますPersonWithAddresesPersonWithAllData...それぞれが、独自の永続化ロジックを使用して、特定の十分に制限されたグラフをラップします。これは非効率的または不器用に見えるかもしれませんし、状況によってはそうかもしれませんが、オブジェクトの完全なグラフを保存する必要がある場合のユースケースは実際には限られていることがよくあります。

  • さらに、表形式のレポート (表示される一連の列を返す特定の SELECTS) などの場合は、上記を使用せず、単純で愚かな POJO (おそらくマップでさえ) を使用します。

ここで私の関連する回答を参照してください

于 2013-07-29T15:00:27.843 に答える
11

持続性アプローチ

シンプル/基本的なものから洗練された/リッチなものまで、ソリューションの範囲は次のとおりです。

  • SQL/JDBC - オブジェクト内のハードコード SQL
  • SQL ベースのフレームワーク (jOOQ、MyBatis など) - アクティブ レコード パターン (別の一般的なオブジェクトが行データを表し、SQL を処理します)
  • ORM-Framework (Hibernate、EclipseLink、DataNucleus など) - Data Mapper パターン (エンティティごとのオブジェクト) と Unit Of Work パターン (Persistence Context / Entity Manager)

最初の 2 つのレベルのいずれかを実装しようとします。これは、オブジェクト モデルから SQL に焦点を移すことを意味します。しかし、あなたの質問は、オブジェクト モデルが SQL にマップされている (つまり、ORM の動作) を含むユース ケースを求めています。最初の 2 つのレベルのいずれかからの機能に対して、3 番目のレベルからの機能を追加したいと考えています。

Active Record 内でこの動作を実装することを試みることができます。ただし、これには、各 Active Record インスタンスにリッチ メタデータを添付する必要があります。実際に関与するエンティティ、他のエンティティとの関係、遅延読み込み設定、カスケード更新設定です。これにより、効果的に非表示のマップされたエンティティ オブジェクトになります。また、ユースケース 1 と 2 では、jOOQ と MyBatis はこれを行いません。

あなたの要求を達成する方法?

フレームワークまたは未加工の SQL/JDBC 上の小さなカスタム レイヤーとして、狭い ORM 動作をオブジェクトに直接実装します。

ユース ケース 1: 各エンティティ オブジェクト リレーションシップのメタデータを格納します。(i) リレーションシップを遅延ロードする必要があるかどうか (クラス レベル)、および (ii) 遅延ロードが発生したかどうか (オブジェクト レベル)。次に getter メソッドで、これらのフラグを使用して遅延読み込みを行うかどうかを決定し、実際に行います。

ユース ケース 2: ユース ケース 1 と同様 - 自分で行います。各エンティティ内にダーティ フラグを格納します。各エンティティ オブジェクトの関係に対して、保存をカスケードする必要があるかどうかを示すフラグを保存します。次に、エンティティが保存されると、各「保存カスケード」関係に再帰的にアクセスします。発見されたダーティ エンティティを書き込みます。

パターン

長所

  • SQL フレームワークへの呼び出しは簡単です。

短所

  • オブジェクトはより複雑になります。オープン ソース製品内のユース ケース 1 と 2 のコードを見てみましょう。些細なことじゃない
  • オブジェクト モデルのサポートの欠如。ドメインで Java のオブジェクト モデルを使用している場合、データ操作のサポートは少なくなります。
  • スコープ クリープとアンチパターンのリスク: 上記の不足している機能は氷山の一角です。ビジネス ロジックでホイールとインフラストラクチャの膨張を再発明することになる可能性があります。
  • 非標準ソリューションに関する教育とメンテナンス。JPA、JDBC、および SQL が標準です。他のフレームワークやカスタム ソリューションはそうではありません。

価値がある?

このソリューションは、データ処理要件がかなり単純で、エンティティの数が少ないデータ モデルである場合にうまく機能します。

  • もしそうなら、素晴らしい!上記を行います。
  • そうでない場合、このソリューションは適合性が低く、誤った労力の節約になります。つまり、ORM を使用するよりも時間がかかり、複雑になります。その場合は、JPA をもう一度見てください。思ったより単純かもしれません。CRUD 用の ORM と複雑なクエリ用の生の SQL をサポートしています :-)。
于 2013-07-29T06:47:50.560 に答える
8

過去 10 年間、私は JDBC、EJB エンティティ Bean、Hibernate、GORM、そして最後に JPA (この順序で) を使用していました。現在のプロジェクトでは、パフォーマンスに重点が置かれているため、プレーン JDBC の使用に戻りました。だから欲しかった

  • SQL ステートメントの生成を完全に制御: ステートメントを DB パフォーマンス チューナーに渡し、最適化されたバージョンをプログラムに戻すことができます。
  • データベースに送信される SQL ステートメントのを完全に制御
  • ストアド プロシージャ (トリガー)、ストアド関数 (SQL クエリでの複雑な計算用)
  • 利用可能なすべての SQL 機能を制限なく使用できるようにするため (CTE を使用した再帰クエリ、ウィンドウ集計関数など)

データ モデルはデータ ディクショナリで定義されます。モデル駆動型のアプローチを使用して、ジェネレーターはヘルパー クラス、DDL スクリプトなどを作成します。データベースに対するほとんどの操作は読み取り専用です。少数のユースケースのみが書かれています。

質問 1: 子の取得

システムはユースケースに基づいて構築されており、特定のユースケース/リクエストのすべてのデータを取得するための専用のSQL ステートメントが 1 つあります。一部の SQL ステートメントは 20kb を超えており、Java/Scala で記述されたストアド関数を使用して結合、計算、並べ替え、ページ付けなどを行い、結果がデータ転送オブジェクトに直接マップされ、それが次にビューに供給されます。 (アプリケーション層でのそれ以上の処理はありません)。結果として、データ転送オブジェクトもユース ケース固有になります。特定のユース ケースのデータのみが含まれます (それ以上でもそれ以下でもありません)。

結果セットはすでに「完全に結合」されているため、レイジー/イーガー フェッチなどは必要ありません。データ転送オブジェクトは完成しています。キャッシュは必要ありません (データベース キャッシュは問題ありません)。例外: 結果セットが大きい場合 (約 50,000 行)、データ転送オブジェクトがキャッシュ値として使用されます。

質問 2: 節約

コントローラーが GUI からの変更を元にマージした後、データを保持する特定のオブジェクトがあります。基本的には、階層内の状態 (新規、削除、変更など) を持つ行です。階層を下ってデータを保存するのは手動の繰り返しです。新しいデータまたは変更されたデータは、いくつかのヘルパー クラスを生成する SQLinsertまたはupdateコマンドを使用して永続化されます。削除済みアイテムに関しては、これはカスケード削除 (PostgreSql) に最適化されています。複数の行を削除する場合、これも 1 つのdelete ... where id in ...ステートメントに最適化されます。

繰り返しますが、これはユースケース固有であるため、一般的なアプローチを扱っているわけではありません。より多くのコード行が必要ですが、これらは最適化を含む行です。

これまでの経験

  • Hibernate や JPA を学ぶ努力を過小評価すべきではありません。キャッシュの構成、クラスターでのキャッシュの無効化、熱心/遅延フェッチ、およびチューニングに費やされる時間も考慮する必要があります。別の Hibernate メジャー バージョンへの移行は、単なる再コンパイルではありません。
  • ORM なしでアプリケーションを構築する労力を過大評価すべきではありません。
  • SQL を直接使用する方が簡単です。特に DB パフォーマンス チューナーを使用する場合は、SQL (HQL、JPQL など)に近いことは同じではありません。
  • 特に Scala で記述されたストアド関数と組み合わせた場合、長くて複雑なクエリを実行する場合、SQL サーバーは非常に高速です。
  • ユース ケース固有の SQL ステートメントを使用しても、コード サイズは小さくなります。「完全に結合された」結果セットにより、アプリケーション層で多くの行が節約されます。

関連情報:

更新: インターフェイス

Person論理データ モデルにエンティティがある場合Person、JPA エンティティと同様に、(CRUD 操作用に) エンティティを永続化するクラスがあります。

しかし、Personユース ケース、クエリ、ビジネス ロジックの観点からは、エンティティは 1 つだけではありません。各サービス メソッドは独自の の概念を定義し、そのユース ケースに必要な値Personのみを正確に含んでいます。それ以上でもそれ以下でもありません。私たちは Scala を使用しているため、多くの小さなクラスの定義と使用は非常に効率的です (setter/getter ボイラー プレート コードは必要ありません)。

例:

class GlobalPersonUtils {
  public X doSomeProcessingOnAPerson(
    Person person, PersonAddress personAddress, PersonJob personJob, 
    Set<Person> personFriends, ...)
}

に置き換えられます

class Person {
  List addresses = ...
  public X doSomeProcessingOnAPerson(...)
}

class Dto {
  List persons = ...
  public X doSomeProcessingOnAllPersons()
  public List getPersons()
}

Personsユース ケース固有の使用Adressesなど: この場合、 はPersonすでにすべての関連データを集約しています。より多くのクラスが必要ですが、JPA エンティティを渡す必要はありません。

この処理は読み取り専用であり、結果はビューによって使用されることに注意してください。City例:個人のリストから個別のインスタンスを取得します。

データが変更された場合、これは別のユース ケースです。人物の市区町村が変更された場合、これは別のサービス メソッドによって処理され、データベースから人物が再度取得されます。

于 2013-08-01T23:06:03.510 に答える
1

質問の中心的な問題は、リレーショナル モデル自体にあるようです。これまで説明してきたことから、グラフ データベースは問題領域を非常にきれいにマッピングします。別の方法として、ドキュメント ストアは問題にアプローチする別の方法です。インピーダンスは依然として存在しますが、一般的にドキュメントはセットよりも簡単に推論できるためです。もちろん、どのアプローチにも注意が必要な独自の癖があります。

于 2013-08-03T04:36:44.577 に答える