我已经使用JPA(实现Hibernate)一段时间了,每次我需要创建实体时,我发现自己在AccessType、不可变属性、等于/hashCode、... .等问题上苦苦挣扎
所以我决定试着找出每个问题的一般最佳实践,并把它写下来供个人使用。
我不介意任何人对此发表评论,或者告诉我哪里错了。
实体类
实现序列化
原因:规范要求您必须这样做,但是一些JPA提供者并没有强制执行这一点。Hibernate作为JPA提供者并没有强制执行这一点,但是如果没有实现Serializable,它可能会在ClassCastException中失败。
构造函数
创建一个包含实体所有必需字段的构造函数
原因:构造函数应该始终让创建的实例处于正常状态。
除了这个构造函数:有一个包的私有默认构造函数
原因:默认构造函数需要Hibernate初始化实体;允许私有,但是需要包私有(或公共)可见性来生成运行时代理和高效的数据检索,而不需要字节码插装。
字段/属性
Use field access in general and property access when needed
Reason: this is probably the most debatable issue since there are no clear and convincing arguments for one or the other (property access vs field access); however, field access seems to be general favourite because of clearer code, better encapsulation and no need to create setters for immutable fields
Omit setters for immutable fields (not required for access type field)
properties may be private
Reason: I once heard that protected is better for (Hibernate) performance but all I can find on the web is: Hibernate can access public, private, and protected accessor methods, as well as public, private and protected fields directly. The choice is up to you and you can match it to fit your application design.
= / hashCode
Never use a generated id if this id is only set when persisting the entity
By preference: use immutable values to form a unique Business Key and use this to test equality
if a unique Business Key is not available use a non-transient UUID which is created when the entity is initialised; See this great article for more information.
never refer to related entities (ManyToOne); if this entity (like a parent entity) needs to be part of the Business Key then compare the ID's only. Calling getId() on a proxy will not trigger the loading of the entity, as long as you're using property access type.
实体例子
@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
我将尝试回答几个关键点:这来自长期的Hibernate/持久性经验,包括几个主要的应用程序。
实体类:实现Serializable?
Keys需要实现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,而不是域驱动的实体(这些代码示例是用于域驱动的实体),那么这种情况可能会更严重。
我将尝试回答几个关键点:这来自长期的Hibernate/持久性经验,包括几个主要的应用程序。
实体类:实现Serializable?
Keys需要实现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
听起来是个好建议。
希望这能有所帮助!