Lombok @Builder 和 JPA 默认构造函数

118

我正在使用Project Lombok和Spring Data JPA。 有没有办法将Lombok的@Builder与JPA默认构造函数连接起来?

代码:

@Entity 
@Builder
class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}
据我所知,JPA需要默认构造函数,但可以通过@Builder注解来覆盖默认构造函数。是否有任何解决方法?
这段代码给我报错: org.hibernate.InstantiationException: No default constructor for entity: : app.domain.model.Person

5
尝试添加 @NoArgsConstructor。https://projectlombok.org/api/lombok/NoArgsConstructor.html - Robert Niestroj
1
尝试添加一个无参构造函数。据我所知,@Builder 不会覆盖您的无参构造函数。 - Ken Chan
2
是的,但@Id是必填字段。NoArgs不够用。 - krzakov
3
我不明白你想要什么。你怎么能有一个无参构造函数来创建值呢?@Id 要么是必需的,要么不是。如果需要,你需要构造函数参数;否则,你可以使用 NoArgs。我在这里漏掉了什么? - Roel Spilker
8个回答

127

更新

基于反馈和John的回答,我已经更新了答案,不再使用@Tolerate@Data,而是通过@Getter@Setter创建访问器和变量,通过@NoArgsConstructor创建默认构造函数,最终通过@AllArgsConstructor创建建造者所需的所有参数构造函数。

由于您想要使用建造者模式,我想您希望限制构造函数和变量方法的可见性。 为实现此目的,我们通过@NoArgsConstructor@AllArgsConstructor注释上的access属性以及@Setter注释上的value属性设置可见性为package private

重要提示

请记得正确覆盖toStringequalshashCode方法。 有关详细信息,请参阅Vlad Mihalcea的以下文章:

package com.stackoverflow.SO34299054;

import static org.junit.Assert.*;

import java.util.Random;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.junit.Test;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@SuppressWarnings("javadoc")
public class Answer {

    @Entity
    @Builder(toBuilder = true)
    @AllArgsConstructor(access = AccessLevel.PACKAGE)
    @NoArgsConstructor(access = AccessLevel.PACKAGE)
    @Setter(value = AccessLevel.PACKAGE)
    @Getter
    public static class Person {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;

        /*
         * IMPORTANT:
         * Set toString, equals, and hashCode as described in these
         * documents:
         * - https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
         * - https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
         * - https://vladmihalcea.com/hibernate-facts-equals-and-hashcode/
         */
    }

    /**
     * Test person builder.
     */
    @Test
    public void testPersonBuilder() {

        final Long expectedId = new Random().nextLong();
        final Person fromBuilder = Person.builder()
            .id(expectedId)
            .build();
        assertEquals(expectedId, fromBuilder.getId());

    }

    /**
     * Test person constructor.
     */
    @Test
    public void testPersonConstructor() {

        final Long expectedId = new Random().nextLong();
        final Person fromNoArgConstructor = new Person();
        fromNoArgConstructor.setId(expectedId);
        assertEquals(expectedId, fromNoArgConstructor.getId());
    }
}

使用 @Tolerate@Data 的旧版本:

使用 @Tolerate 可以添加一个无参构造函数。

由于您想要使用生成器模式,我想您希望控制 setter 方法的可见性。

@Data 注释使生成的 setter 方法为 public,将 @Setter(value = AccessLevel.PROTECTED) 应用于字段会使它们为 protected

记得正确地重写 toStringequalshashCode。请参考 Vlad Mihalcea 的以下文章获取详细信息:

package lombok.javac.handlers.stackoverflow;

import static org.junit.Assert.*;

import java.util.Random;

import javax.persistence.GenerationType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.Setter;
import lombok.experimental.Tolerate;

import org.junit.Test;

public class So34241718 {

    @Builder
    @Data
    public static class Person {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Setter(value = AccessLevel.PROTECTED)
        Long id;

        @Tolerate
        Person() {}

       /* IMPORTANT:
          Override toString, equals, and hashCode as described in these 
          documents:
          - https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
          - https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
          - https://vladmihalcea.com/hibernate-facts-equals-and-hashcode/
          */
    }

    @Test
    public void testPersonBuilder() {

        Long expectedId = new Random().nextLong();
        final Person fromBuilder = Person.builder()
            .id(expectedId)
            .build();
        assertEquals(expectedId, fromBuilder.getId());

    }

    @Test
    public void testPersonConstructor() {

        Long expectedId = new Random().nextLong();
        final Person fromNoArgConstructor = new Person();
        fromNoArgConstructor .setId(expectedId);
        assertEquals(expectedId, fromNoArgConstructor.getId());
    }
}

2
我和krzakov有同样的问题,但是通过你的提示,我使用了@Tolerate解决了它。感谢Jeff。但是你添加@Data注释的原因是什么呢?在这种情况下,setter不是必需的,而@Data会覆盖equal/hash/toString的默认行为,可能会导致问题。 - wollodev
3
不要在实体类中使用@Data注解。 - wst
3
@Data 默认会利用所有字段来生成 equalshashCode 方法,包括 id 字段。举个例子,你可能会在保存前后使用相同的实体表示方式,但是从 Java 的角度来看,它们是不同的实例(一个有 id,一个没有)。这可能导致混乱和一致性问题。如果你想要覆盖这些方法,可以使用 @Data。Hibernate 文档中有一章节讨论了这个问题:http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode - wst
@wst,这很合理。谢谢。 - srnjak
@wollodev之前已经写过了,但我想确认一下,现在引入Builder后是否还需要使用setters? - Keyhan
显示剩余7条评论

109

您还可以通过在类定义中结合使用@Data @Builder @NoArgsConstructor @AllArgsConstructor显式解决此问题。


注意,这不会自动创建访问器方法(getter)。 - Jeff
@Jeff 然后只需添加 @Data - Deniss M.
6
您不希望使用 @Data,因为它会生成 equals、hashCode 和 toString 方法,在 jpa 实体的情况下应该手动编写。请参阅上面我的回答中的详细信息。 - Jeff

14

看起来注解的顺序在这里很重要,使用相同的注解,但不同的顺序,你可以使代码工作或者不工作。

这是一个不工作的例子:

@AllArgsConstructor
@Builder
@Data
@Entity
@EqualsAndHashCode
@NoArgsConstructor
@RequiredArgsConstructor
@Table
@ToString
public class Person implements Serializable {
  private String name;
}

而这是一个有效的示例:

@Builder
@Data
@Entity
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
@Table
@ToString
public class Person implements Serializable {
  private String name;
}

所以一定要将@Builder注解放在最顶部位置,在我的情况下,我遇到了这个错误,因为我想按字母顺序排序注解。


所以请确保@Builder注解位于代码中最高的位置。在我的例子中,我遇到了这个错误是因为我想要按照字母表的顺序对注解进行排序。

10
如果在构造函数上使用注释lombok.Tolerate,同时在某些属性上使用注释javax.validation.constraints.NotNull,则Sonarqube将把它标记为关键错误: PROPERTY被标记为"javax.validation.constraints.NotNull",但在此构造函数中未初始化。 如果项目使用SpringData和JPA,可以使用org.springframework.data.annotation.PersistenceConstructor(Spring注释,不是JPA!)来解决这个问题。
然后,与Lombok结合使用,注释将如下所示:
@RequiredArgsConstructor(onConstructor = @__(@PersistenceConstructor))

对于Lombok的构建器,您还需要添加以下内容:
@Builder
@AllArgsConstructor

5

为了使用以下组合

  • lombok
  • JPA
    • CRUD
    • 适当的@EqualsAndHashCode
  • 不可变性 - 公共最终字段
  • 无getter方法
  • 无setter方法
  • 通过@Builder@With进行更改

我使用了:

//Lombok & JPA
//https://dev59.com/tVsX5IYBdhLWcg3wFsPV

//Mandatory in conjunction with JPA: an equal based on fields is not desired
@lombok.EqualsAndHashCode(onlyExplicitlyIncluded = true)
//Mandatory in conjunction with JPA: force is needed to generate default values for final fields, that will be overriden by JPA
@lombok.NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
//Hides the constructor to force usage of the Builder.
@lombok.AllArgsConstructor(access = AccessLevel.PRIVATE)
@lombok.ToString
//Good to just modify some values
@lombok.With
//Mandatory in conjunction with JPA: Some suggest that the Builder should be above Entity - https://dev59.com/tVsX5IYBdhLWcg3wFsPV#52048267
//Good to be used to modify all values
@lombok.Builder(toBuilder = true)
//final fields needed for imutability, the default access to public - since are final is safe 
@lombok.experimental.FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
//no getters and setters
@lombok.Getter(value = AccessLevel.NONE)
@lombok.Setter(value = AccessLevel.NONE)

//JPA
@javax.persistence.Entity
@javax.persistence.Table(name = "PERSON_WITH_MOTTO")
//jpa should use field access 
@javax.persistence.Access(AccessType.FIELD)
public class Person {
  @javax.persistence.Id
  @javax.persistence.GeneratedValue
  //Used also automatically as JPA
  @lombok.EqualsAndHashCode.Include
  Long id;
  String name;
  String motto;
}

5
使用@NoArgsConstructor@AllArgsConstructor有助于解决具有@Builder的默认构造函数问题。
例如:
@Entity 
@Builder
@NoArgsConstructor
@AllArgsContructor
class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

这是因为@Builder需要所有参数构造函数,只指定默认构造函数会导致问题。更多解释请参考:https://github.com/rzwitserloot/lombok/issues/1389#issuecomment-369404719

3
我使用了以下所有注释来解决这个问题:
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor(access = AccessLevel.PACKAGE)

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接