JavaRush /Java 博客 /Random-ZH /JPA:技术介绍
Viacheslav
第 3 级

JPA:技术介绍

已在 Random-ZH 群组中发布
现代开发世界充满了旨在让生活更轻松的各种规范。了解了这些工具,您就可以选择合适的工具。如果你不知道,你的生活就会变得更加困难。这篇评论将揭开 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