現代の開発の世界には、作業を楽にするために設計されたさまざまな仕様が溢れています。ツールを理解すれば、適切なツールを選択できます。知らないと、人生がより困難になる可能性があります。このレビューは、JPA - Java Persistence API の概念に関する秘密のベールを取り除きます。読んだ後は、この不思議な世界にさらに深く入り込んでいただければ幸いです。
JPA 仕様の「 」章に示されている例から内容をコピーしてみましょう
エンティティ間の接続を確立することは、マッピングまたは関連付け (関連付けマッピング) と呼ばれます。JPA を使用して確立できる関連付けの種類を以下に示します。
同じトピックに関する良い回答は、ここで読むことができます:「ORM oneToMany、manyToMany 関係を I'm Five のように説明する」。カテゴリにトピックとの関連がある場合
導入
ご存知のとおり、プログラムの主なタスクの 1 つはデータの保存と処理です。古き良き時代には、人々は単にデータをファイルに保存していました。しかし、読み込みと編集の同時アクセスが必要になるとすぐに、負荷がかかると (つまり、複数のリクエストが同時に到着すると)、データを単にファイルに保存するだけでは問題が発生します。データベースがどのような問題をどのように解決するかについて詳しくは、「データベースの構造」の記事を読むことをお勧めします。これは、データをデータベースに保存することにしたことを意味します。長い間、Java は JDBC API (Java Database Connectivity) を使用してデータベースを操作できました。JDBC について詳しくは、「JDBC またはすべての始まり」を参照してください。しかし、時が経ち、開発者はそのたびに、Java オブジェクトをデータベースに保存する際の簡単な操作のために、同じタイプの不要な「メンテナンス」コード (いわゆるボイラープレート コード) を記述する必要が生じ、またその逆に、データベースからのデータを使用して Java オブジェクトを作成する必要がありました。データベース。そして、これらの問題を解決するためにORMという概念が生まれました。 ORM - オブジェクト リレーショナル マッピング、またはロシア語のオブジェクト リレーショナル マッピングに翻訳されました。データベースとオブジェクト指向プログラミング言語の概念を結び付けるプログラミング技術です。簡単に説明すると、ORM は Java オブジェクトとデータベース内のレコードの間の接続です。ORM は 本質的に、Java オブジェクトをデータベース内のデータとして表現できる (またはその逆) という概念です。これは、JPA 仕様 - Java Persistence API の形式で具体化されました。仕様はすでにこの概念を表現する Java API の記述です。仕様は、ORM の概念に従って動作するためにどのようなツールを提供する必要があるか (つまり、どのようなインターフェイスを介して作業できるか) を示しています。そして、これらの資金の使い方。仕様にはツールの実装については記載されていません。これにより、1 つの仕様に対して異なる実装を使用することが可能になります。これを単純化して、仕様は API の説明であると言うことができます。JPA 仕様のテキストは、Oracle の Web サイト「 JSR 338: JavaTM Persistence API 」で参照できます。したがって、JPA を使用するには、そのテクノロジーを使用するための実装が必要です。JPA 実装は JPA プロバイダーとも呼ばれます。最も注目すべき JPA 実装の 1 つはHibernateです。したがって、検討することを提案します。プロジェクトの作成
JPA は Java に関するものであるため、Java プロジェクトが必要になります。ディレクトリ構造を手動で作成し、必要なライブラリを自分で追加することもできます。しかし、プロジェクトのアセンブリを自動化するためのシステムを使用する方がはるかに便利で正確です (つまり、本質的に、これはプロジェクトのアセンブリを管理する単なるプログラムです。ディレクトリを作成し、必要なライブラリをクラスパスに追加するなど)。 。)。そのようなシステムの 1 つが Gradle です。Gradle の詳細については、「Gradle の概要」を参照してください。ご存知のとおり、Gradle の機能 (つまり、できること) は、さまざまな Gradle プラグインを使用して実装されます。Gradle と「 Gradle Build Init Plugin 」プラグインを使ってみましょう。コマンドを実行しましょう:
gradle init --type java-application
Gradle は必要なディレクトリ構造を作成し、ビルド スクリプト内にプロジェクトの基本的な宣言的記述を作成しますbuild.gradle
。それで、アプリケーションがあります。アプリケーションで何を記述またはモデル化したいのかを考える必要があります。たとえば、app.quickdatabasediagrams.com など のモデリング ツールを使用してみましょう。ここで、説明したのは「ドメイン モデル」であると言う価値があります。ドメインとは「主題領域」です。一般にドメインとはラテン語で「所有」を意味します。中世において、これは王や封建領主が所有していた地域に与えられた名前でした。そしてフランス語では、それは単に「地域」を意味する「ドメーヌ」という言葉になりました。したがって、「ドメイン モデル」=「サブジェクト モデル」と説明しました。このモデルの各要素は、ある種の「エッセンス」、つまり実生活からのものです。私たちの場合、これらはエンティティです: カテゴリ ( Category
)、件名 ( Topic
)。たとえばモデルという名前で、エンティティ用に別のパッケージを作成しましょう。そして、エンティティを記述する Java クラスをそこに追加しましょう。Java コードでは、このようなエンティティは通常のPOJOであり、次のようになります。
public class Category {
private Long id;
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
クラスの内容をコピーして、類推してクラスを作成してみましょうTopic
。彼が異なるのは、自分が属するカテゴリーについて知っていることだけです。Topic
したがって、カテゴリ フィールドとそれを操作するためのメソッドをクラスに 追加しましょう。
private Category category;
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
これで、独自のドメイン モデルを持つ Java アプリケーションが完成しました。ここで、JPA プロジェクトへの接続を開始します。
JPAの追加
したがって、覚えているとおり、JPA はデータベースに何かを保存することを意味します。したがって、データベースが必要です。プロジェクトでデータベース接続を使用するには、データベースに接続するための依存関係ライブラリを追加する必要があります。覚えているとおり、私たちは Gradle を使用して、ビルド スクリプトを作成しましたbuild.gradle
。その中で、プロジェクトに必要な依存関係について説明します。依存関係は、それなしではコードが動作できないライブラリです。まず、データベースへの接続の依存関係について説明します。これは、単に JDBC を操作する場合と同じ方法で行います。
dependencies {
implementation 'com.h2database:h2:1.4.199'
これでデータベースができました。これで、Java オブジェクトをデータベース概念 (Java から SQL へ) にマッピングする役割を担うレイヤーをアプリケーションに追加できるようになりました。覚えているとおり、これには Hibernate と呼ばれる JPA 仕様の実装を使用します。
dependencies {
implementation 'com.h2database:h2:1.4.199'
implementation 'org.hibernate:hibernate-core:5.4.2.Final'
次に、JPA を構成する必要があります。仕様とセクション「8.1 永続ユニット」を読むと、永続ユニットが構成、メタデータ、エンティティのある種の組み合わせであることがわかります。また、JPA が機能するには、構成ファイル内に と呼ばれる永続ユニットを少なくとも 1 つ記述する必要がありますpersistence.xml
。その場所は仕様の章「8.2 永続ユニットのパッケージ化」で説明されています。このセクションによると、Java SE 環境がある場合は、それを META-INF ディレクトリのルートに配置する必要があります。
8.2.1 persistence.xml file
。
<persistence>
<persistence-unit name="JavaRush">
<description>Persistence Unit For test</description>
<class>hibernate.model.Category</class>
<class>hibernate.model.Topic</class>
</persistence-unit>
</persistence>
しかし、これだけでは十分ではありません。JPA プロバイダーが誰であるかを伝える必要があります。JPA 仕様を実装する人:
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
次に、設定を追加してみましょう(properties
)。それらの一部 ( で始まるjavax.persistence
) は標準の JPA 構成であり、JPA 仕様のセクション「8.2.1.9 プロパティ」で説明されています。一部の設定はプロバイダー固有です (私たちの場合、それらは JPA プロバイダーとしての Hibernate に影響します。設定ブロックは次のようになります:
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />
<property name="javax.persistence.jdbc.user" value="sa" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
これで、JPA 互換の config ができましたpersistence.xml
。JPA プロバイダー Hibernate と H2 データベースがあり、ドメイン モデルである 2 つのクラスもあります。最後にこれをすべて機能させましょう。カタログでは/test/java
、私たちの Gradle が単体テスト用のテンプレートを生成し、それを AppTest と名付けました。使ってみましょう。JPA 仕様の「7.1 永続コンテキスト」の章に記載されているように、JPA 世界のエンティティは永続コンテキストと呼ばれる空間に存在します。ただし、永続コンテキストを直接操作することはありません。Entity Manager
このために、「エンティティ マネージャー」を使用します。コンテキストとそこにどのような存在が住んでいるかを知っているのは彼です。私たちは「オム」と対話しますEntity Manager
。あとは、これをどこから入手できるかを理解するだけですEntity Manager
。JPA 仕様の「7.2.2 アプリケーション管理エンティティ マネージャーの取得」の章によれば、 を使用する必要がありますEntityManagerFactory
。したがって、JPA 仕様を準備して、「7.3.2 Java SE 環境でのエンティティ マネージャー ファクトリの取得」の章から例を取得し、単純な単体テストの形式でフォーマットしてみましょう。
@Test
public void shouldStartHibernate() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
EntityManager entityManager = emf.createEntityManager();
}
このテストでは、すでに「認識されない JPApersistence.xml XSD バージョン」というエラーが表示されます。その理由は、persistence.xml
JPA 仕様のセクション「8.3persistence.xml スキーマ」に記載されているように、使用するスキーマを正しく指定する必要があるためです。
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
version="2.2">
さらに、要素の順序も重要です。したがって、provider
クラスをリストする前に指定する必要があります。この後、テストは正常に実行されます。JPA直接接続が完了しました。次に進む前に、残りのテストについて考えてみましょう。各テストには が必要ですEntityManager
。EntityManager
実行の開始時に各テストに独自のテストがあることを確認してください。さらに、データベースは常に新しいものにしたいと考えています。inmemory
オプションを使用しているため、 を閉じるだけで十分ですEntityManagerFactory
。作成はFactory
コストのかかる作業です。しかし、テストの場合はそれが正当化されます。JUnit を使用すると、各テストの実行前 (Before) と実行後 (After) に実行されるメソッドを指定できます。
public class AppTest {
private EntityManager em;
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
em = emf.createEntityManager();
}
@After
public void close() {
em.getEntityManagerFactory().close();
em.close();
}
ここで、テストを実行する前に、新しいテストが作成されますEntityManagerFactory
。これには、新しいデータベースの作成が伴います。hibernate.hbm2ddl.auto
という意味がありますcreate
。そして新しい工場から新しいものを入手しますEntityManager
。
エンティティ
覚えているとおり、以前にドメイン モデルを記述するクラスを作成しました。これらが私たちの「本質」であるとすでに述べました。これは、 を使用して管理するエンティティですEntityManager
。カテゴリの本質を保存するための簡単なテストを書いてみましょう。
@Test
public void shouldPersistCategory() {
Category cat = new Category();
cat.setTitle("new category");
// JUnit обеспечит тест свежим EntityManager'ом
em.persist(cat);
}
しかし、このテストはすぐには機能しません。なぜなら... エンティティが何であるかを理解するのに役立つさまざまなエラーが表示されます。
-
Unknown entity: hibernate.model.Category
Category
なぜ Hibernate はこれが 何であるかを理解しないのでしょうかentity
? 重要なのは、エンティティは JPA 標準に従って記述されなければならないということです。JPA 仕様の「2.1 エンティティ クラス」の章に記載されているように、
エンティティ クラスには アノテーション を付ける必要があります。@Entity
-
No identifier specified for entity: hibernate.model.Category
エンティティには、あるレコードを別のレコードから区別するために使用できる一意の識別子が必要です。
JPA仕様の「2.4 主キーとエンティティのアイデンティティ」の章によれば、「すべてのエンティティは主キーを持つ必要がある」、つまり すべてのエンティティには「主キー」が必要です。このような主キーはアノテーションで指定する必要があります@Id
-
ids for this class must be manually assigned before calling save()
ID はどこかから取得したものでなければなりません。手動で指定することも、自動的に取得することもできます。
したがって、「11.2.3.3 GeneratedValue」および「11.1.20 GeneratedValue アノテーション」の章で示すように、アノテーションを指定できます@GeneratedValue
。
@Entity
public class Category {
@Id
@GeneratedValue
private Long id;
さらに、注釈は@Id
どれを使用するかを示しますAccess Type
。アクセス タイプの詳細については、JPA 仕様の「2.3 アクセス タイプ」セクションを参照してください。非常に簡単に言うと、なぜなら... @Id
フィールド ( ) の上に指定したfield
場合、アクセス タイプは ではなくfield-based
、 default になりますproperty-based
。したがって、JPA プロバイダーはフィールドから値を直接読み取り、保存します。@Id
ゲッターの上に配置すると、property-based
アクセスが使用されます。ゲッターとセッターを介して。テストを実行すると、(オプションのおかげで) どのようなリクエストがデータベースに送信されたかも確認できますhibernate.show_sql
。ただし、保存すると、「」は表示されませんinsert
。実際には何も保存していないことが判明しましたか? JPA では、次のメソッドを使用して永続コンテキストとデータベースを同期できますflush
。
entityManager.flush();
しかし、今実行すると、「トランザクションは進行中でありません」というエラーが発生します。ここで、JPA がトランザクションをどのように使用するかを学びましょう。
JPAトランザクション
覚えているとおり、JPA は永続化コンテキストの概念に基づいています。ここは実体が住む場所です。そして、 を通じてエンティティを管理しますEntityManager
。コマンドを実行するとpersist
、エンティティがコンテキスト内に配置されます。より正確には、EntityManager
これを行う必要があることを伝えます。ただし、このコンテキストは単なるストレージ領域です。「一次キャッシュ」と呼ばれることもあります。ただし、データベースに接続する必要があります。このコマンドはflush
、以前はエラーで失敗しましたが、永続コンテキストのデータをデータベースと同期します。ただし、これには転送が必要であり、この転送はトランザクションです。JPA のトランザクションについては、仕様の「7.5 トランザクションの制御」セクションで説明されています。JPA でトランザクションを使用するための特別な API があります。
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
テストの前後に実行されるコードにトランザクション管理を追加する必要があります。
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
em = emf.createEntityManager();
em.getTransaction().begin();
}
@After
public void close() {
if (em.getTransaction().isActive()) {
em.getTransaction().commit();
}
em.getEntityManagerFactory().close();
em.close();
}
追加後、以前は存在しなかった SQL の式が挿入ログに表示されます。
EntityManager
トランザクションに蓄積された変更はデータベースにコミット (確認および保存) されました。では、自分の本質を探ってみましょう。ID でエンティティを検索するテストを作成しましょう。
@Test
public void shouldFindCategory() {
Category cat = new Category();
cat.setTitle("test");
em.persist(cat);
Category result = em.find(Category.class, 1L);
assertNotNull(result);
}
この場合、以前に保存したエンティティを受け取りますが、ログには SELECT クエリが表示されません。そしてすべては、「エンティティ マネージャー、ID=1 のカテゴリ エンティティを見つけてください。」という言葉に基づいています。そして、エンティティ マネージャーはまずコンテキストを調べ (一種のキャッシュを使用します)、見つからない場合にのみデータベースを調べます。SELECT
ID を 2 に変更する価値があります (そのようなことはありません。保存したインスタンスは 1 つだけです)。リクエストが表示されることがわかります。コンテキスト内にエンティティが見つからず、EntityManager
データベースがエンティティを見つけようとしているため、コンテキスト内のエンティティの状態を制御するために使用できるさまざまなコマンドがあります。エンティティがある状態から別の状態への遷移は、エンティティのライフ サイクルと呼ばれますlifecycle
。
エンティティのライフサイクル
エンティティのライフサイクルは、JPA仕様の「3.2 エンティティインスタンスのライフサイクル」の章に記載されています。なぜなら エンティティはコンテキスト内に存在し、 によって制御されているEntityManager
場合、エンティティは制御されていると言います。管理された。エンティティの生涯の段階を見てみましょう。
// 1. New or Transient (временный)
Category cat = new Category();
cat.setTitle("new category");
// 2. Managed or Persistent
entityManager.persist(cat);
// 3. Транзакция завершена, все сущности в контексте detached
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
// 4. Сущность изымаем из контекста, она становится detached
entityManager.detach(cat);
// 5. Сущность из detached можно снова сделать managed
Category managed = entityManager.merge(cat);
// 6. И можно сделать Removed. Интересно, что cat всё равно detached
entityManager.remove(managed);
それを統合した図は次のとおりです。
マッピング
JPA では、エンティティ間の関係を記述することができます。ドメイン モデルを扱ったときに、エンティティ相互の関係をすでに調べたことを思い出してください。次に、quickdatabasediagrams.comリソースを使用しました。Topic
トピックを説明するエンティティを見てみましょう。Topic
に対する態度については何と言えますかCategory
?多くはTopic
1 つのカテゴリに属します。したがって、関連付けが必要ですManyToOne
。この関係を JPA で表現してみましょう。
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
どの注釈を付けるかを覚えておくには、最後の部分がその上に注釈が示されるフィールドを担当していることを覚えておくとよいでしょう。ToOne
- 特定のインスタンス。ToMany
- コレクション。これで、接続は一方向になりました。双方向のコミュニケーションにしましょう。このカテゴリに含まれるCategory
すべての人についての知識を追加しましょう。リストがあるため、Topic
これは で終わる必要があります。それは、「多くの話題に対して」という態度です。疑問は残ります -または: ToMany
Topic
OneToMany
ManyToMany
ToMany
、これらの各トピックはカテゴリを 1 つだけ持つことができ、その場合は になりますOne
。それ以外の場合は、になりますMany
。したがって、Category
すべてのトピックのリストは次のようになります。
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
Category
そして、基本的にすべてのトピックのリストを取得するゲッターを作成すること を忘れないでください。
public Set<Topic> getTopics() {
return this.topics;
}
双方向の関係を自動的に追跡するのは非常に困難です。したがって、JPA はこの責任を開発者に移します。これが意味するのは、Topic
とのエンティティ関係を確立するときはCategory
、データの一貫性を自分たちで確保する必要があるということです。これは次のように簡単に実行できます。
public void setCategory(Category category) {
category.getTopics().add(this);
this.category = category;
}
チェックする簡単なテストを書いてみましょう。
@Test
public void shouldPersistCategoryAndTopics() {
Category cat = new Category();
cat.setTitle("test");
Topic topic = new Topic();
topic.setTitle("topic");
topic.setCategory(cat);
em.persist(cat);
}
マッピングはまったく別のトピックです。このレビューの目的は、これを達成する手段を理解することです。マッピングの詳細については、こちらをご覧ください。
JPQL
JPA は、Java Persistence Query Language でのクエリという興味深いツールを導入しました。この言語は SQL に似ていますが、SQL テーブルではなく Java オブジェクト モデルを使用します。例を見てみましょう:@Test
public void shouldPerformQuery() {
Category cat = new Category();
cat.setTitle("query");
em.persist(cat);
Query query = em.createQuery("SELECT c from Category c WHERE c.title = 'query'");
assertNotNull(query.getSingleResult());
}
ご覧のとおり、クエリではCategory
テーブルではなくエンティティへの参照を使用しました。そして、このエンティティのフィールドにもありますtitle
。JPQL は多くの便利な機能を提供するため、独自の記事を作成する価値があります。詳細についてはレビューをご覧ください。
基準API
最後に、Criteria API について触れたいと思います。JPA は、動的クエリ構築ツールを導入します。Criteria API の使用例:@Test
public void shouldFindWithCriteriaAPI() {
Category cat = new Category();
em.persist(cat);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Category> query = cb.createQuery(Category.class);
Root<Category> c = query.from(Category.class);
query.select(c);
List<Category> resultList = em.createQuery(query).getResultList();
assertEquals(1, resultList.size());
}
この例は、リクエスト " SELECT c FROM Category c
" を実行するのと同じです。 Criteria API は強力なツールです。詳細については、ここで読むことができます。
GO TO FULL VERSION