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
}
@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.
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.
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!
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.
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.
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With