ChatGPT解决这个技术问题 Extra ChatGPT

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

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

但我想知道哪个更好?通过属性访问还是通过字段访问?各自的优点和缺点是什么?


M
Martin

两者都有争论,但大多数都源于某些用户需求“如果需要添加逻辑怎么办”或“xxxx 破坏封装”。然而,没有人真正评论过这个理论,并给出了一个合理的论据。

当 Hibernate/JPA 持久化一个对象时,它实际上在做什么 - 好吧,它是在持久化对象的状态。这意味着以易于复制的方式存储它。

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

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

保存对象的内部状态不会破坏封装 - 否则您并不真正了解封装的含义以及它存在的原因。它就像对象序列化一样。

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

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


软件工程的答案是:使用访问器违反了 DRY。
@Martin我对您答案的最后一段有一个后续问题。您写了“一个示例可能是您希望避免保留计算值”。如何通过基于属性的访问来避免保留计算值?我知道你在争辩不这样做,但我没有明白这一点。你能解释一下吗?
@Geek现在我读回来了,我自己也不完全确定。我写这个答案已经两年了。我想一个更好的例子可能是您正在使用遗留数据库,并且数据以与您的对象模型不同的方式呈现 - 访问器可以提供两者之间的映射。
映射访问器的一个很好的用例是当您需要将映射信息添加到与任何持久性实现无关的第三方实体类的子类时。由于这些类中的字段是私有的,因此您必须覆盖访问器并向它们添加映射注释。另一种选择是使用 XML 映射,但有些事情很难做到。因此,如果您想要注释并映射第三方类,则将它们子类化并在访问器上添加注释是可行的方法。
@ElnurAbdurrakhimov 我们去了,一个很好的例子。谢谢。
C
Community

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

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

我相信字段访问是 Spring 推荐的成语,但我找不到支持它的参考。

有一个 related SO question 试图衡量性能并得出“没有区别”的结论。


如果您不在实体中提供 setter getter,那么该字段的用途是什么......您不能在应用程序中使用它,因为字段是私有的
没有为您的字段提供 getter 和 setter 是不好的做法吗?我想我在这里的评论并不总是正确的,因为我假设一个公共字段,而它显然可能是一个从未访问过的私有字段。
@anshulkatta 我觉得我真的应该解决您的问题,因为这就是封装的全部内容。理想情况下,您的所有字段都应该是私有的,并且如果可能的话,它们应该没有 getter 或 setter——这是您可以期望的最佳封装级别。考虑一个密码检查器。 2 个私有字段 passwordHash 和 failedAttempts。两者都可以是私有的,没有 getter 或 setter。它们由 bool checkPassword(string password) 使用,它进行散列,检查 passwordHash,然后更新 failedAttempts 并返回结果。无需其他代码访问这两个字段。
@anshulkatta 需要注意的是,在 OOP 中,getter 和 setter 是一种反模式,它们来自过程式编程,有一个类打破了封装原则,并生成了大量样板代码,即同一种代码一遍又一遍地重复。对象应该是不可变的,如果需要修改它的属性,它应该通过方法来完成,而不仅仅是返回属性的值。
不是这样。在这种情况下,“反模式”是一个见仁见智的问题,而不是教条。话虽如此,现场访问仍然是首选。一个更好的主意是完全避开 ORM 解决方案。了解如何编写正确的 SQL。
D
Dave

这是您必须使用属性访问器的情况。想象一下,你有一个 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
 }

现在究竟应该如何注释这个类?答案是您根本无法使用字段或属性访问对其进行注释,因为此时您无法指定目标实体。您必须注释具体的实现。但是由于在这个超类中声明了持久属性,因此您必须在子类中使用属性访问。

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


触摸。我没有想到这一点。我敢打赌,Hibernate 会为此踢出一些疯狂的 sql。
这个问题听起来很难在没有注释属性的情况下解决,但我从来没有遇到过我需要一个抽象泛型类的情况,该类具有很多我也想坚持的实现。通常,我创建一个类层次结构以使我的对象具有多态性(这使其成为通用类型的中断),而不仅仅是为了代码重用。无论如何,“大量的实现”通常都会违反 SRP,在这种情况下,我可能会将其移至单独的类中。有没有一个具体的例子可以让这个用例更加明显?
我唯一的具体例子是我的应用程序,我无法在 500 个字符的评论中描述它;-)
您可以使用 abstract T getOneThing()abstract void setOneThing(T thing),并使用字段访问。
S
Simulant

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

如果需要,我可以添加逻辑(如接受的答案中所述)。

它允许我在不初始化代理的情况下调用 foo.getId()(在使用 Hibernate 时很重要,直到 HHH-3718 得到解决)。

退税:

它使代码的可读性降低,例如,您必须浏览整个类以查看周围是否有@Transient。


但是,如果您使用没有“代理”的 JPA 提供程序,那么您就不会有这个问题,即“您的 JPA 提供程序正在强加给您”。
H
Hanno Fietz

我更喜欢访问器,因为我可以在需要时向访问器添加一些业务逻辑。这是一个例子:

