创建完美的JPA实体


422

我已经使用JPA(实现休眠)一段时间了,每次我需要创建实体时,我都会遇到诸如AccessType,不可变属性,equals / hashCode等问题。
因此,我决定尝试找出每个问题的最佳常规做法,并写下来供个人使用。
但是,我不介意让任何人对此发表评论或告诉我我错了。

实体类

  • 实现可序列化

    原因:规范要求您必须这样做,但是某些JPA提供程序没有强制执行此操作。作为JPA提供程序的Hibernate不会强制执行此操作,但是如果尚未实现Serializable,它可能会因ClassCastException失败而失败。

建设者

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

    原因:构造函数应始终使创建的实例保持健全状态。

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

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

字段/属性

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

    原因:这可能是最有争议的问题,因为没有明确的,令人信服的论点(财产使用权与实地使用权);但是,由于更清晰的代码,更好的封装并且无需为不可变字段创建设置器,因此字段访问似乎是普遍喜欢的方法

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

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

等于/哈希码

  • 如果仅在持久化实体时设置此ID,请不要使用生成的ID
  • 根据喜好:使用不可变值形成唯一的业务密钥,并使用它来测试是否相等
  • 如果唯一的业务密钥不可用,则使用在初始化实体时创建的非临时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

}

欢迎添加到此列表的其他建议...

更新

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

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

6
这不是一个问题,它是一个复审请求和一个清单请求。而且,它是开放式的,含糊不清的,或者换句话说:JPA实体是否完美取决于它的用途。我们是否应该列出实体在实体的所有可能使用中可能需要的所有东西?
优点

我知道我对此表示歉意,这不是一个明确的问题。这实际上不是请求列表,而是要求提供评论/备注,尽管也欢迎其他建议。随时详细说明JPA实体的可能用途。
Stijn Geukens

我也希望字段是final(根据您对设置员的遗漏来判断,我想您也是如此)。
Sridhar Sarnobat

必须尝试一下,但我认为final不会起作用,因为Hibernate仍然需要能够在这些属性上设置值。
Stijn Geukens

哪里notNull来的?
bruno

Answers:


73

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

实体类:实现可序列化吗?

密钥需要实现Serializable。将要在HttpSession中存储的东西,或者由RPC / Java EE通过电线发送的东西,都需要实现Serializable。其他东西:不是很多。把时间花在重要的事情上。

构造函数:使用实体的所有必填字段创建构造函数吗?

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

避免将太多字段放入构造函数中。构造函数应该很方便,并且要使对象基本清醒。名称,类型和/或父母通常都是有用的。

OTOH(如果今天(今天)应用规则要求客户提供地址,请将其留给设置者。那是“弱规则”的一个例子。也许下周,您想在进入“输入详细信息”屏幕之前创建“客户”对象吗?不要绊倒自己,留下未知,不完整或“部分输入”数据的可能性。

构造函数:另外,封装私有默认构造函数?

是的,但请使用“受保护的”而不是私有包。当必要的内部结构不可见时,子类化的东西是一个真正的痛苦。

字段/属性

从实例外部对休眠使用“属性”字段访问。在实例内,直接使用字段。原因:允许标准反射(Hibernate最简单,最基本的方法)起作用。

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

注意:编写equals()函数时,请在'other'实例上使用getter作为值!否则,您将在代理实例上点击未初始化/空白的字段。

受保护的(休眠)性能更好?

不太可能。

等于/哈希码?

这与在保存实体之前使用实体有关-这是一个棘手的问题。哈希/比较不变值?在大多数业务应用程序中,根本没有。

客户可以更改地址,更改其业务名称等,虽然不常见,但确实如此。当数据输入不正确时,还需要进行校正。

通常保持不变的几件事是育儿,也许还有类型/种类-通常,用户重新创建记录,而不是更改它们。但是这些并不能唯一地标识实体!

因此,总之,所谓的“不变”数据并不是真的。主键/ ID字段是出于精确目的而生成的,以提供这种保证的稳定性和不变性。

当A)如果在“不经常更改的字段”上进行比较/哈希处理,或者B)在“未保存的数据”,如果您比较/哈希ID。

Equals / HashCode-如果没有唯一的业务密钥,请使用在初始化实体时创建的非临时UUID

是的,这是必要时的好策略。请注意,UUID不是免费的,但是从性能角度来看,它使集群变得复杂。

等于/哈希码-从不引用相关实体

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

听起来像个好建议。

希望这可以帮助!


