Create the perfect JPA entity
我已经和JPA(实现休眠)一起工作了一段时间,每次我需要创建实体时,我都会发现自己在访问类型、不可变属性、equals/hashcode等问题上遇到困难。.所以我决定尝试找出每个问题的一般最佳实践,并将其写下来供个人使用。不过,我不介意任何人对此发表评论或告诉我哪里错了。
实体类实现可序列化
原因:规范说您必须这样做,但是一些JPA提供者并不强制执行这一点。Hibernate as JPA提供程序不强制执行此操作,但如果未实现序列化,则它可能在其胃部深处的某个地方使用ClassCastException失败。
构造函数
使用实体的所有必需字段创建构造函数
原因:构造函数应始终使创建的实例处于正常状态。
除了此构造函数:还有一个包私有的默认构造函数
原因:需要默认构造函数使Hibernate初始化实体;允许使用private,但需要包private(或public)可见性来生成运行时代理和高效的数据检索,而不需要使用字节码检测。
字段/属性
一般情况下使用字段访问,必要时使用属性访问
原因:这可能是最有争议的问题,因为其中一个或另一个(属性访问与字段访问)没有明确和令人信服的论据;但是,由于代码更清晰、封装更好并且不需要为不可变的字段创建setter,字段访问似乎是最受欢迎的。
省略不可变字段的setter(访问类型字段不需要)
- 属性可能是私有的原因:我曾经听说Protected对(Hibernate)性能更好,但我在Web上只能找到:Hibernate可以直接访问public、private和protected访问器方法,以及public、private和protected字段。选择取决于您,您可以将其匹配以适合您的应用程序设计。
等值/哈希码
- 如果仅在持久化实体时设置此ID,则从不使用生成的ID
- 按首选项:使用不可变的值形成唯一的业务密钥,并使用该值测试相等性
- 如果唯一的业务密钥不可用,请使用在初始化实体时创建的非暂时UUID;有关详细信息,请参阅这篇伟大的文章。
- 不要引用相关实体(manytoone);如果此实体(如父实体)需要成为业务密钥的一部分,则只比较ID。在代理上调用getid()不会触发实体的加载,只要您使用的是属性访问类型。
实例实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | @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
JPA2.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方法实现的要求,仅限于我所知的主键类和映射键。
我将尝试回答几个关键点:这是来自长时间的休眠/持久性体验,包括几个主要的应用程序。
实体类:实现可序列化?
键需要实现可序列化。要在HTTPSTIN中运行的东西,或者通过RPC/JavaEE在网上发送,需要实现可串行化。其他东西:不多。把时间花在重要的事情上。
构造函数:用实体的所有必需字段创建一个构造函数?
应用程序逻辑的构造函数应该只有几个关键的"外键"或"类型/种类"字段,这些字段在创建实体时始终是已知的。其余的应该通过调用setter方法来设置——这就是它们的用途。
避免在构造函数中放入太多字段。施工人员应方便,并给予对象基本的健全性。名称、类型和/或父级通常都很有用。
如果应用程序规则(今天)要求客户有地址,请将其留给设置者。这是一个"弱规则"的例子。也许下周,你想在进入"输入详细信息"屏幕之前创建一个客户对象?不要绊倒自己,留下未知、不完整或"部分输入"数据的可能性。
构造器:还有,包私有默认构造器?
是的,但是使用"受保护"而不是包私有。当必要的内部结构不可见时,子类化是一种真正的痛苦。
字段/属性
对休眠和实例外部使用"property"字段访问。在实例中,直接使用字段。原因:允许标准反射(Hibernate最简单和最基本的方法)工作。
对于应用程序的"不可变"字段,Hibernate仍然需要能够加载这些字段。您可以尝试将这些方法设置为"私有",和/或在它们上面放置注释,以防止应用程序代码进行不必要的访问。
注意:编写equals()函数时,请使用getter获取"other"实例上的值!否则,您将在代理实例上点击未初始化/空字段。
保护对(休眠)性能更好?
不太可能。
等于/hashcode?
这与保存实体之前与实体合作有关——这是一个棘手的问题。对不可变值进行哈希/比较?在大多数业务应用程序中,没有。
客户可以更改地址、更改业务名称等——这并不常见,但会发生。当数据输入不正确时,还需要进行修正。
通常情况下,很少有东西是不可变的,它们是育儿的,也许是类型/种类——通常用户会重新创建记录,而不是更改这些记录。但这些并不能唯一地标识实体!
所以,无论是长的还是短的,所谓的"不变的"数据并不是真的。主键/ID字段是为精确目的而生成的,以提供这样的保证稳定性和不可变性。
当a)在"不经常更改的字段"上比较/哈希,或b)在"未保存的数据"上比较/哈希时,如果在ID上比较/哈希,则需要计划并考虑比较和哈希处理工作阶段的需要。
equals/hashcode--如果唯一的业务密钥不可用,请使用在初始化实体时创建的非暂时UUID
是的,这是一个很好的策略。但是,要知道UUID并不是免费的,从性能角度考虑——集群会使事情复杂化。
equals/hashcode--从不引用相关实体
如果相关实体(如父实体)需要成为业务键的一部分,则添加一个不可插入、不可更新的字段来存储父ID(与manytoone joincolumn同名),并在相等性检查中使用此ID。
听起来是个好建议。
希望这有帮助!
我在回答中加了2分:
对于字段或属性访问(不考虑性能因素),两者都是通过getter和setter合法访问的,因此,我的模型逻辑可以以相同的方式设置/获取它们。当持久性运行时提供程序(Hibernate、EclipseLink或其他)需要在表A中保留/设置某些记录时,就会产生这种差异,表A中的某个外键引用了表B中的某个列。如果是属性访问类型,持久性运行时系统使用我的编码设置器方法为表B列中的单元格分配一个新值。对于字段访问类型,持久性运行时系统直接在表B列中设置单元。在单向关系的上下文中,这种差异并不重要,但是如果setter方法设计得很好,可以考虑一致性,那么必须使用我自己的编码setter方法(属性访问类型)来处理双向关系。一致性是双向关系的一个关键问题。有关设计良好的setter的简单示例,请参阅此链接。
关于equals/hashcode:对于参与双向关系的实体,不可能使用Eclipse自动生成的equals/hashcode方法,否则它们将具有循环引用,从而导致stackoverflow异常。一旦您尝试双向关系(比如说OneToone)并自动生成equals()或hashcode()甚至toString(),您将被捕获在这个stackOverflow异常中。
实体界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public interface Entity 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); } |
Basic implementation for all entities,简化equals/hashcode implementations:
ZZU1
Room entity implement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | @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实体的每一个案例中,都有基于商业领域的实体平等。That might be more of a case if these JPA entities are thought of as domain-driven valueobjects,instead of domain-driven entities(which these code examples are for).