创build完美的JPA实体

我一直在使用JPA(实现Hibernate)一段时间,每次我需要创build实体时,我发现自己正在与AccessType,不可变属性,equals / hashCode …等问题挣扎。
所以我决定尝试找出每个问题的一般最佳做法,并写下来供个人使用。
我不介意任何人对此发表评论,或者告诉我我错在哪里。

实体类

  • 实现Serializable

    原因: 规范说你必须,但是一些JPA提供者不执行这个。 作为JPA提供程序的Hibernate不强制执行此操作,但如果尚未实现Serializable,则它可能会在ClassCastException的深处发生故障。

构造函数

  • 用实体的所有必填字段创build一个构造函数

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

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

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

字段/属性

  • 一般情况下使用字段访问,需要时使用属性访问

    理由:这可能是最值得商榷的问题,因为没有一个明确而有说服力的论据(财产准入和实地准入); 然而,由于代码更清晰,封装更好,并且不需要为不可变字段创buildsetter,所以字段访问似乎是最受欢迎的

  • 省略set不可变字段(访问types字段不需要)

  • 物业可能是私人的
    原因:我曾经听说受保护对于(Hibernate)性能更好,但是我可以在网上find的是: Hibernate可以直接访问公共,私有和受保护的访问方法以及公共,私有和受保护的字段。 这个select取决于你,你可以匹配它来适应你的应用程序devise。

等于/的hashCode

  • 如果仅在持久化实体时才设置此ID,请勿使用生成的ID
  • 优先:使用不可变的值形成一个唯一的业务密钥,并用它来testing相等性
  • 如果唯一的业务密钥不可用,则使用在初始化实体时创build的非暂时性UUID ; 看到这个伟大的文章更多的信息。
  • 从不涉及相关实体(ManyToOne); 如果此实体(如父实体)需要成为业务密钥的一部分,则仅比较该ID。 只要你使用属性访问types ,在代理上调用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 } 

其他build议添加到此列表不只是欢迎…

UPDATE

由于阅读这篇文章,我已经适应了我实施eq / hC的方式:

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

JPA 2.0规范指出:

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

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

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

实体类:实现Serializable?

需要实现Serializable。 那些要进入HttpSession的东西,或者通过RPC / Java EE发送的东西,需要实现Serializable。 其他的东西:不是很多。 把时间花在重要的事情上。

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

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

避免将太多的字段放入构造函数中。 施工人员应该方便,并给予对象基本的理智。 姓名,types和/或父母通常都是有用的。

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

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

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

字段/属性

对Hibernate使用'property'字段访问,并从实例外部使用。 在实例中,直接使用这些字段。 原因:允许标准的reflection,最简单和最基本的方法为Hibernate工作。

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

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

受保护对于(Hibernate)性能更好?

不太可能。

等于/的hashCode?

这与在实体保存之前一起工作有关 – 这是一个棘手的问题。 散列/比较不可变的值? 在大多数商业应用程序中,没有任何。

客户可以改变地址,改变他们的业务名称等等 – 不常见,但是它发生了。 当数据input不正确时,还需要进行更正。

通常保持不变的less数事情是父母,也许是types/种类 – 通常用户重新创buildlogging,而不是改变这些。 但是这些并不能唯一地标识实体!

所以,总之,所谓的“不变的”数据并不是真的。 主密钥/ ID字段是为确切的目的而产生的,从而提供这种有保证的稳定性和不变性。

你需要计划和考虑你需要比较和散列和请求处理的工作阶段:A)如果你比较/散列“不经常更改的字段”,那么在UI中使用“更改/绑定数据”;或者B)使用“未保存的数据“,如果你比较/哈希ID。

Equals / HashCode – 如果唯一的业务密钥不可用,请使用在初始化实体时创build的非暂时性UUID

是的,在需要的时候这是一个很好的策略。 请注意,虽然UUID不是免费的,但性能明智 – 集群使事情变得复杂。

Equals / HashCode – 永远不会引用相关的实体

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

听起来很好的build议。

希望这可以帮助!

在对Stijns半综合名单表示赞赏之后,2个更正是:

  1. 关于Field或Property访问(远离性能方面的考虑),都是通过getter和setter方法合法访问的,因此,我的模型逻辑可以用相同的方式设置/获取它们。 当持久化运行时提供者(Hibernate,EclipseLink或者其他)需要在表A中持久化/设置一个具有引用表B中某个列的外键的logging时,这种区别就起到了作用。在属性访问types的情况下,持久性运行时系统使用我的编码设置器方法为表B列中的单元格分配一个新值。 在字段访问types的情况下,持久性运行时系统直接在表B列中设置单元格。 这种差异在单向关系的上下文中并不重要,但是如果设置方法的devise合理,则必须使用我自己的编码设置器方法(属性访问types)进行双向关系。 。 一致性是双向关系的一个关键问题,请参阅此链接 ,以获得devise良好的二传手的简单示例。

  2. 参考Equals / hashCode:对于参与双向关系的实体,使用Eclipse自动生成的Equals / hashCode方法是不可能的,否则它们会有一个循环引用,导致一个stackoverflowexception。 一旦你尝试了一个双向关系(比如OneToOne)并自动生成Equals()或者hashCode()甚至toString(),你将会被捕获到这个stackoverflowexception。

实体界面

 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); } } 

房间实体impl:

 @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实体被认为是领域驱动的ValueObjects,而不是Domain-Driven Entities(这些代码示例适用于这些实体),那么情况可能更多。

Interesting Posts