关于java:创建完美的JPA实体

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).