简体   繁体   中英

JPA / Hibernate is Inserting NULL When Default Value is Set on Field

I have an entity that has a Boolean value that gets persisted to the DB as a 'Y' or 'N' character:

@Data
@Entity
@Table(name = "MYOBJECT", schema = "MYSCHEMA")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyObject {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MYOBJECT_SEQ")
    @SequenceGenerator(name = "MYOBJECT_SEQ", sequenceName = "MYSCHEMA.MYOBJECT_SEQ")
    private Long id;

    @NotEmpty(message = "Name may not be empty")
    @SafeHtml(whitelistType = WhiteListType.NONE, message = "Name may not contain html or javascript")
    @Column(name = "NAME", nullable = false, length = 60)
    private String name;

    // Other fields... 

    @Type(type = "yes_no")
    @Column(name = "ACTIVE", length = 1, nullable = false)
    private Boolean isActive = Boolean.TRUE;

    @SafeHtml(whitelistType = WhiteListType.NONE, message = "username may not contain html or javascript")
    @Column(name = "USERNAME", nullable = false, length = 30)
    private String username;
}

However when I tried to run a test to save this to the database, I would get a NULL Constraint Violation error for activeIndicator , despite the fact that I was initializing it to Boolean.TRUE , as you can see in the code above. When I turned on debug logs for Hibernate, I was able to verify that it was sending NULL in the insert statement . Below is my test code:

@Test
public void testSave() {
    // Setup Data
    final String NAME = "Test name";
    final String USERNAME = "Test username";

    MyObject stagedObject = MyObject.builder().name(NAME).username(USERNAME).build();

    // Run test
    MyObject savedObject = repo.save(stagedObject);
    entityManager.flush(); // force JPA to sync with db
    entityManager.clear(); // force JPA to fetch a fresh copy of the objects instead of relying on what's in memory

    // Assert
    MyObject fetchedObject = repo.findOne(savedObject.getId());
    assertThat(fetchedObject.getIsActive(), is(Boolean.TRUE));
    // other assertions
}

Turns out that the fault didn't lie with Hibernate, but with Lombok. If you're using annotations such as @Data , @EqualsAndHashCodeOf , @NoArgsConstructor , @AllArgsConstructor , or @Builder , your domain object may be using Lombok. (Groovy has some similar annotations, just check if your imports are using groovy.transform or lombok as the package prefix)

The bit of code causing my problems was in my test class:

MyObject.builder().name(NAME).username(USERNAME).build();

After much trial and error, it became apparent that while my JPA/Hibernate setup was correct,the initialization default:

private Boolean activeIndicator = Boolean.TRUE;

was not being executed.

It took some digging, but apparently Lombok's Builder does not use Java's initialization , but its own means to create the object. This means that initialization defaults are overlooked when you call build() , hence the field will remain NULL if you don't explicitly set it in your builder chain. Note that normal Java initialization and constructors are unaffected by this; the problem ONLY manifests itself when creating objects with Lombok's builder.

The issue detailing this problem and Lombok's explanation on why they won't implement a fix is detailed here: https://github.com/rzwitserloot/lombok/issues/663#issuecomment-121397262

There are many workarounds to this, and the one you may want to use depends on your needs.

  • Custom Constructor(s) : One can create custom constructors either in place of, or alongside the builder. When you need defaults to be set, use your constructors instead of the builder. - This solution makes it seem like you shouldn't even bother with a builder. The only reason you may want to keep the builder is if the builder is capable of delegating creation to your constructors, therefore getting the default values initialized when it builds. I have no idea if that's possible as I haven't tested it myself and I decided to go a different route. Do respond if you've looked into this option further.
  • Overriding the Build Method: If you're going to be using the Builder extensively and don't want to be writing a bunch of constructors, this solution is for you. You can set defaults in your own implementation of the build method and delegate to the super to do the actual build. Just be sure to document your code well so future maintainers know to set defaults on both the object and in the build method so they're set no matter how the object is initialized.
  • JPA PrePersist Annotation: If your requirements for defaults are for database constraints and you're using JPA or Hibernate, this option is available to you. This annotation will run the method it's attached to before every insert and update of the entity, regardless of how it was initialized. This is nice so that you don't have to set defaults in two places like you'd have to with the build override strategy. I went with this option as my business requirements have a strong defaulting requirement on this field. I added the code below to my entity class to fix the problem. I also removed the "= Boolean.TRUE" part of the isActive field declaration as it was no longer necessary.

     @PrePersist public void defaultIsActive() { if(isActive == null) { isActive = Boolean.TRUE; } } 

在具有默认值的字段上使用@ Builder.Deafult批注。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM