创建完美的 JPA 实体 [已关闭]

2022-08-31 04:31:01

我已经使用JPA(实现Hibernate)一段时间了,每次我需要创建实体时,我发现自己都在为AccessType,不可变属性,equals/hashCode等问题而苦苦挣扎。
因此,我决定尝试找出每个问题的一般最佳实践,并将其写下来供个人使用。
然而,我不介意任何人对此发表评论或告诉我我错在哪里。

实体类

  • 实现可序列化

    原因:规范说你必须这样做,但一些JPA提供者不强制执行。作为JPA提供者的Hibernate不会强制执行这一点,但是如果Sscribializable尚未实现,它可能会在ClassCastException的某个地方失败。

构造 函数

  • 创建一个包含实体所有必填字段的构造函数

    原因:构造函数应始终使创建的实例保持正常状态。

  • 除了这个构造函数:有一个包私有默认构造函数

    原因:需要默认构造函数才能让休眠初始化实体;允许 private,但包私有(或公共)可见性对于运行时代理生成和高效数据检索(无需字节码检测)是必需的。

字段/属性

  • 通常使用字段访问,并在需要时使用属性访问

    原因:这可能是最有争议的问题,因为没有明确和令人信服的论据(财产访问与现场访问);但是,字段访问似乎是普遍的最爱,因为代码更清晰,封装更好,并且不需要为不可变字段创建 setter

  • 省略不可变字段的 setter(访问类型字段不需要)

  • 属性可能是私有
    原因:我曾经听说过受保护更适合(休眠)性能,但我在网络上能找到的只是:休眠可以直接访问公共,私有和受保护的访问器方法,以及公共,私有和受保护的字段。选择取决于您,您可以匹配它以适合您的应用程序设计。

等于/哈希代码

  • 如果仅在持久化实体时设置了此 ID,则切勿使用生成的 id
  • 按首选项:使用不可变值形成唯一的业务密钥,并使用它来测试相等性
  • 如果唯一的业务密钥不可用,请使用在实体初始化时创建的非瞬态 UUID;有关详细信息,请参阅这篇精彩文章
  • 从不提及相关实体(ManyToOne);如果此实体(如父实体)需要成为业务密钥的一部分,则仅比较 ID。在代理上调用 getId() 不会触发实体的加载,只要您使用的是属性访问类型

示例实体

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

其他建议添加到此列表中非常受欢迎...

更新

自从阅读本文以来,我调整了实现 eq/hC 的方式:

  • 如果一个不可变的简单业务密钥可用:使用
  • 在所有其他情况下:使用 uuid

答案 1

JPA 2.0 规范规定:

  • 实体类必须具有无参数构造函数。它可能还有其他构造函数。无参数构造函数必须是公共的或受保护的。
  • 实体类必须是顶级类。枚举或接口不得指定为实体。
  • 实体类不能是最终的。实体类的任何方法或持久性实例变量都不能是最终的。
  • 如果要将实体实例作为分离对象(例如,通过远程接口)按值传递,则实体类必须实现可序列化接口。
  • 抽象类和具体类都可以是实体。实体可以扩展非实体类以及实体类,非实体类可以扩展实体类。

据我所知,该规范不包含有关实体的 equals 和 hashCode 方法实现的要求,仅包含对主键类和映射键的要求。


答案 2

我将尝试回答几个关键点:这是来自长期的Hibernate/持久性经验,包括几个主要应用程序。

实体类:实现可序列化?

密钥需要实现可序列化。将要进入HttpSession或由RPC / Java EE通过网络发送的东西需要实现Serializable。其他东西:没有那么多。把你的时间花在重要的事情上。

构造函数:创建一个包含实体所有必填字段的构造函数?

应用程序逻辑的构造函数应该只有几个关键的“外键”或“类型/种类”字段,这些字段在创建实体时始终是已知的。其余的应该通过调用 setter 方法来设置 - 这就是它们的用途。

避免在构造函数中放入太多字段。构造函数应该方便,并为对象提供基本的理智。姓名,类型和/或父母通常都是有用的。

OTOH如果应用程序规则(今天)要求客户拥有地址,请将其留给设置者。这是“弱规则”的一个例子。也许下周,您想在转到“输入详细信息”屏幕之前创建一个 Customer 对象?不要绊倒自己,为未知,不完整或“部分输入”的数据留出可能性。

构造函数:另外,包私有默认构造函数?

是的,但使用“受保护”而不是包私有。当必要的内部不可见时,子类化的东西是一个真正的痛苦。

字段/属性

对休眠使用“属性”字段访问,并从实例外部访问。在实例中,直接使用这些字段。原因:允许标准反射(最简单和最基本的Hibernate方法)工作。

至于应用程序的“不可变”字段 - 休眠仍然需要能够加载这些字段。您可以尝试将这些方法设置为“私有”,和/或对它们进行注释,以防止应用程序代码进行不必要的访问。

注意:编写 equals() 函数时,对 “other” 实例上的值使用 getters!否则,您将在代理实例上命中未初始化/空字段。

受保护是否更适合(休眠)性能?

不可能。

等于/哈希码?

这与在实体被保存之前与实体合作有关 - 这是一个棘手的问题。对不可变值进行哈希/比较?在大多数业务应用程序中,没有任何应用程序。

客户可以更改地址,更改其业务名称等 - 不常见,但它确实发生了。当数据输入不正确时,也需要进行更正。

通常保持不可变的少数事情是育儿,也许是类型/种类 - 通常用户重新创建记录,而不是更改这些记录。但这些并不能唯一地识别实体!

因此,无论长短,声称的“不可变”数据并不是真的。生成主键/ ID字段是为了提供这种有保证的稳定性和不可变性的确切目的。

您需要计划并考虑比较和散列以及请求处理工作阶段的需求,A)如果您比较/散列“不经常更改的字段”,则从UI处理“已更改/绑定的数据”,或者B)使用“未保存的数据”,如果您在ID上进行比较/散列。

等于/哈希代码 -- 如果唯一的业务密钥不可用,请使用在初始化实体时创建的非瞬态 UUID

是的,在需要时这是一个很好的策略。请注意,UUID在性能方面并非免费 - 并且群集使事情复杂化。

Equals/HashCode -- 从不引用相关实体

“如果相关实体(如父实体)需要成为业务密钥的一部分,则添加一个不可插入的、不可更新的字段来存储父 ID(与 ManytoOne JoinColumn 同名),并在相等性检查中使用此 ID”

听起来像是一个很好的建议。

希望这有帮助!


推荐