创建完美的JPA实体

我一直在使用JPA(实现Hibernate)一段时间了,每次我需要创建实体时,我发现自己在AccessType、不可变属性、equals/hashCode、... .
等问题上苦苦挣扎 所以我决定试着找出每个问题的一般最佳实践,并把它写下来供个人使用 我不介意任何人评论它或告诉我哪里错了。

实体类

  • < p >实现Serializable

    原因:规范要求您必须这样做,但是一些JPA提供者并不强制执行这一点。Hibernate作为JPA提供者并没有强制执行这一点,但是如果没有实现Serializable,它可能会在ClassCastException中失败。 < / p >

构造函数

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

    原因:构造函数应该始终让创建的实例处于正常状态。 < / p >

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

    原因:默认构造函数需要Hibernate初始化实体;允许私有,但是需要包私有(或公共)可见性来生成运行时代理和高效的数据检索,而不需要字节码插装。 < / p >

字段/属性

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

    原因:这可能是最具争议的问题,因为两者之间没有明确和令人信服的论据(财产访问vs实地访问);然而,字段访问似乎是普遍的最爱,因为更清晰的代码,更好的封装和不需要为不可变字段创建设置 < / p >

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

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

= / hashCode

  • 如果这个id只在持久化实体时设置,则永远不要使用生成的id
  • 根据偏好:使用不可变值来形成唯一的Business Key,并使用它来测试是否相等
  • 如果唯一业务密钥不可用,则使用非瞬态UUID,该UUID是在实体初始化时创建的;更多信息请参见这篇很棒的文章
  • 从来没有指相关实体(ManyToOne);如果这个实体(如父实体)需要成为Business Key的一部分,则只比较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
180715 次浏览

JPA 2.0规范声明:

  • 实体类必须有一个无参数构造函数。它也可能有其他的构造函数。无参数构造函数必须为public或protected。
  • 实体类必须是顶级类。enum或接口不能是
  • 实体类不能是final类。实体类的任何方法或持久实例变量都不能是final的。
  • 如果要将实体实例作为分离对象按值传递(例如,通过远程接口),实体类必须实现Serializable接口。
  • 抽象类和具体类都可以是实体。实体可以扩展非实体类以及实体类,而非实体类可以扩展实体类。

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

我对答案的2美分补充如下:

    关于字段或属性访问(远离性能考虑),两者都可以通过getter和setter合法访问,因此,我的模型逻辑可以以相同的方式设置/获取它们。 当持久性运行时提供者(Hibernate、EclipseLink或其他)需要在表A中持久化/设置一些记录时,区别就显现出来了,这些记录有一个外键指向表B中的某些列。对于属性访问类型,持久性运行时系统使用我编码的setter方法为表B列中的单元格分配一个新值。对于Field访问类型,持久性运行时系统直接在表B列中设置单元格。 这种差异在单向关系的上下文中并不重要,但对于双向关系,如果setter方法设计得很好,可以考虑一致性,则必须使用我自己编写的setter方法(属性访问类型)。一致性是双向关系的关键问题,请参考链接获得一个设计良好的setter的简单示例
  1. 引用Equals/hashCode:对于参与双向关系的实体,不可能使用Eclipse或Lombok自动生成的Equals/hashCode方法,否则它们将有一个循环引用,导致stackoverflow异常。一旦您尝试双向关系(例如OneToOne)并自动生成Equals()或hashCode()甚至toString(),您将陷入此stackoverflow异常。

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

实体类:实现Serializable?

需要实现Serializable。将要进入HttpSession的东西,或者由RPC/Java EE通过线路发送的东西,需要实现Serializable。其他方面:没那么多。把时间花在重要的事情上。

构造函数:创建一个构造函数与实体的所有必需字段?

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

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

OTOH如果应用程序规则(今天)要求客户有一个地址,把它留给setter。这就是“弱规则”的一个例子。也许下周,您想在进入Enter Details屏幕之前创建一个Customer对象?不要把自己绊倒,为未知、不完整或“部分输入”的数据留下可能性。

构造函数:同样,包私有默认构造函数?

是的,但是使用“protected”而不是package private。当必要的内部内容不可见时,子类化内容是一件非常痛苦的事情。

字段/属性

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

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

注意:当编写equals()函数时,使用getter来获取'other'实例上的值!否则,您将在代理实例上命中未初始化/空字段。

受保护的(Hibernate)性能更好?

不太可能的。

= / HashCode吗?

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

客户可以更改地址,更改业务名称等等——不常见,但确实会发生。当数据输入不正确时,还需要能够进行更正。

少数通常保持不变的东西是Parenting和Type/Kind——通常用户会重新创建记录,而不是更改它们。但是这些并不能唯一地识别实体!

总之,所谓的“不可变”数据并不是真的不可变。主键/ ID字段的生成是为了提供这样的稳定性和保证;不变性。

你需要计划;考虑你比较的需要;散列,当A)处理来自UI的“更改/绑定数据”(如果你比较/哈希“不经常更改的字段”),或者B)处理“未保存的数据”(如果你比较/哈希ID)时,请求处理工作阶段。

Equals/HashCode——如果惟一的业务键不可用,则使用在初始化实体时创建的非瞬态UUID

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

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

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

听起来是个好建议。

希望这能有所帮助!

实体界面

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,而不是域驱动的实体(这些代码示例是用于域驱动的实体),那么这种情况可能会更严重。