创建完美的JPA实体

我一直在使用JPA(实现Hibernate)一段时间,每次我需要创建实体时,我发现自己正在与AccessType,不可变属性,equals / hashCode ......等问题挣扎。
所以我决定尝试找出每个问题的一般最佳做法,并写下来供个人使用。
但我不介意任何人对此发表评论或告诉我我错在哪里。

实体类

  • 实现Serializable

    原因:规范说你必须,但是一些JPA提供者不强制执行此操作。 作为JPA提供程序的Hibernate不会强制执行此操作,但如果Serializable尚未实现,它可能会在ClassCastException的深处发生故障。

  • 构造函数

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

    原因:构造函数应始终将创建的实例保留为正常状态。

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

    原因:默认的构造函数需要Hibernate初始化实体; 私有是允许的,但包私有(或公共)可见性是运行时代理生成和有效的数据检索需要的字节码工具。

  • 字段/属性

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

    理由:这可能是最值得商榷的问题,因为没有任何明确和有说服力的论点(财产准入和实地准入); 然而,由于代码更清晰,封装更好,并且不需要为不可变字段创建setter,因此字段访问似乎是最受欢迎的

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

  • 物业可能是私人的
    原因:我曾经听说受保护对于(Hibernate)性能更好,但我在网上可以找到的是:Hibernate可以直接访问公共,私有和受保护的访问方法以及公共,私有和受保护的方法。 选择取决于你,你可以匹配它来适应你的应用程序设计。
  • 等于/的hashCode

  • 如果仅在持久化实体时才设置此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
    
    }
    

    其他建议添加到此列表不止是欢迎...

    UPDATE

    由于阅读这篇文章,我已经适应了我实施eq / hC的方式:

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

  • JPA 2.0规范指出:

  • 实体类必须有一个无参数构造函数。 它也可能有其他的构造函数。 无参数构造函数必须是公共的或受保护的。
  • 实体类必须是顶级类。 枚举或接口不能被指定为实体。
  • 实体类不能是最终的。 实体类的方法或持久化实例变量可能是最终的。
  • 如果实体实例要作为分离对象 (例如,通过远程接口) 作为值传递 ,则实体类必须实现Serializable接口。
  • 抽象类和具体类都可以是实体。 实体可以扩展非实体类以及实体类,非实体类可以扩展实体类。
  • 就我所知,该规范不包含有关实体的equals和hashCode方法的实现的要求,仅针对主键类和映射键。


    我将尝试回答几个关键点:这来自包括几个主要应用程序的长时间的Hibernate /持久性体验。

    实体类:实现Serializable?

    密钥需要实现Serializable。 即将进入HttpSession或者通过RPC / Java EE通过线路发送的东西需要实现Serializable。 其他的东西:不是很多。 把时间花在重要的事情上。

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

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

    避免将太多的字段放入构造函数中。 施工人员应该方便,并给予物体基本的理智。 姓名,类型和/或父母通常都是有用的。

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

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

    是的,但使用'保护'而不是私人包装。 当必要的内部部件不可见时,子类化东西是一种真正的痛苦。

    字段/属性

    对Hibernate使用'property'字段访问,并从实例外部使用。 在该实例中,直接使用这些字段。 原因:允许标准反射,Hibernate的最简单和最基本的方法工作。

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

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

    受保护对于(Hibernate)性能更好?

    不太可能。

    等于/的hashCode?

    这与在实体保存之前一起工作有关 - 这是一个棘手的问题。 散列/比较不可变的值? 在大多数商业应用程序中,没有任何。

    客户可以更改地址,更改其业务名称等等 - 不常见,但会发生。 当数据输入不正确时,还需要进行更正。

    通常保持不变的少数事情是Parenting,也许是Type / Kind--通常用户重新创建记录,而不是改变它们。 但是这些并不能唯一标识实体!

    所以,总之,声称的“不可变的”数据并不是真的。 主密钥/ ID字段是为了确切的目的而产生的,以提供这种有保证的稳定性和不变性。

    在A)使用UI中的“更改/绑定数据”时,如果您比较/散列“不常更改的字段”,或者B)使用“更改/绑定数据”,则需要计划和考虑您需要比较和散列和请求处理工作阶段。未保存的数据“,如果你比较/散列ID。

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

    是的,在需要时这是一个很好的策略。 请注意,尽管UUID不是免费的,但性能明智 - 集群使事情变得复杂。

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

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

    听起来很好的建议。

    希望这可以帮助!


    在对Stijns半全面清单表示钦佩之后,2个更正是:

  • 通过引用Field或Property访问(远离性能考虑),两者都可以通过getter和setter合法访问,因此,我的模型逻辑可以以相同的方式设置/获取它们。 当持久运行时提供程序(Hibernate,EclipseLink或其他)需要在表A中持久/设置某个记录时存在差异,该表中有一个外键引用表B中的某个列。在属性访问类型的情况下,持久性运行时系统使用我的编码设置器方法为表B列中的单元格分配一个新值。 在Field访问类型的情况下,持久性运行时系统直接在表B列中设置单元格。 这种差异在单向关系的背景下并不重要,但是必须使用我自己的编码设置器方法(属性访问类型)作为双向关系,只要设置器方法设计得很好以说明一致性。 对于双向关系来说,一致性是一个关键问题,请参阅此链接,以获取设计良好的二传手的简单示例。

  • 参考Equals / hashCode:对于参与双向关系的实体,使用Eclipse自动生成的Equals / hashCode方法是不可能的,否则它们会有循环引用,导致出现一个stackoverflow异常。 一旦你尝试了一种双向关系(比如OneToOne)并自动生成Equals()或者hashCode()甚至toString(),你将会在这个stackoverflow例外中被捕获。

  • 链接地址: http://www.djcxy.com/p/6311.html

    上一篇: Create the perfect JPA entity

    下一篇: Hibernate Child Collection Limited When Using Left Join in Criteria