【问题标题】:Create the perfect JPA entity [closed]创建完美的 JPA 实体 [关闭]
【发布时间】:2011-08-27 09:28:43
【问题描述】:

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

实体类

  • 实现可序列化

    原因:规范说你必须这样做,但是一些 JPA 提供者并没有强制执行。 Hibernate 作为 JPA 提供者不会强制执行此操作,但如果尚未实现 Serializable,它可能会因 ClassCastException 而失败。

构造函数

  • 使用实体的所有必需字段创建构造函数

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

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

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

字段/属性

  • 一般使用字段访问权限,并在需要时使用属性访问权限

    原因:这可能是最有争议的问题,因为其中一个没有明确和令人信服的论据(财产访问与字段访问);然而,由于更清晰的代码、更好的封装以及无需为不可变字段创建设置器,字段访问似乎受到普遍欢迎

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

  • 属性可能是私有的
    原因:我曾经听说 protected 对(Hibernate)性能更好,但我在网上只能找到:Hibernate 可以直接访问公共、私有和受保护的访问器方法,以及公共、私有和受保护的字段。选择权由您决定,您可以根据自己的应用设计进行匹配。

等于/hashCode

  • 如果此 id 仅在持久化实体时设置,则切勿使用生成的 id
  • 根据偏好:使用不可变的值来形成唯一的业务密钥并使用它来测试相等性
  • 如果唯一的业务密钥不可用,则使用在初始化实体时创建的非瞬态 UUID;请参阅this great article 了解更多信息。
  • 从不引用相关实体 (ManyToOne);如果该实体(如父实体)需要成为业务密钥的一部分,则仅比较 ID。只要您使用的是property access type,在代理上调用 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

}

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

更新

自从阅读this article 以来,我已经调整了实现 eq/hC 的方式:

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

【问题讨论】:

  • 这不是一个问题,它是一个审查请求和一个列表请求。此外,它非常开放和模糊,或者换一种说法:JPA 实体是否完美取决于它的用途。我们是否应该列出一个实体在所有可能的实体使用中可能需要的所有东西?
  • 我知道这不是一个明确的问题,对此我深表歉意。这实际上不是对列表的请求,而是对 cme​​ts/remarks 的请求,尽管欢迎其他建议。随意详细说明 JPA 实体的可能用途。
  • 我还希望字段为final(根据您省略设置器来判断,我猜您也是)。
  • 必须尝试一下,但我认为 final 不会起作用,因为 Hibernate 仍然需要能够设置这些属性的值。
  • notNull 来自哪里?

标签: java hibernate jpa equals


【解决方案1】:

JPA 2.0 Specification 声明:

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

规范对实体的equals和hashCode方法的实现没有要求,据我所知仅针对主键类和映射键。

【讨论】:

  • True、equals、hashcode、... 不是 JPA 要求,但当然是推荐并认为是好的做法。
  • @TheStijn 好吧,除非您打算比较分离实体的相等性,否则这可能是不必要的。每次您请求时,实体管理器都会保证返回给定实体的相同实例。因此,据我所知,您可以对托管实体进行身份比较。您能否详细说明一下您认为这是一种良好做法的场景?
  • 我力求始终正确实现 equals/hashCode。 JPA 不需要,但我认为当实体或添加到集合时这是一个很好的做法。您可以决定仅在将实体添加到 Set 时才实现 equals,但您是否总是提前知道?
  • @TheStijn JPA 提供者将确保在任何给定时间上下文中只有一个给定实体的实例,因此即使您的集合在不实现 equals/hascode 的情况下也是安全的,前提是您只使用托管实体。为实体实现这些方法并非没有困难,例如,看看这个Hibernate Article 关于这个主题。我的观点是,如果您只使用托管实体,则最好不使用它们,否则请提供非常谨慎的实现。
  • @TheStijn 这是一个很好的混合场景。正如您最初建议的那样,它证明了实现 eq/hC 的必要性,因为一旦实体放弃了持久层的安全性,您就不能再信任 JPA 标准强制执行的规则。在我们的例子中,DTO 模式从一开始就在架构上强制执行。通过设计,我们的持久性 API 不提供与业务对象交互的公共方式,仅提供一个使用 DTO 与我们的持久层交互的 API。