@Entity
public class Person {

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

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


请注意,这是关于 ORM 如何访问您的字段/属性而不是您的应用程序代码。通过字段访问,您的 getNickName() 方法将完全按照您的预期工作。如果您在 getter/setter 之外使用持久的“属性”,则情况并非如此。这就是您可能会遇到属性访问和延迟加载问题的地方。所以不,我一般不同意这个论点。但是,上次我检查 Hibernate 时遇到了 @Id 字段的字段访问问题。
这个答案与问题无关。达菲莫的最佳答案
访问器内部不应该有任何业务逻辑。这不是明显的行为。
为什么此响应标记为正确?您可以映射字段并以相同的方式为其提供 setter/getter 是不正确的。
我即将对此进行试验,但这可以通过接口继承吗?
T
Thorben Janssen

让我试着总结一下选择基于字段的访问的最重要原因。如果您想深入了解,请阅读我博客上的这篇文章:Access Strategies in JPA and Hibernate – Which is better, field or property access?

到目前为止,基于字段的访问是更好的选择。这里有5个原因:

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

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

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

基于字段的访问的另一个优点是您的持久性提供程序,例如 Hibernate 或 EclipseLink,不使用实体属性的 getter 和 setter 方法。这意味着您不需要提供任何不应被您的业务代码使用的方法。 generated primary key attributes 或版本列的 setter 方法最常出现这种情况。您的持久性提供程序管理这些属性的值,您不应以编程方式设置它们。

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

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

例如,您可以使用它来wrap an optional association or attribute into a Java Optional.

原因 4:无需将实用程序方法标记为 @Transient

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

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

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

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


V
Vlad Mihalcea

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

在实现 equals/hashCode 和直接引用字段(而不是通过它们的 getter)时,属性访问可能会导致非常讨厌的错误。这是因为只有在访问 getter 时才初始化代理,而直接字段访问只会返回 null。属性访问要求您将所有实用程序方法(例如 addChild/removeChild)注释为@Transient。通过字段访问,我们可以通过完全不暴露 getter 来隐藏 @Version 字段。 getter 也可能导致添加 setter,并且永远不应该手动设置版本字段(这可能会导致非常讨厌的问题)。所有版本增量都应通过 OPTIMISTIC_FORCE_INCREMENT 或 PESSIMISTIC_FORCE_INCREMENT 显式锁定来触发。


1. 字段访问策略如何防止这种情况发生?无论访问方式如何,这似乎都是代理的普遍缺陷。 2. 你确定,它应该只是实用程序吸气剂吗? (但无论如何都是一个很好的论点)。 3. 在使用 DTO 而不是分离实体的情况下,公开 version 字段的访问器通常很有用。
1. 因为当代理被初始化时。 2. 100% 确定。 3. 这是一个有效的观点。
请原谅我的无知和可能缺乏文本解释,(我不是以英语为母语的人)。澄清一下,字段访问是指不需要正确的 getter/setter 方法,所以对于整体使用的 POJO,字段是公共的吗?但是您在第一个主题中说了些什么,而链接的博客文章则相反。我所理解的是,在使用代理和字段访问时不能使用 equals。
不。字段访问意味着 Hibernate 通过反射使用字段来读取实体属性。有关更多详细信息,请查看 Hibernate User Guide 中的访问类型部分。
0
01es

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

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


我是 Hibernate 的新手,并且在同一个问题上苦苦挣扎。我认为这篇文章提供了最明确的答案。谢谢你。
C
Community

如果您想在设置器中做更多的事情而不仅仅是设置值(例如加密或计算),我强烈建议您在获取器(属性访问)上进行字段访问而不是注释。

属性访问的问题是在加载对象时也会调用 setter。在我们想要引入加密之前,这对我来说已经好几个月了。在我们的用例中,我们想在 setter 中加密一个字段并在 getter 中解密它。现在属性访问的问题是,当 Hibernate 加载对象时,它还调用 setter 来填充字段,从而再次加密加密的值。这篇文章还提到了这一点:Java Hibernate: Different property set function behavior depending on who is calling it

这让我很头疼,直到我记得字段访问和属性访问之间的区别。现在,我已将所有注释从属性访问移至字段访问,并且现在可以正常工作。


是的——我发现如果你使用属性访问,除了设置字段值之外,你真的不能在你的设置器中做任何事情。
+1 远离 getter/setter。我使用 projectlombok.org 并将它们隐藏在开发人员面前。
J
Justin Standard

我认为注释属性更好,因为更新字段直接破坏封装,即使您的 ORM 这样做。

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


这就是为什么您将验证器注释放在访问器上,将持久性注释放在字段上
J
Joshua Taylor

我相信属性访问与字段访问在延迟初始化方面略有不同。

考虑以下 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>

以及以下单元测试:

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

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

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

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


这很好笑! :) 但我敢肯定,这是一种特定于实现的行为。我
是的,我想这是因为只有持久类被检测了。然而,这很可惜,因为 id 字段通常是没有业务价值且不需要任何访问器的字段。
F
Faraz

