Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
}
like image 220
AForsberg Avatar asked Oct 11 '16 14:10

AForsberg


People also ask

What does @ID do in JPA?

@Id will only declare the primary key. it will not insert generated value. if you use @GeneratedValue then it will generate the value of the field.

What is nullable true in hibernate?

Hibernate doesn't perform any validation if you annotate an attribute with @Column(nullable = false). This annotation only adds a not null constraint to the database column, if Hibernate creates the database table definition. The database then checks the constraint, when you insert or update a record.

What is the use of @entity in hibernate?

Hibernate will scan that package for any Java objects annotated with the @Entity annotation. If it finds any, then it will begin the process of looking through that particular Java object to recreate it as a table in your database!

What is nullable false in hibernate?

The @Column(nullable = false) Annotation It's used mainly in the DDL schema metadata generation. This means that if we let Hibernate generate the database schema automatically, it applies the not null constraint to the particular database column.


1 Answers

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;
        }
    }
    
like image 113
AForsberg Avatar answered Oct 14 '22 10:10

AForsberg