JavaRush /Java Blog /Random-TW /JPA:技術介紹
Viacheslav
等級 3

JPA:技術介紹

在 Random-TW 群組發布
現代開發世界充滿了各種旨在讓生活更輕鬆的規範。了解了這些工具,您就可以選擇合適的工具。如果你不知道,你的生活就會變得更加困難。這篇評論將揭開 JPA(Java Persistence API)概念的神秘面紗。我希望您讀完後會想要更深入地探索這個神秘的世界。
JPA:技術簡介 - 1

介紹

眾所周知,程式的主要任務之一是儲存和處理資料。在過去的美好時光,人們只是將資料儲存在文件中。但是,一旦需要同時進行讀取和編輯訪問,當存在負載時(即多個請求同時到達),將資料簡單地儲存在文件中就成為問題。有關資料庫解決什麼問題以及如何解決問題的更多信息,我建議您閱讀文章“資料庫的結構”。這意味著我們決定將資料儲存在資料庫中。長期以來,Java 一直能夠使用 JDBC API(Java 資料庫連線)來處理資料庫。您可以在此處閱讀有關 JDBC 的更多信息:“ JDBC 或一切的開始。” 但隨著時間的推移,開發人員每次都需要編寫相同類型和不必要的「維護」程式碼(所謂的樣板程式碼),以執行在資料庫中保存Java 物件的瑣碎操作,反之亦然,使用來自資料庫的資料建立Java 物件。資料庫。於是,為了解決這些問題,ORM這樣的概念就誕生了。 ORM-物件關係映射或翻譯成俄語物件關係映射。它是一種將資料庫與物件導向程式語言的概念連結起來的程式設計技術。簡單來說,ORM 是 Java 物件和資料庫中的記錄之間的連接: JPA:技術簡介 - 2ORM 本質上是 Java 物件可以表示為資料庫中的資料(反之亦然)的概念。它以 JPA 規格——Java Persistence API 的形式體現。這個規範已經是表達這一概念的 Java API 的描述。該規範告訴我們必須提供哪些工具(即我們可以透過哪些介面進行工作)才能根據 ORM 概念進行工作。以及如何使用這些資金。該規範沒有描述工具的實作。這使得對一種規範使用不同的實現成為可能。你可以簡化它,說規範是 API 的描述。JPA規範的文字可以在Oracle網站上找到:「JSR 338:JavaTM Persistence API」。因此,為了使用 JPA,我們需要一些使用該技術的實作。JPA 實作也稱為 JPA 提供者。Hibernate是最著名的 JPA 實作之一。因此,我建議考慮一下。
JPA:技術簡介 - 3

創建專案

由於 JPA 是關於 Java 的,因此我們需要一個 Java 專案。我們可以自己手動建立目錄結構並自己添加必要的庫。但使用系統來自動化專案組裝要方便和正確得多(即,本質上,這只是一個為我們管理專案組裝的程序。創建目錄,將必要的庫添加到類路徑等) .)。Gradle 就是這樣的系統之一。您可以在這裡閱讀有關 Gradle 的更多資訊:「Gradle 簡介」。眾所周知,Gradle 功能(即它可以做的事情)是使用各種 Gradle 插件來實現的。讓我們使用 Gradle 和「Gradle Build Init Plugin」插件。讓我們運行命令:

gradle init --type java-application
Gradle 將為我們建立必要的目錄結構,並在建置腳本中建立專案的基本聲明性描述build.gradle。所以,我們有一個應用程式。我們需要考慮我們想要用我們的應用程式描述或建模什麼。讓我們使用一些建模工具,例如:app.quickdatabasediagrams.com JPA:技術簡介 - 4這裡值得一提的是,我們所描述的是我們的「領域模型」。域是一個「主題領域」。一般來說,「domain」在拉丁文中是「佔有」的意思。在中世紀,這是國王或封建領主擁有的地區的名稱。在法語中,它變成了“domaine”這個詞,簡單地翻譯為“區域”。因此,我們描述了我們的“領域模型”=“主題模型”。這個模型的每個元素都是某種“本質”,來自現實生活的東西。在我們的例子中,這些是實體:類別 ( 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:技術簡介 - 5

添加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 工作,您需要在配置文件中描述至少一個持久單元,稱為persistence.xml. 其位置在規範章節「8.2 持久性單元包裝」中描述。根據本節,如果我們有Java SE環境,那麼我們必須將其放在根目錄的META-INF目錄中。
JPA:技術簡介 - 6
讓我們從「JPA規範」章節中給出的範例複製內容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 屬性」部分中進行了描述。一些配置是特定於提供者的(在我們的例子中,它們影響 Hibernate 作為 Jpa 提供者。我們的設定區塊將如下所示:
<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 相容的配置persistence.xml,有一個 JPA 提供者 Hibernate,有一個 H2 資料庫,還有 2 個類別是我們的領域模型。讓我們最終讓這一切發揮作用。在目錄中/test/java,我們的 Gradle 友好地產生了一個用於單元測試的模板,並將其命名為 AppTest。讓我們使用它。正如 JPA 規範的「7.1 持久性上下文」一章中所述,JPA 世界中的實體存在於稱為持久性上下文的空間中。但我們不直接使用持久性上下文。為此,我們使用Entity Manager“實體管理器”。他知道上下文以及那裡有什麼實體。我們與「om」互動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();
}
此測試已經顯示錯誤「無法辨識的 JPA persistence.xml XSD 版本」。原因是persistence.xml您需要正確指定要使用的模式,如“8.3 persistence.xml Schema”部分中的 JPA 規範中所述:
<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
JPA:技術簡介 - 7

實體

我們記得,我們​​之前創建了描述域模型的類別。我們已經說過,這些是我們的「本質」。這是我們將使用 來管理的實體EntityManager。讓我們寫一個簡單的測試來保存類別的本質:
@Test
public void shouldPersistCategory() {
	Category cat = new Category();
	cat.setTitle("new category");
	// JUnit обеспечит тест свежим EntityManager'ом
	em.persist(cat);
}
但這個測試不會立即起作用,因為… 我們將收到各種錯誤,這將幫助我們了解實體是什麼:
  • Unknown entity: hibernate.model.Category
    為什麼 Hibernate 不明白Category這是什麼entity?問題是實體必須根據 JPA 標準進行描述。
    實體類別必須使用annotation進行註釋@Entity,如JPA規範的「2.1實體類別」一章所述。

  • 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.3GenerateValue」和「11.1.20GenerateValue註解」章節所示,我們可以指定註解@GeneratedValue

因此,為了使類別類別成為一個實體,我們必須進行以下變更:
@Entity
public class Category {
    @Id
    @GeneratedValue
    private Long id;
另外,註釋也@Id指出了要使用哪一個Access Type。您可以在 JPA 規範的「2.3 存取類型」部分中閱讀有關存取類型的更多資訊。簡而言之,因為... 我們@Id在欄位 ( field) 上面指定了,那麼存取類型將為 default field-based,而不是property-based。因此,JPA 提供者將直接從欄位讀取並儲存值。如果我們放置@Id在 getter 之上,那麼property-based將使用存取權限,即 透過 getter 和 setter。執行測試時,我們還可以看到哪些請求發送到資料庫(感謝選項hibernate.show_sql)。但是保存時,我們看不到任何insert。原來我們其實什麼都沒保存?JPA 允許您使用以下方法同步持久性上下文和資料庫flush
entityManager.flush();
但如果我們現在執行,我們會得到一個錯誤:no transaction is inprogress。現在是時候了解 JPA 如何使用交易了。
JPA:技術簡介 - 8

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 表達式:
JPA:技術簡介 - 9
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 的類別實體。” 實體管理器首先在其上下文中尋找(使用一種快取),只有當沒有找到時,它才會在資料庫中尋找。值得將ID更改為2(沒有這樣的事情,我們只保存了1個實例),我們將看到SELECT請求出現。因為在上下文中沒有找到實體,且EntityManager資料庫正在嘗試尋找實體,所以我們可以使用不同的命令來控制上下文中實體的狀態。實體從一種狀態到另一種狀態的轉變稱為實體的生命週期lifecycle
JPA:技術簡介 - 10

實體生命週期

實體的生命週期在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:技術簡介 - 11
JPA:技術簡介 - 12

測繪

在JPA中我們可以描述實體之間的關係。讓我們記住,在處理領域模型時,我們已經研究了實體之間的關係。然後我們使用了quickdatabasediagrams.com資源:
JPA:技術簡介 - 13
實體之間建立連線稱為映射或關聯(Association Mappings)。下面列出了可以使用 JPA 建立的關聯類型:
JPA:技術簡介 - 14
Topic讓我們來看看描述主題的實體。Topic對於態度我們能說什麼Category?許多人Topic都屬於一個類別。因此,我們需要一個協會ManyToOne。讓我們在 JPA 中表達這種關係:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
要記住要放置哪些註釋,您可以記住最後一部分負責指示註釋的欄位。ToOne- 具體實例。ToMany- 收藏。現在我們的連結是單向的。讓我們將其設為雙向通訊。讓我們添加有關此類別中的Category每個人的知識。Topic它必須以 結尾ToMany,因為我們有一個清單Topic。即「對很多」話題的態度。問題仍然是 -OneToManyManyToMany
JPA:技術簡介 - 15
關於同一主題的一個很好的答案可以在這裡閱讀:「像我五歲一樣解釋 ORM oneToMany、manyToMany 關係」。如果一個類別與主題有聯繫ToMany,那麼每個主題只能有一個類別,則為One,否則為Many。所以Category所有主題的清單將如下所示:
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
我們不要忘記本質上Category寫一個 getter 來獲取所有主題的清單:
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);
}
映射是一個完全獨立的主題。作為本次審查的一部分,值得了解這是透過什麼方式實現的。您可以在此處閱讀有關映射的更多資訊:
JPA:技術簡介 - 16

JPQL

JPA 引入了一個有趣的工具 - Java 持久性查詢語言中的查詢。這種語言與 SQL 類似,但使用 Java 物件模型而不是 SQL 表。讓我們來看一個例子:
@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 提供了許多有用的功能,值得單獨撰寫一篇文章。更多詳細資訊可以在評論中找到:
JPA:技術簡介 - 17

標準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是一個強大的工具。你可以在這裡讀更多關於它的內容:

結論

正如我們所看到的,JPA 提供了大量的功能和工具。他們每個人都需要經驗和知識。即使在 JPA 審查的框架內,也不可能提及所有內容,更不用說詳細的深入探討了。但我希望讀完它之後,能夠更清楚什麼是 ORM 和 JPA、它是如何運作的以及可以用它做什麼。好吧,對於零食我提供了各種材料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION