Hibernate Annotations - 哪个更好,字段或属性访问?

Mar*_*nor 129 java orm annotations hibernate jpa

这个问题与Hibernate Annotation Placement Question有些相关.

但我想知道哪个更好?通过属性访问或通过字段访问?各有哪些优缺点?

Mar*_*tin 236

两者都存在争议,但大多数都源于某些用户要求"如果需要添加逻辑",或"xxxx打破封装".然而,没有人真正对这一理论发表评论,并给出了合理的理由.

什么是Hibernate/JPA在持久化对象时实际做的事情 - 好吧,它是持久化对象的状态.这意味着以一种可以轻松复制的方式存储它.

什么是封装?封装意味着使用应用程序/客户端可以用来安全访问数据的接口封装数据(或状态) - 保持数据的一致性和有效性.

把它想象成MS Word.MS Word在内存中维护文档的模型 - 文档STATE.它提供了一个用户可以用来修改文档的界面 - 一组按钮,工具,键盘命令等.但是,当您选择保留(保存)该文档时,它会保存内部状态,而不是一组按键和鼠标点击用于生成它.

保存对象的内部状态不会破坏封装 - 否则您并不真正理解封装意味着什么,以及它存在的原因.它就像对象序列化一样.

因此,在大多数情况下,坚持FIELDS而不是ACCESSORS是合适的.这意味着可以从数据库中精确地重新创建对象,就像存储它一样.它不应该需要任何验证,因为这是在原始文件创建时,在它存储在数据库之前完成的(除非,上帝禁止,你在DB中存储无效数据!!!!).同样,应该不需要计算值,因为它们是在存储对象之前已经计算的.该对象应该看起来像它保存之前的方式.事实上,通过在getter/setter中添加额外的东西,实际上增加了重新创建不是原始副本的东西的风险.

当然,添加此功能是有原因的.可能存在一些用于持久访问者的有效用例,但是,它们通常很少见.一个例子可能是你想要避免持久计算一个值,尽管你可能想问一个问题,为什么你不在值的getter中按需计算它,或懒惰地在getter中初始化它.我个人认为没有任何好的用例,这里的答案都没有给出"软件工程"的答案.

  • 当您需要将映射信息添加到与任何持久性实现无关的第三方实体类的子类时,映射访问器的一个很好的用例.由于字段在这些类中是私有的,因此您必须覆盖访问者并向其添加映射注释.另一种选择是使用XML映射,但有些事情很难做到.因此,如果您需要注释并映射第三方类,则对它们进行子类化并在访问器上添加注释是可行的方法. (21认同)
  • 软件工程的答案是:使用访问器违反DRY. (8认同)
  • @ElnurAbdurrakhimov我们去了,一个很好的例子.谢谢. (5认同)
  • @Geek现在我读回来了,我不完全确定自己.我写这个答案已经两年了.我想一个更好的例子可能是您使用遗留数据库的位置,并且数据以与您的对象模型不同的方式呈现 - 访问者可以提供两者之间的映射. (4认同)

duf*_*ymo 77

我更喜欢字段访问,因为这样我就不会被迫为每个属性提供getter/setter.

通过Google进行的快速调查表明,现场访问占大多数(例如,http://java.dzone.com/tips/12-feb-jpa-20-why-accesstype).

我认为现场访问是Spring推荐的习惯用法,但是我无法找到支持它的参考.

有一个相关的SO问题,试图衡量绩效,并得出结论,"没有差异".

  • @anshulkatta我觉得我应该真正解决你的问题,因为这就是封装的全部内容.理想情况下,您的所有字段都应该是私有的,如果可能的话,它们应该没有getter或setter - 这是您可以期望的最佳封装级别.考虑一下密码检查器.2个私有字段passwordHash和failedAttempts.两者都可以是私人的,没有吸气剂或二传手.它们由bool checkPassword(字符串密码)使用,它使用哈希,检查passwordHash,然后更新failedAttempts并返回结果.无需其他代码即可访问这两个字段. (3认同)
  • @anshulkatta应该注意的是,在OOP中,getter和setter是一个反模式,它们来自过程式编程,有一个类与它们打破封装原则,并生成很多样板代码,即同一种代码一遍又一遍地重复.对象应该是不可变的,如果需要修改其属性,则应该通过除了返回属性值之外的其他方法来完成. (2认同)

HDa*_*ave 38

这是您必须使用属性访问器的情况.想象一下,你有一个GENERIC抽象类,有很多实现优点可以继承到8个具体的子类:

public abstract class Foo<T extends Bar> {

    T oneThing;
    T anotherThing;

    // getters and setters ommited for brevity

    // Lots and lots of implementation regarding oneThing and anotherThing here
 }
Run Code Online (Sandbox Code Playgroud)

现在究竟该如何注释这个类?答案是您无法使用字段或属性访问来注释它,因为此时您无法指定目标实体.您必须注释具体实现.但是由于持久化属性是在这个超类中声明的,所以你必须在子类中使用属​​性访问.

在具有抽象通用超类的应用程序中,字段访问不是一个选项.

  • 如果没有注释属性,这个问题在机械上很难解决,但是我从来没有遇到过需要一个抽象泛型类的情况,我需要继续使用很多实现.通常我会创建一个类层次结构,以使我的对象具有多态性(使其成为通用的中断类型),而不仅仅是代码重用.并且"很多很多实现"经常违反SRP,在这种情况下,我可能会将它移到单独的类中.是否有一个具体的例子使这个用例更明显? (8认同)
  • 你可以使用`abstract T getOneThing()`和`abstract void setOneThing(T thing)`,并使用字段访问. (3认同)
  • 德勤.我没有想到这一点.我打赌Hibernate会为这些开出一些疯狂的sql. (2认同)

Pas*_*ent 33

我更倾向于使用属性访问器:

  • 如果需要,我可以添加逻辑(如接受的答案中所述).
  • 它允许我在foo.getId() 不初始化代理的情况下调用(在使用Hibernate时很重要,直到HHH-3718得到解决).

退税:

  • 它使代码的可读性降低,例如,您可以浏览整个类以查看是否@Transient存在.


Mig*_*ing 32

我更喜欢访问器,因为我可以随时在我的访问器中添加一些业务逻辑.这是一个例子:

@Entity
public class Person {

  @Column("nickName")
  public String getNickName(){
     if(this.name != null) return generateFunnyNick(this.name);
     else return "John Doe";
  }
}
Run Code Online (Sandbox Code Playgroud)

此外,如果你将另一个libs放入混合中(比如一些JSON转换的lib或BeanMapper或Dozer或其他基于getter/setter属性的bean映射/克隆lib),你将保证lib与持久性同步.经理(都使用getter/setter).

  • 请注意,这是关于ORM如何访问您的字段/属性而不是您的应用程序代码.通过字段访问,您的getNickName()方法将完全按照您的预期工作.如果在getter/setter之外使用持久性"属性",则情况并非如此.这是您可能遇到属性访问和延迟加载问题的地方.所以不,我不同意这个论点.但是,上次我检查Hibernate时遇到了@Id字段的字段访问问题. (16认同)
  • 这个答案与这个问题无关.duffymo的最佳答案 (10认同)
  • 访问者内部不应该有任何业务逻辑.这不是明显的行为. (8认同)

Tho*_*sen 16

让我尝试总结选择基于字段的访问的最重要原因。如果您想深入了解,请阅读我博客上的这篇文章:JPA 和 Hibernate 中的访问策略 – 字段访问和属性访问哪个更好?

迄今为止,基于现场的访问是更好的选择。这里有5个原因:

原因 1:更好的代码可读性

如果您使用基于字段的访问,您可以使用映射注释来注释实体属性。通过将所有实体属性的定义放在类的顶部,您可以获得所有属性及其映射的相对紧凑的视图。

原因 2:省略不应由您的应用程序调用的 getter 或 setter 方法

基于字段的访问的另一个优点是您的持久性提供程序(例如,Hibernate 或 EclipseLink)不使用实体属性的 getter 和 setter 方法。这意味着您不需要提供任何不应由您的业务代码使用的方法。对于生成的主键属性或版本列的setter 方法来说,这是最常见的情况。您的持久性提供程序管理这些属性的值,您不应以编程方式设置它们。

理由三:灵活实现getter和setter方法

因为您的持久性提供程序不调用 getter 和 setter 方法,所以它们不会被迫满足任何外部要求。您可以以任何您想要的方式实现这些方法。这使您能够实现特定于业务的验证规则、触发额外的业务逻辑或将实体属性转换为不同的数据类型。

例如,您可以使用它来将可选关联或属性包装到 Java 中Optional

理由 4:无需将实用方法标记为 @Transient

基于字段的访问策略的另一个好处是您不需要使用@Transient. 这个注解告诉你的持久化提供者一个方法或属性不是实体持久化状态的一部分。并且因为使用字段类型访问持久状态由您的实体的属性定义,您的 JPA 实现会忽略您的实体的所有方法。

原因 5:避免使用代理时出现错误

Hibernate 为延迟获取的一对一关联使用代理,以便它可以控制这些关联的初始化。这种方法几乎适用于所有情况。但是如果您使用基于属性的访问,它会引入一个危险的陷阱。

如果您使用基于属性的访问,当您调用 getter 方法时,Hibernate 会初始化代理对象的属性。如果您在业务代码中使用代理对象,情况总是如此。但是相当多的equals 和 hashCode 实现直接访问属性。如果这是您第一次访问任何代理属性,这些属性仍未初始化。


01e*_*1es 13

这实际上取决于具体案例 - 两种选择都是有原因的.IMO归结为三种情况:

  1. setter有一些逻辑,在从数据库加载实例时不应该执行; 例如,某些值验证发生在setter中,但是来自db的数据应该是有效的(否则它不会到达那里(:);在这种情况下,字段访问是最合适的;
  2. setter有一些应该始终被调用的逻辑,即使在从db加载实例时也是如此; 例如,初始化的属性用于计算某些计算字段(例如,属性 - 货币金额,计算属性 - 同一实例的多个货币属性的总和); 在这种情况下,需要属性访问.
  3. 上述情况都不是 - 然后两个选项都适用,只保持一致(如果在这种情况下选择现场访问,则在类似情况下一直使用它).


Chr*_*oph 12

如果你想在setter中做更多的事情而不仅仅是设置值(例如加密或计算),我强烈建议对getter(属性访问)进行字段访问和NOT注释.

属性访问的问题是在加载对象时也会调用setter.在我们想要引入加密之前,这对我来说很有用.在我们的用例中,我们想要加密setter中的字段并在getter中解密它.现在属性访问的问题是,当Hibernate加载对象时,它还调用setter来填充字段,从而再次加密加密值.这篇文章还提到了这一点: Java Hibernate:不同的属性设置函数行为取决于谁调用它

这让我头疼,直到我记得现场访问和财产访问之间的区别.现在我已将所有注释从属性访问移至现场访问,现在工作正常.

  • +1远离getter/setters.我使用http://projectlombok.org/并让它们对开发人员隐藏起来. (2认同)

Vla*_*cea 9

我更喜欢使用字段访问,原因如下:

  1. 在实现equals/hashCode并直接引用字段(而不是通过它们的getter)时,属性访问可能会导致非常讨厌的错误.这是因为代理仅在访问getter时初始化,而直接字段访问只返回null.

  2. 属性访问要求您注释的所有实用程序方法(如的addChild/removeChild之)作为@Transient.

  3. 通过字段访问,我们可以通过不暴露getter来隐藏@Version字段.一个getter也可以导致添加一个setter,并且version永远不应该手动设置该字段(这可能导致非常讨厌的问题).应通过OPTIMISTIC_FORCE_INCREMENTPESSIMISTIC_FORCE_INCREMENT显式锁定来触发所有版本增量.


Jus*_*ard 7

我认为对属性进行注释会更好,因为更新字段会直接破坏封装,即使您的ORM执行此操作也是如此.

这是一个很好的例子,说明它会烧掉你的位置:你可能希望在同一个地方(字段或属性)中使用hibernate验证器和持久性的注释.如果要测试在字段上注释的hibernate验证器驱动的验证,则不能使用实体的模拟将单元测试与验证器隔离.哎哟.

  • 这就是为什么你在字段上的访问器和持久性注释上放置验证器注释的原因 (2认同)

too*_*kit 6

我认为属性访问与字段访问在延迟初始化方面略有不同.

考虑以下2个基本bean的映射:

<hibernate-mapping package="org.nkl.model" default-access="field">
  <class name="FieldBean" table="FIELD_BEAN">
    <id name="id">
      <generator class="sequence" />
    </id>
    <property name="message" />
  </class>
</hibernate-mapping>

<hibernate-mapping package="org.nkl.model" default-access="property">
  <class name="PropBean" table="PROP_BEAN">
    <id name="id">
      <generator class="sequence" />
    </id>
    <property name="message" />
  </class>
</hibernate-mapping>
Run Code Online (Sandbox Code Playgroud)

以下单元测试:

@Test
public void testFieldBean() {
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    FieldBean fb = new FieldBean("field");
    Long id = (Long) session.save(fb);
    tx.commit();
    session.close();

    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    fb = (FieldBean) session.load(FieldBean.class, id);
    System.out.println(fb.getId());
    tx.commit();
    session.close();
}

@Test
public void testPropBean() {
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    PropBean pb = new PropBean("prop");
    Long id = (Long) session.save(pb);
    tx.commit();
    session.close();

    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    pb = (PropBean) session.load(PropBean.class, id);
    System.out.println(pb.getId());
    tx.commit();
    session.close();
}
Run Code Online (Sandbox Code Playgroud)

您将看到所需选择的细微差别:

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        FIELD_BEAN
        (message, id) 
    values
        (?, ?)
Hibernate: 
    select
        fieldbean0_.id as id1_0_,
        fieldbean0_.message as message1_0_ 
    from
        FIELD_BEAN fieldbean0_ 
    where
        fieldbean0_.id=?
0
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        PROP_BEAN
        (message, id) 
    values
        (?, ?)
1
Run Code Online (Sandbox Code Playgroud)

也就是说,调用fb.getId()需要选择,而pb.getId()不是.


Far*_*raz 5

默认情况下,JPA 提供程序访问实体字段的值,并使用实体\xe2\x80\x99s JavaBean 属性访问器(getter)和修改器(setter)方法将这些字段映射到数据库列。因此,实体中私有字段的名称和类型对于 JPA 来说并不重要。相反,JPA 仅查看\nJavaBean 属性访问器的名称和返回类型。您可以使用以下命令更改此设置@javax.persistence.Access,这使您能够显式指定 JPA 提供程序\n应采用的访问方法。

\n\n
@Entity\n@Access(AccessType.FIELD)\npublic class SomeEntity implements Serializable\n{\n...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

AccessType 枚举的可用选项是 PROPERTY(默认)和 FIELD。对于PROPERTY,提供程序可以使用JavaBean 属性方法获取和设置字段值。FIELD 使提供者使用实例字段获取和设置字段值。作为最佳实践,您应该坚持\n默认值并使用 JavaBean 属性,除非您有令人信服的理由不这样做。

\n\n

您可以将这些属性注释放在私有字段或公共访问器方法上。如果\n您使用AccessType.PROPERTY(默认)并注释私有字段而不是 JavaBean\n访问器,则字段名称必须与 JavaBean 属性名称匹配。但是,如果您对 JavaBean 访问器进行注释,则名称\n不必匹配。同样,如果您使用AccessType.FIELD并注释 JavaBean 访问器而不是字段,则字段名称也必须与 JavaBean 属性名称匹配。在这种情况下,如果您对字段进行注释,则它们不必匹配。最好保持一致并注释 JavaBean 访问器AccessType.PROPERTY和字段AccessType.FIELD

\n\n

重要的是,切勿在同一个实体中混合使用 JPA 属性注释和 JPA 字段注释。这样做会导致未指定的行为,并且很可能\n导致错误。

\n