I'm using Spring Boot, Spring Data REST, Spring HATEOAS, Hibernate, Spring Data Envers. I'm auditing my entities in order to tracking all changes. I am consuming REST service from an Angular 4 application. My goal is display a lix of boxes for each Revision (envers) showing only modified properties.
My entities are annotated with @Audited
. This is my model:
@Audited
public class LicensePlate extends AbstractEntity {
private static final long serialVersionUID = -6871697166535810224L;
@NotEmpty
@ColumnTransformer(read = "UPPER(licensePlate)", write = "UPPER(?)")
@Column(nullable = false, unique = true)
private String licensePlate;
@NotNull
@Range(min = 1, max = 99)
@Column(nullable = false, columnDefinition = "INTEGER default 50")
private int seats = 50;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Country country;
As you can see, Country is LAZY and I can't to set it EAGER.
I found this class that recursively check lazy properties in the model and initialize them.
public class HibernateUtil {
public static byte[] hibernateCollectionPackage = "org.hibernate.collection".getBytes();
public static void initializeObject(Object o, String insidePackageName) {
Set<Object> seenObjects = new HashSet<Object>();
initializeObject(o, seenObjects, insidePackageName.getBytes());
seenObjects = null;
}
private static void initializeObject(Object o, Set<Object> seenObjects, byte[] insidePackageName) {
seenObjects.add(o);
Method[] methods = o.getClass().getMethods();
for (Method method : methods) {
String methodName = method.getName();
// check Getters exclusively
if (methodName.length() < 3 || !"get".equals(methodName.substring(0, 3)))
continue;
// Getters without parameters
if (method.getParameterTypes().length > 0)
continue;
int modifiers = method.getModifiers();
// Getters that are public
if (!Modifier.isPublic(modifiers))
continue;
// but not static
if (Modifier.isStatic(modifiers))
continue;
try {
// Check result of the Getter
Object r = method.invoke(o);
if (r == null)
continue;
// prevent cycles
if (seenObjects.contains(r))
continue;
// ignore simple types, arrays und anonymous classes
if (!isIgnoredType(r.getClass()) && !r.getClass().isPrimitive() && !r.getClass().isArray()
&& !r.getClass().isAnonymousClass()) {
// ignore classes out of the given package and out of the
// hibernate collection
// package
if (!isClassInPackage(r.getClass(), insidePackageName) && !isClassInPackage(r.getClass(), hibernateCollectionPackage)) {
continue;
}
// initialize child object
Hibernate.initialize(r);
// traverse over the child object
initializeObject(r, seenObjects, insidePackageName);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
return;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
} catch (IllegalAccessException e) {
e.printStackTrace();
return;
}
}
}
private static final Set<Class<?>> IGNORED_TYPES = getIgnoredTypes();
private static boolean isIgnoredType(Class<?> clazz) {
return IGNORED_TYPES.contains(clazz);
}
private static Set<Class<?>> getIgnoredTypes() {
Set<Class<?>> ret = new HashSet<Class<?>>();
ret.add(Boolean.class);
ret.add(Character.class);
ret.add(Byte.class);
ret.add(Short.class);
ret.add(Integer.class);
ret.add(Long.class);
ret.add(Float.class);
ret.add(Double.class);
ret.add(Void.class);
ret.add(String.class);
ret.add(Class.class);
ret.add(Package.class);
return ret;
}
private static Boolean isClassInPackage(Class<?> clazz, byte[] insidePackageName) {
Package p = clazz.getPackage();
if (p == null)
return null;
byte[] packageName = p.getName().getBytes();
int lenP = packageName.length;
int lenI = insidePackageName.length;
if (lenP < lenI)
return false;
for (int i = 0; i < lenI; i++) {
if (packageName[i] != insidePackageName[i])
return false;
}
return true;
}
}
This is the REST controller:
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/licensePlates/{id}/revisions/{revid}", method = RequestMethod.GET)
public ResponseEntity<?> findRevision(@PathVariable(value = "id") Long id, @PathVariable(value = "revid") Integer revId,
Pageable pageable) {
if (id != null) {
List<RestChange> changes = new ArrayList<>();
AuditReader reader = AuditReaderFactory.get(entityManager);
AuditQuery q = reader.createQuery().forEntitiesAtRevision(LicensePlate.class, revId);
LicensePlate current = (LicensePlate) q.getSingleResult();
HibernateUtil.initializeObject(current, "it.model");
//PREVIOUS
if (revId > 1) {
q = reader.createQuery().forEntitiesAtRevision(LicensePlate.class, revId - 1);
LicensePlate previous = (LicensePlate) q.getSingleResult();
HibernateUtil.initializeObject(previous, "it.model");
Diff diff = javers.compare(previous, current);
for (Change change : diff.getChanges()) {
log.debug("Change: " + change);
if (change instanceof ValueChange) {
ValueChange c = (ValueChange) change;
RestChange restChange = new RestChange();
restChange.setPropertyName(c.getPropertyName());
restChange.setPreviousValue(c.getLeft());
restChange.setValue(c.getRight());
changes.add(restChange);
} else if (change instanceof ReferenceChange) {
ReferenceChange c = (ReferenceChange) change;
RestChange restChange = new RestChange();
restChange.setPropertyName(c.getPropertyName());
restChange.setPreviousValue(c.getLeftObject().get().toString());
restChange.setValue(c.getRightObject().get().toString());
changes.add(restChange);
}
}
}
return new ResponseEntity<>(new Resources<>(changes), HttpStatus.OK);
} else {
throw new ResourceNotFoundException();
}
}
I debugged and the property country
in the current
and previous
beans remains a proxy even after the call to Hibernate.initialize. The only way I found to fix the problem is call Hibernate.initialize(current.getCountry()) and then re-set the value in the licensePlate
bean but I don't feel this is right.
I checked all others question with the similar problem and in all cases Hibernate.initialize was the solution of the question, but here seems it doesn't work. What am I doing wrong?