2
回复:构造函数,我经常只看到零arg(即无),并且调用代码中有很多设置方法,这对我来说似乎有些混乱。几个适合您需要的构造函数,使调用代码更简洁,真的有什么问题吗?
飓风

完全自以为是,特别是关于ctor。还有什么更漂亮的代码?一堆不同的ctor,让您知道创建obj的健全状态或无参数的ctor所需的值(组合),不知道应该设置什么,以什么顺序设置并容易出现用户错误?
mohamnag '18

1
@mohamnag取决于。对于内部系统生成的数据,严格有效的bean非常有用;但是,现代的业务应用程序由大量的用户数据输入CRUD或向导屏幕组成。用户输入的数据至少在编辑过程中通常是部分或格式不正确的。通常,能够记录不完整的状态以供日后完成甚至具有商业价值-认为保险申请被捕获,客户注册等。将约束最小化(例如主键,业务键和状态)可以在实际中提供更大的灵活性。业务情况。
Thomas W

1
@ThomasW首先,我不得不坚决支持域驱动设计,并在类名中使用名称,在方法中使用完整的动词。在此范例中,您所指的实际上是DTO,而不是应用于临时数据存储的域实体。或者您只是误解了/构造了域。
mohamnag

@ThomasW当我过滤掉所有试图说我是新手的句子时,除了用户输入之外,您的注释中没有其他信息。正如我之前所说,该部分应在DTO中完成,而不是直接在实体中完成。让我们再聊50年,您可能会像Fowler一样成为DDD背后的大人物的5%!欢呼声:D
mohamnag

144

JPA 2.0规范规定:

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

据我所知,该规范对实体的equals和hashCode方法的实现没有任何要求,仅针对主键类和映射键。


13
正确,等于,哈希码等不是JPA要求,但当然推荐并认为是好的做法。
Stijn Geukens

6
@TheStijn好吧,除非您打算比较分离的实体是否相等,否则可能没有必要。保证实体管理器每次请求时都返回给定实体的相同实例。因此,据我所知,您可以对托管实体的身份进行比较。您能否详细说明一下您认为这是一种良好实践的方案?
埃德温·达洛佐

2
我努力始终拥有一个正确的equals / hashCode实现。JPA不是必需的,但我认为这是将实体添加到集合或将其添加到集合时的一种良好做法。您可以决定仅在将实体添加到集合中时才实现equals,但是您始终事先知道吗?
Stijn Geukens

10
@TheStijn JPA提供程序将确保在任何给定时间上下文中给定实体只有一个实例,因此,即使您的集合在不实施均等/哈希码的情况下也是安全的,前提是您仅使用托管实体。为实体实现这些方法并非没有困难,例如,请参阅有关该主题的Hibernate文章。我的观点是,如果仅使用托管实体,那么最好不要使用它们,否则请提供非常谨慎的实现。
2011年

2
@TheStijn这是很好的混合方案。它证明了按照您最初建议的那样实现eq / hC的必要性,因为一旦实体放弃了持久层的安全性,您将不再信任JPA标准强制执行的规则。在我们的案例中,DTO模式从一开始就在架构上得到了实施。通过设计,我们的持久性API不提供与业务对象进行交互的公共方式,而仅提供一种使用DTO与我们的持久性层进行交互的API。
埃德温·达洛佐

13

我在这里获得的2美分收益是:

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

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


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);
}

所有实体的基本实现,简化了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);
}
}

房间实体展示:

@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,而不是域驱动的实体(这些代码示例所针对的域),则情况可能更是如此。


4
尽管使用父实体类删除样板代码是一种好方法,但在equals方法中使用数据库定义的ID并不是一个好主意。在您的情况下,比较2个新实体甚至会抛出NPE。即使您将它设为null安全,两个新实体也将始终相等,直到它们被持久化。Eq / hC应该是不变的。
Stijn Geukens

2
Equals()不会抛出NPE,因为会检查DB ID是否为null,如果DB ID为null,则相等性为false。
ahaaman 2013年

3
确实,我看不到我怎么会错过代码是空安全的。但是,使用ID的IMO仍然是不好的做法。参数:onjava.com/pub/a/onjava/2006/09/13/...
斯泰恩Geukens

在沃恩·弗农(Vaughn Vernon)的《实施DDD》一书中,有人争辩说,如果您使用“早期PK生成”,则可以将id用作等于(先生成一个id,然后将其传递给实体的构造函数,而不是让数据库生成您保留实体时的ID。)
Wim Deblauwe 2015年

还是如果您不打算平等比较非持久性实体?你为什么要...
Enerccio
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.