默认情况下,JPA 提供者访问实体字段的值,并使用实体的 JavaBean 属性访问器(getter)和修改器(setter)方法将这些字段映射到数据库列。因此,实体中私有字段的名称和类型与 JPA 无关。相反,JPA 只查看 JavaBean 属性访问器的名称和返回类型。您可以使用 @javax.persistence.Access 注释来更改它,这使您能够明确指定 JPA 提供程序应采用的访问方法。

@Entity
@Access(AccessType.FIELD)
public class SomeEntity implements Serializable
{
...
}

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

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

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


M
Manuel Palacio

Are we there yet

这是一个旧的演示文稿,但 Rod 建议对属性访问进行注释会鼓励贫乏的域模型,并且不应该是注释的“默认”方式。


J
Jiří Vypědřík

支持字段访问的另一点是,否则您将被迫公开集合的设置器,对我来说,这是一个坏主意,因为将持久集合实例更改为不受 Hibernate 管理的对象肯定会破坏您的数据一致性。

因此,我更喜欢将集合作为受保护字段初始化为默认构造函数中的空实现,并且只公开它们的 getter。然后,只有像 clear()remove()removeAll() 等这样的托管操作是可能的,它们永远不会让 Hibernate 不知道更改。


您不会被迫暴露任何东西,因为 setter 可以受到保护。此外,这些设置器不是正在实现的接口的一部分,因此即使它们是公共的,它们也不容易访问。
佚名

我更喜欢字段,但我遇到了一种情况,似乎迫使我将注释放在 getter 上。

对于 Hibernate JPA 实现,@Embedded 似乎不适用于字段。所以这必须在吸气剂上进行。一旦你把它放在 getter 上,那么各种 @Column 注释也必须放在 getter 上。 (我认为 Hibernate 不希望在这里混合字段和 getter。)一旦您将 @Column 放在一个类中的 getter 上,从头到尾这样做可能是有意义的。


J
Joshua Taylor

我喜欢字段访问器。代码更干净。所有的注释都可以放在一个类的一个部分中,代码更容易阅读。

我发现了属性访问器的另一个问题:如果你的类上有 getXYZ 方法,这些方法没有被注释为与持久属性相关联,hibernate 会生成 sql 来尝试获取这些属性,从而导致一些非常混乱的错误消息。浪费了两个小时。这段代码不是我写的;我过去一直使用字段访问器,从未遇到过这个问题。

此应用程序中使用的休眠版本:

<!-- hibernate -->
<hibernate-core.version>3.3.2.GA</hibernate-core.version>
<hibernate-annotations.version>3.4.0.GA</hibernate-annotations.version>
<hibernate-commons-annotations.version>3.1.0.GA</hibernate-commons-annotations.version>
<hibernate-entitymanager.version>3.4.0.GA</hibernate-entitymanager.version>

u
user10801787

您应该选择通过字段访问而不是通过属性访问。使用字段,您可以限制发送和接收的数据。使用 via 属性,您可以作为主机发送更多数据,并设置 G 面额(工厂设置了大部分属性)。


V
Vladimir Dyuzhev

通常 bean 是 POJO,所以无论如何它们都有访问器。

所以问题不是“哪个更好?”,而只是“何时使用字段访问?”。答案是“当您不需要该领域的 setter/getter 时!”。


问题是您不能在 POJO 中混合使用字段访问和属性访问 - 您必须选择一个
真的吗?我一定忘记了。无论如何,我总是使用 POJO 和 d 访问器。
请注意,使用 JPA 2.0(在提出此问题时尚未出现),您现在可以使用 @AccessType 注释混合访问类型。
s
sundary

我对休眠中的访问类型有同样的问题,并找到了 some answers here


C
Community

我在这里解决了延迟初始化和字段访问Hibernate one-to-one: getId() without fetching entire object


S
Steve11235

我们创建了实体 bean 并使用了 getter 注释。我们遇到的问题是:某些实体对某些属性的更新时间有复杂的规则。解决方案是在每个 setter 中设置一些业务逻辑,以确定实际值是否更改,如果更改,是否应允许更改。当然,Hibernate 总是可以设置属性,所以我们最终得到了两组 setter。很难看。

阅读以前的帖子,我还看到从实体内部引用属性可能会导致无法加载集合的问题。

最重要的是,我会倾向于在未来对这些领域进行注释。


u
user261548

我正在考虑这个,我选择了方法访问器

为什么?

因为字段和方法访问器是相同的,但如果以后我需要在加载字段中添加一些逻辑,我保存移动放置在字段中的所有注释

问候

格鲁哈特


e
ely

为了使您的类更干净,请将注释放在字段中,然后使用 @Access(AccessType.PROPERTY)


m
moueza

两个都 :

EJB3 规范要求您在将被访问的元素类型上声明注释,即,如果您使用属性访问,则为 getter 方法,如果您使用字段访问,则为字段。

https://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/#entity-mapping


V
Vikki

AccessType.PROPERTY:EJB 持久性实现将通过 JavaBean“setter”方法将状态加载到您的类中,并使用 JavaBean“getter”方法从您的类中检索状态。这是默认设置。

AccessType.FIELD:直接从类的字段中加载和检索状态。您不必编写 JavaBean “getters”和“setters”。