我已经使用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


JPA 2.0规范规定:

The entity class must have a no-arg constructor. It may have other constructors as well. The no-arg constructor must be public or protected. The entity class must a be top-level class. An enum or interface must not be designated as an entity. The entity class must not be final. No methods or persistent instance variables of the entity class may be final. If an entity instance is to be passed by value as a detached object (e.g., through a remote interface), the entity class must implement the Serializable interface. Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.

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


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

With reference to Field or Property access (away from performance considerations) both are legitimately accessed by means of getters and setters, thus, my model logic can set/get them in the same manner. The difference comes to play when the persistence runtime provider (Hibernate, EclipseLink or else) needs to persist/set some record in Table A which has a foreign key referring to some column in Table B. In case of a Property access type, the persistence runtime system uses my coded setter method to assign the cell in Table B column a new value. In case of a Field access type, the persistence runtime system sets the cell in Table B column directly. This difference is not of importance in the context of a uni-directional relationship, yet it is a MUST to use my own coded setter method (Property access type) for a bi-directional relationship provided the setter method is well designed to account for consistency. Consistency is a critical issue for bi-directional relationships refer to this link for a simple example for a well-designed setter. With reference to Equals/hashCode: It is impossible to use the Eclipse nor Lombok auto-generated Equals/hashCode methods for entities participating in a bi-directional relationship, otherwise they will have a circular reference resulting in a stackoverflow Exception. Once you try a bidirectional relationship (say OneToOne) and auto-generate Equals() or hashCode() or even toString() you will get caught in this stackoverflow exception.


我将尝试回答几个关键点:这来自长期的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,而不是域驱动的实体(这些代码示例是用于域驱动的实体),那么这种情况可能会更严重。