【解决方案2】:

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

实体类:实现可序列化?

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

构造函数:使用实体的所有必填字段创建构造函数?

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

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

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

构造函数:还有,封装私有默认构造函数?

是的,但使用“受保护”而不是包私有。当必要的内部结构不可见时,子类化是一件非常痛苦的事情。

字段/属性

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

至于应用程序的“不可变”字段——Hibernate 仍然需要能够加载这些字段。您可以尝试将这些方法设为“私有”,和/或在其上添加注释,以防止应用程序代码进行不必要的访问。

注意:在编写 equals() 函数时,使用 getter 获取“其他”实例上的值!否则,您将在代理实例上遇到未初始化/空字段。

受保护的(休眠)性能更好?

不太可能。

等于/哈希码?

这与在保存实体之前处理实体有关——这是一个棘手的问题。对不可变值进行散列/比较?在大多数业务应用程序中,没有。

客户可以更改地址、更改公司名称等 - 不常见,但确实会发生。当数据输入不正确时,也需要进行更正。

通常保持不可变的几件事是 Parenting,也许是 Type/Kind——通常用户重新创建记录,而不是更改这些记录。但这些并不能唯一标识实体!

因此,总而言之,声称的“不可变”数据并不是真的。生成主键/ ID 字段的确切目的是提供这种有保证的稳定性和不变性。

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

Equals/HashCode -- 如果唯一的业务密钥不可用,则使用初始化实体时创建的非瞬态 UUID

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

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

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

听起来不错。

希望这会有所帮助!

【讨论】:

  • Re:构造函数,我经常只看到零参数(即没有),并且调用代码有很长的设置器列表,这对我来说似乎有点混乱。拥有几个适合您需要的构造函数,使调用代码更简洁,真的有什么问题吗?
  • 完全固执己见,特别是关于 ctor。什么是更漂亮的代码?一堆不同的 ctor,让您知道需要哪些(组合)值来创建 obj 的健全状态或无参数 ctor,它不知道应该设置什么以及以什么顺序,并且容易出现用户错误?
  • @mohamnag 取决于。对于内部系统生成的数据,严格有效的 bean 很棒;然而,现代商业应用程序由大量用户数据输入 CRUD 或向导屏幕组成。至少在编辑过程中,用户输入的数据通常是部分格式或格式不正确的。很多时候,能够记录不完整的状态以供以后完成甚至具有商业价值——想想保险应用程序捕获、客户注册等。将约束保持在最低限度(例如主键、业务键和状态)允许更大的实际灵活性业务情况。
  • @ThomasW 首先我不得不说,我对领域驱动设计和使用名称作为类名和方法的完整动词有强烈的看法。在这个范例中,您所指的实际上是 DTO,而不是应该用于临时数据存储的域实体。或者您只是误解了/-结构化您的域。
  • @ThomasW 当我过滤掉你想说我是新手的所有句子时,除了用户输入之外,你的评论中没有任何信息。正如我之前所说,这部分应在 DTO 中完成,而不是直接在实体中完成。让我们再谈 50 年,你可能会成为像 Fowler 那样的 DDD 背后的大人物所经历的 5%!欢呼:D
【解决方案3】:

我的 2 美分加上这里的答案是:

  1. 关于字段或属性访问(远离性能考虑)都可以通过 getter 和 setter 合法访问,因此,我的模型逻辑可以以相同的方式设置/获取它们。 当持久性运行时提供程序(Hibernate、EclipseLink 或其他)需要在表 A 中持久化/设置某些记录时,差异就会出现,该记录具有引用表 B 中某些列的外键。在属性访问类型的情况下,持久性运行时系统使用我的编码设置器方法为表 B 列中的单元格分配一个新值。对于 Field 访问类型,持久化运行时系统直接设置表 B 列中的单元格。 这种差异在单向关系的上下文中并不重要,但必须使用我自己的编码 setter 方法(属性访问类型)来实现双向关系,前提是 setter 方法设计得很好以考虑一致性.一致性是双向关系的一个关键问题,请参阅link,了解设计良好的 setter 的简单示例。

  2. 关于 Equals/hashCode 的引用:对于参与双向关系的实体,不可能使用 Eclipse 自动生成的 Equals/hashCode 方法,否则它们会有循环引用导致 stackoverflow 异常。一旦你尝试双向关系(比如 OneToOne)并自动生成 Equals() 或 hashCode() 甚至 toString(),你就会陷入这个 stackoverflow 异常。

【讨论】:

    【解决方案4】:

    实体接口

    public interface Entity<I> extends Serializable {
    
    /**
     * @return entity identity
     */
    I getId();
    
    /**
     * @return HashCode of entity identity
     */
    int identityHashCode();
    
    /**
     * @param other
     *            Other entity
     * @return true if identities of entities are equal
     */
    boolean identityEquals(Entity<?> other);
    }
    

    所有实体的基本实现,简化 Equals/Hashcode 实现:

    public abstract class AbstractEntity<I> implements Entity<I> {
    
    @Override
    public final boolean identityEquals(Entity<?> other) {
        if (getId() == null) {
            return false;
        }
        return getId().equals(other.getId());
    }
    
    @Override
    public final int identityHashCode() {
        return new HashCodeBuilder().append(this.getId()).toHashCode();
    }
    
    @Override
    public final int hashCode() {
        return identityHashCode();
    }
    
    @Override
    public final boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if ((o == null) || (getClass() != o.getClass())) {
            return false;
        }
    
        return identityEquals((Entity<?>) o);
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() + ": " + identity();
        // OR 
        // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
    }
    

    房间实体实现:

    @Entity
    @Table(name = "ROOM")
    public class Room extends AbstractEntity<Integer> {
    
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @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;
    }
    
    public Integer getId(){
        return id;
    }
    
    public Building getBuilding() {
        return building;
    }
    
    public String getNumber() {
        return number;
    }
    
    
    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }
    
    //no setters for number, building nor id
    }
    

    在 JPA 实体的每种情况下,我都认为没有必要根据业务领域来比较实体的相等性。如果这些 JPA 实体被认为是域驱动的值对象,而不是域驱动的实体(这些代码示例是针对这些实体的),那么这种情况可能更多。

    【讨论】:

    • 虽然使用父实体类取出样板代码是一个好方法,但在 equals 方法中使用 DB 定义的 id 不是一个好主意。在您的情况下,比较 2 个新实体甚至会引发 NPE。即使您将其设为 null 安全,那么 2 个新实体也将始终相等,直到它们被持久化。 Eq/hC 应该是不可变的。
    • Equals() 不会抛出 NPE,因为会检查 DB id 是否为 null,如果 DB id 为 null,则相等性为 false。
    • 确实,我不明白我是如何错过代码是空安全的。但是 IMO 使用 id 仍然是不好的做法。参数:onjava.com/pub/a/onjava/2006/09/13/…
    • 在 Vaughn Vernon 的《实施 DDD》一书中,有人认为如果使用“早期 PK 生成”(先生成一个 id 并将其传递给实体而不是让数据库在您持久化实体时生成 id。)
    • 或者如果你不打算平等比较非持久性实体?你为什么要...
    猜你喜欢
    • 2018-04-13
    • 2013-08-30
    • 1970-01-01
    • 1970-01-01
    • 2019-03-02
    • 1970-01-01
    • 2021-01-13
    • 2017-04-04
    • 1970-01-01
    相关资源
    最近更新 更多