介绍
众所周知,程序的主要任务之一是存储和处理数据。在过去的美好时光,人们只是将数据存储在文件中。但是,一旦需要同时进行读取和编辑访问,当存在负载时(即多个请求同时到达),将数据简单地存储在文件中就成为问题。有关数据库解决什么问题以及如何解决问题的更多信息,我建议您阅读文章“
数据库的结构
”。这意味着我们决定将数据存储在数据库中。长期以来,Java 一直能够使用 JDBC API(Java 数据库连接)来处理数据库。您可以在此处阅读有关 JDBC 的更多信息:“
JDBC 或一切的开始
。” 但随着时间的推移,开发人员每次都需要编写相同类型和不必要的“维护”代码(所谓的样板代码),以执行在数据库中保存 Java 对象的琐碎操作,反之亦然,使用来自数据库的数据创建 Java 对象。数据库。于是,为了解决这些问题,ORM这样的概念就诞生了。
ORM——对象关系映射或翻译成俄语对象关系映射。
它是一种将数据库与面向对象编程语言的概念联系起来的编程技术。简单来说,ORM 是 Java 对象和数据库中的记录之间的连接:
![JPA:技术简介 - 2]()
ORM 本质上是 Java 对象可以表示为数据库中的数据(反之亦然)的概念。它以 JPA 规范——Java Persistence API 的形式体现。该规范已经是表达这一概念的 Java API 的描述。该规范告诉我们必须提供哪些工具(即我们可以通过哪些接口进行工作)才能根据 ORM 概念进行工作。以及如何使用这些资金。该规范没有描述工具的实现。这使得对一种规范使用不同的实现成为可能。你可以简化它,说规范是 API 的描述。JPA规范的文本可以在Oracle网站上找到:“
JSR 338:JavaTM Persistence API
”。因此,为了使用 JPA,我们需要一些使用该技术的实现。JPA 实现也称为 JPA 提供程序。
Hibernate
是最著名的 JPA 实现之一。因此,我建议考虑一下。
创建项目
由于 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 项目了。
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规范”章节中给出的示例中复制内容
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
。
实体
我们记得,我们之前创建了描述域模型的类。我们已经说过,这些是我们的“本质”。这是我们将使用 来管理的实体
EntityManager
。让我们编写一个简单的测试来保存类别的本质:
@Test
public void shouldPersistCategory() {
Category cat = new Category();
cat.setTitle("new category");
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 {
@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 交易
我们记得,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 的类别实体。” 实体管理器首先在其上下文中查找(使用一种缓存),只有当没有找到时,它才会在数据库中查找。值得将ID更改为2(没有这样的事情,我们只保存了1个实例),我们将看到
SELECT
请求出现。因为在上下文中没有找到实体,并且
EntityManager
数据库正在尝试查找实体,所以我们可以使用不同的命令来控制上下文中实体的状态。实体从一种状态到另一种状态的转变称为实体的生命周期
lifecycle
。
实体生命周期
实体的生命周期在JPA规范的“3.2实体实例的生命周期”一章中描述。因为 实体生活在一个上下文中并受 控制
EntityManager
,那么他们说实体受到控制,即 管理。让我们看一下实体生命的各个阶段:
Category cat = new Category();
cat.setTitle("new category");
entityManager.persist(cat);
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
entityManager.detach(cat);
Category managed = entityManager.merge(cat);
entityManager.remove
(managed);
这是一个巩固它的图表:
Topic
让我们看一下描述主题的实体。
Topic
对于态度我们能说什么
Category
?许多人
Topic
都属于一个类别。因此,我们需要一个协会
ManyToOne
。让我们在 JPA 中表达这种关系:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
关于同一主题的一个很好的答案可以在这里阅读:“
像我五岁一样解释 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 和 Hibernate 的关联映射
”。
《
JPA 和对象之间的关系
》
“
Hibernate 中的相关实体
”
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 和 Hibernate 进行 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
是一个强大的工具。你可以在这里读更多关于它的内容:
JPA 标准 API 查询
JPA标准
JPA 2.0 中的动态类型安全查询
结论
正如我们所看到的,JPA 提供了大量的功能和工具。他们每个人都需要经验和知识。即使在 JPA 审查的框架内,也不可能提及所有内容,更不用说详细的深入探讨了。但我希望读完它之后,能够更清楚什么是 ORM 和 JPA、它们如何工作以及可以用它们做什么。好吧,对于零食我提供了各种材料:
JPA - 简介
关于 Java 的思考 - Hibernate 初学者
关于 Java 的思考 - Hibernate 技巧
高级 JPA 主题
#维亚切斯拉夫
JavaRush 是一个从零开始学习 Java 语言编程的在线课程。本课程是初学者掌握 Java 语言的绝佳方式。它包含 1200 多个可即时验证的任务,以及基本范围内的 Java 基础理论。为了帮助你在教育上取得成功,我们实现了一组激励功能:小测验、编码项目以及有关高效学习和 Java 语言开发人员职业方面的内容。