创建完美的JPA实体

Sti*_*ens 410 java hibernate jpa equals

我一直在使用JPA(实现Hibernate)一段时间,每次我需要创建实体时,我发现自己正在努力解决AccessType,immutable属性,equals/hashCode等问题.
因此,我决定尝试找出每个问题的一般最佳实践,并将其写下来供个人使用.
但是我不介意任何人对此发表评论或告诉我哪里错了.

实体类

  • 实现Serializable

    原因:规范说你必须这样做,但是有些JPA提供商并没有强制执行.Hibernate作为JPA提供程序并不强制执行此操作,但如果未实现Serializable,它可能会因为ClassCastException而陷入困境.

构造函数

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

    原因:构造函数应始终将创建的实例保持在合理的状态.

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

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

字段/属性

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

    原因:这可能是最有争议的问题,因为对于其中一个(属性访问与字段访问)没有明确和令人信服的论据; 然而,由于更清晰的代码,更好的封装以及不需要为不可变字段创建setter,因此字段访问似乎是最受欢迎的

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

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

等于/的hashCode

  • 如果仅在持久化实体时设置此ID,则永远不要使用生成的id
  • 优先级:使用不可变值来形成唯一的Business Key,并使用它来测试相等性
  • 如果唯一的业务密钥不可用,则使用在初始化实体时创建的非瞬态UUID ; 有关更多信息,请参阅此精彩文章.
  • 从不参考相关实体(ManyToOne); 如果此实体(如父实体)需要成为业务密钥的一部分,则仅比较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

}
Run Code Online (Sandbox Code Playgroud)

其他建议加入此列表非常受欢迎......

UPDATE

自从阅读本文以来,我已经调整了我实现eq/hC的方式:

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

Edw*_*rzo 143

JPA 2.0规范规定:

  • 实体类必须具有无参数构造函数.它也可能有其他构造函数.no-arg构造函数必须是公共的或受保护的.
  • 实体类必须是顶级类.不得将枚举或接口指定为实体.
  • 实体类不能是最终的.实体类的方法或持久性实例变量可能不是最终的.
  • 如果要通过值将实体实例作为分离对象传递(例如,通过远程接口),则实体类必须实现Serializable接口.
  • 抽象类和具体类都可以是实体.实体可以扩展非实体类以及实体类,非实体类可以扩展实体类.

据我所知,该规范不包含关于实体的equals和hashCode方法的实现的要求,仅对主键类和映射键没有要求.

  • True,equals,hashcode,......不是JPA要求,但当然是推荐并被视为良好做法. (13认同)
  • @TheStijn JPA提供程序将确保在任何给定时间上下文中只有一个给定实体的实例,因此即使您的集合是安全的而不实现equals/hascode,前提是您只使用托管实体.为实体实现这些方法并非没有困难,例如,看看这个[Hibernate Article](http://community.jboss.org/wiki/EqualsAndHashCode)关于这个主题.我的观点是,如果你只使用托管实体,没有它们你会更好,否则提供非常仔细的实现. (10认同)
  • @TheStijn好吧,除非你打算比较分离的实体是否相等,否则这可能是不必要的.每次您要求时,实体管理器都保证返回给定实体的相同实例.因此,据我所知,您可以对托管实体的身份比较做得很好.您能否详细说明一下您认为这是一个好习惯的场景? (6认同)
  • 我努力始终有一个正确的equals/hashCode实现.JPA不需要,但我认为这是实体或添加到集合时的一个好习惯.您可以决定仅在将实体添加到集合时实现等于但您是否始终事先知道? (2认同)
  • @TheStijn这是一个很好的混合场景.它证明了你最初建议实现eq/hC的必要性,因为一旦实体放弃了持久层的安全性,你就不再相信JPA标准所强制执行的规则.在我们的例子中,DTO模式从一开始就在架构上得到了强制执行.根据设计,我们的持久性API不提供与业务对象交互的公共方式,只提供使用DTO与我们的持久层交互的API. (2认同)

Tho*_*s W 68

我将尝试回答几个要点:这是来自长期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,也许是Type/Kind - 通常用户重新创建记录,而不是更改这些记录.但这些并不能唯一地识别实体!

因此,无论多长时间,所声称的"不可变"数据都不是真的.生成主键/ ID字段用于精确目的,提供这种保证的稳定性和不变性.

您需要计划并考虑您对比较和散列以及请求处理工作阶段的需求A)如果您在"不经常更改的字段"上比较/散列,则使用UI中的"更改/绑定数据",或者B)使用"未保存的数据",如果您在ID上比较/哈希.

Equals/HashCode - 如果唯一的Business Key不可用,请使用在初始化实体时创建的非瞬态UUID

是的,这在需要时是一个很好的策略.请注意,UUID在性能方面不是免费的 - 而且群集使事情变得复杂.

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

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

听起来不错.

希望这可以帮助!

  • Re:构造函数,我经常只看到零arg(即没有),并且调用代码有很长的setter列表,这对我来说似乎有些混乱.拥有几个适合您需求的构造函数确实存在任何问题,使调用代码更简洁吗? (2认同)

Sym*_*Sym 13

我在这里得到的2美分补充是:

  1. 参考字段或属性访问(远离性能考虑)两者都可以通过getter和setter合法访问,因此,我的模型逻辑可以以相同的方式设置/获取它们.当持久性运行时提供程序(Hibernate,EclipseLink或其他)需要持久化/设置表A中的某些记录时,差异就会发挥作用,表A中的外键引用了表B中的某些列.如果是属性访问类型,则持久化运行时系统使用我的编码setter方法为表B列中的单元格分配一个新值.在Field访问类型的情况下,持久性运行时系统直接在表B列中设置单元格.在单向关系的背景下,这种差异并不重要,但是必须使用我自己的编码setter方法(属性访问类型)来实现双向关系,前提是setter方法经过精心设计以考虑一致性.对于双向关系,一致性是一个关键问题链接为精心设计的setter的简单示例.

  2. 参考Equals/hashCode:对于参与双向关系的实体,不可能使用Eclipse自动生成的Equals/hashCode方法,否则它们将具有循环引用,从而导致stackoverflow Exception.一旦你尝试了双向关系(比如OneToOne)并自动生成Equals()或hashCode()甚至toString(),你就会陷入这种stackoverflow异常.


aha*_*man 9

实体界面

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);
}
Run Code Online (Sandbox Code Playgroud)

所有实体的基本实现,简化了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);
}
}
Run Code Online (Sandbox Code Playgroud)

房间实体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
}
Run Code Online (Sandbox Code Playgroud)

在JPA实体的每种情况下,我都没有看到基于业务领域比较实体的平等性.如果将这些JPA实体视为域驱动的ValueObjects而不是Domain-Driven Entities(这些代码示例适用于此),则可能更多情况.

  • 虽然使用父实体类来获取样板代码是一种很好的方法,但在equals方法中使用DB定义的id并不是一个好主意.在您的情况下,比较2个新实体甚至会抛出NPE.即使你使它安全无效,那么2个新实体总是相等的,直到它们被保持为止.Eq/hC应该是不可变的. (4认同)
  • 实际上,我没有看到我错过了代码是无效的.但IMO使用id仍然是不好的做法.参数:http://www.onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html (3认同)
  • Equals()不会抛出NPE,因为检查DB id是否为null,如果DB id为null,则相等性为false. (2认同)