我一直在使用JPA(实现Hibernate)已经有一段时间了,每次我需要创建实体时,我发现自己正在努力解决AccessType,不可变属性,equals / hashCode等问题。 因此,我决定尝试找出每个问题的一般最佳实践,并将其写下来供个人使用 但是我不介意任何人对此发表评论或告诉我我错在哪里。
实现Serializable
原因:规范说你必须这样做,但是有些JPA提供商不会强制执行此操作。 Hibernate作为JPA提供程序不会强制执行此操作,但如果未实现Serializable,它可能会在ClassCastException的深处失败。
创建一个包含实体所有必填字段的构造函数
原因:构造函数应始终将创建的实例保持在理智状态。
除了这个构造函数:有一个包私有默认构造函数
原因:默认构造函数需要让Hibernate初始化实体;允许private,但是在没有字节码检测的情况下,运行时代理生成和高效数据检索需要包私有(或公共)可见性。
一般情况下使用字段访问权限,并在需要时使用属性访问
原因:这可能是最值得商榷的问题,因为对于其中一个(属性访问与字段访问)没有明确且令人信服的论据;然而,由于更清晰的代码,更好的封装以及无需为不可变字段创建setter,字段访问似乎是最受欢迎的
忽略不可变字段的setter(访问类型字段不需要)
@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的方式:
答案 0 :(得分:136)
- 实体类必须具有无参数构造函数。它也可能有其他构造函数。 no-arg构造函数必须是公共的或受保护的。
- 实体类必须是顶级类。枚举或接口不能是 指定为实体。
- 实体类不能是最终的。实体类的任何方法或持久性实例变量都不是最终的。
- 如果要通过值将实体实例作为分离对象传递(例如,通过远程接口),则实体类必须实现Serializable接口。
- 抽象类和具体类都可以是实体。实体可以扩展非实体类以及实体类,非实体类可以扩展实体类。
规范不包含有关实体的equals和hashCode方法的实现的要求,只有对于主键类和映射键,据我所知。
答案 1 :(得分:63)
我将尝试回答几个关键点:这是来自长期Hibernate /持久性体验,包括几个主要应用程序。
实体类:实现Serializable?
Keys 需要实现Serializable。将要通过HttpSession进行的东西,或者通过RPC / Java EE通过线路发送的东西需要实现Serializable。其他的东西:不是那么多。把时间花在重要的事情上。
构造函数:创建一个包含实体所有必填字段的构造函数?
应用程序逻辑的构造函数应该只有一些关键的“外键”或“类型/种类”字段,这些字段在创建实体时始终是已知的。其余的应该通过调用setter方法来设置 - 这就是它们的用途。
避免将太多字段放入构造函数中。构造者应该方便,并为对象提供基本的理智。名称,类型和/或父母通常都很有用。
OTOH如果申请规则(今天)要求客户拥有地址,请将其留给设定者。这是“弱势规则”的一个例子。也许下周,你想在进入Enter Details屏幕之前创建一个Customer对象?不要绊倒自己,留下未知,不完整或“部分输入”数据的可能性。
构造函数:还包,私有默认构造函数?
是的,但使用'protected'而不是私有包。当必要的内部结构不可见时,子类化是非常痛苦的。
<强>字段/属性强>
对Hibernate和实例外部使用'property'字段访问。在实例中,直接使用字段。理由:允许标准反射,最简单和最简单的反射。工作的最基本的Hibernate方法。
对于应用程序的字段“不可变” - Hibernate仍然需要能够加载它们。您可以尝试将这些方法设置为“私有”,和/或在其上添加注释,以防止应用程序代码进行不必要的访问。
注意:在编写equals()函数时,在'other'实例上使用getters作为值!否则,您将在代理实例上点击未初始化/空字段。
受保护的(Hibernate)性能更好吗?
不太可能的。
<强>等于/的HashCode吗
这与在与实体保存之前合作相关 - 这是一个棘手的问题。对不可变值进行哈希/比较?在大多数业务应用程序中,没有任何。
客户可以更改地址,更改其业务名称等等 - 不常见,但它会发生。当数据输入不正确时,还需要进行更正。
通常保持不变的一些事情是Parenting和类似/ Kind - 通常用户重新创建记录,而不是更改这些记录。但这些并不能唯一地识别实体!
因此,无论多长时间,声称的“不可变”数据都不是真的。生成主键/ ID字段用于精确目的,提供这种保证的稳定性&amp;不变性。
你需要计划&amp;考虑你需要进行比较&amp;哈希&amp;请求处理工作阶段A)如果你在“不经常更改的字段”上比较/哈希,则使用UI中的“更改/绑定数据”,或者B)使用“未保存数据”,如果比较ID /哈希值。 / p>
Equals / HashCode - 如果没有唯一的业务密钥,请使用在初始化实体时创建的非瞬态UUID
是的,这在需要时是一个很好的策略。请注意,UUID在性能方面不是免费的 - 而且群集会使事情变得复杂。
Equals / HashCode - 永远不会引用相关实体
“如果相关实体(如父实体)需要成为Business Key的一部分,则添加一个不可插入的,不可更新的字段来存储父ID(与ManytoOne JoinColumn同名)并在此中使用此id平等检查“
听起来很不错。
希望这有帮助!
答案 2 :(得分:12)
我在这里得到的2美分的补充是:
参考Field或Property访问(远离性能考虑),两者都可以通过getter和setter合法访问,因此,我的模型逻辑可以以相同的方式设置/获取它们。 当持久性运行时提供程序(Hibernate,EclipseLink或其他)需要持久化/设置表A中的某些记录时,差异就会发挥作用,表A中的外键引用了表B中的某些列。如果是属性访问类型,则持久化运行时系统使用我的编码setter方法为表B列中的单元格分配一个新值。在Field访问类型的情况下,持久性运行时系统直接在表B列中设置单元格。 这种差异在单向关系的背景下并不重要,但是必须使用我自己的编码setter方法(属性访问类型)来实现双向关系,前提是setter方法经过精心设计以考虑一致性。对于双向关系而言,一致性是一个关键问题,请参阅此link,以获得精心设计的setter的简单示例。
参考Equals / hashCode:对于参与双向关系的实体,不可能使用Eclipse自动生成的Equals / hashCode方法,否则它们将具有循环引用,从而导致stackoverflow异常。一旦你尝试了双向关系(比如OneToOne)并自动生成Equals()或hashCode()甚至toString(),你就会陷入这种stackoverflow异常。
答案 3 :(得分:8)
实体界面
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(这些代码示例适用于这些实例),则可能更多情况。