一、简介

在本教程中,我们将讨论 JPA 实体和 Java Serialized 接口如何混合。首先,我们将了解 java.io.Serialized 接口以及为什么需要它。之后,我们将了解 JPA 规范和 Hibernate 作为其最流行的实现。

2.什么是 可串行化 接口?

Serialized 是核心 Java 中为数不多的标记接口之一。标记接口是没有方法或常量的特殊接口。

对象序列化是将 Java 对象转换为字节流的过程 。然后我们可以通过线路传输这些字节流或将它们存储在持久内存中。 反序列化是相反的过程 ,我们获取字节流并将它们转换回 Java 对象。要允许对象序列化(或反序列化),类必须实现 Serialized 接口。否则,我们将遇到 java.io.NotSerializedException序列化广泛应用于RMI、JPA、EJB等技术中

3.JPA和 可序列化

让我们看看 JPA 规范对 Serialized 有何 规定,以及它与 Hibernate 有何关系。

3.1. JPA规范

JPA 的核心部分之一是实体类。我们将此类类标记为实体(使用 @Entity 注释或 XML 描述符)。我们的实体类必须满足几个要求, 根据 JPA 规范,我们最关心的一个要求是:

如果实体实例要作为分离对象按值传递(例如,通过远程接口),则实体类必须实现 Serialized 接口。

在实践中, 如果我们的对象要离开 JVM 的域,就需要序列化

每个实体类都由持久字段和属性组成。该规范要求实体的字段可以是 Java 原语、Java 可序列化类型或用户定义的可序列化类型。

实体类还必须有主键。主键可以是原始键(单个持久字段)或复合键。多个规则适用于组合键,其中之一是 组合键需要可序列化

让我们使用 Hibernate、H2 内存数据库和以 UserId 作为组合键的 User 域对象创建一个简单的示例:

@Entity
public class User {
    @EmbeddedId UserId userId;
    String email;
    
    // constructors, getters and setters
}

@Embeddable
public class UserId implements Serializable{
    private String name;
    private String lastName;
    
    // getters and setters
}

我们可以使用集成测试来测试我们的域定义:

@Test
public void givenUser_whenPersisted_thenOperationSuccessful() {
    UserId userId = new UserId();
    userId.setName("John");
    userId.setLastName("Doe");
    User user = new User(userId, "[email protected]");

    entityManager.persist(user);

    User userDb = entityManager.find(User.class, userId);
    assertEquals(userDb.email, "[email protected]");
}

如果我们的 UserId 类没有实现 Serialized 接口,我们将得到一个 MappingException ,其中包含一条具体消息,表明我们的组合键必须实现该接口。

3.2. Hibernate @JoinColumn 注解

Hibernate官方文档在描述Hibernate中的映射时指出, 当我们使用 @JoinColumn 注释中的 referencedColumnName 时,引用的字段必须是可序列化的 。通常,该字段是另一个实体中的主键。在复杂实体类的极少数情况下,我们的引用必须是可序列化的。

让我们扩展之前的 User 类,其中 电子邮件 字段不再是 String 而是一个独立的实体。此外,我们将添加 一个 Account 类,该类将引用用户并具有字段 类型。 每个 用户 可以拥有多个不同类型的帐户。我们将通过 电子邮件 映射 帐户 ,因为通过电子邮件地址搜索更自然:

@Entity
public class User {
    @EmbeddedId private UserId userId;
    private Email email;
}

@Entity
public class Email implements Serializable {
    @Id
    private long id;
    private String name;
    private String domain;
}

@Entity
public class Account {
    @Id
    private long id;
    private String type;
    @ManyToOne
    @JoinColumn(referencedColumnName = "email")
    private User user;
}

为了测试我们的模型,我们将编写一个测试,为用户创建两个帐户并通过电子邮件对象进行查询:

@Test
public void givenAssociation_whenPersisted_thenMultipleAccountsWillBeFoundByEmail() {
    // object creation 

    entityManager.persist(user);
    entityManager.persist(account);
    entityManager.persist(account2);

    List<Account> userAccounts = entityManager.createQuery("select a from Account a join fetch a.user where a.user.email = :email", Account.class)
      .setParameter("email", email)
      .getResultList();
    
    assertEquals(userAccounts.size(), 2);
}

注意:user 是H2数据库中的保留字,不能用于实体的名称。

如果 Email 类没有实现 Serialized 接口,我们将再次收到 MappingException ,但这次会出现一条有点神秘的消息:“无法确定类型”。

3.3.将实体暴露给表示层

当使用 HTTP 通过网络发送对象时,我们通常会为此目的创建特定的 DTO(数据传输对象)。通过创建 DTO,我们将内部域对象与外部服务解耦。 如果我们想在没有 DTO 的情况下将实体直接暴露给表示层,那么实体必须是可序列化的

我们使用 HttpSession 对象来存储相关数据,帮助我们在多个页面访问我们的网站时识别用户。 Web 服务器可以在正常关闭时将会话数据存储在磁盘上,或者将会话数据传输到集群环境中的另一个 Web 服务器。如果实体是此过程的一部分,那么它必须是可序列化的。否则,我们将遇到 NotSerializedException

4。结论

在本文中,我们介绍了 Java 序列化的基础知识,并了解了它如何在 JPA 中发挥作用。首先,我们回顾了 JPA 规范中有关 Serialized 的 要求。之后,我们将 Hibernate 视为最流行的 JPA 实现。最后,我们介绍了 JPA 实体如何与 Web 服务器配合使用。

与往常一样,本文中提供的所有代码都可以在 GitHub 上找到。