Skip to content

Improve SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS feature #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 23, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@ public enum Feature {
USE_TRANSIENT_ANNOTATION(true),

/**
* If FORCE_LAZY_LOADING is false lazy-loaded object should be serialized as map IdentifierName=>IdentifierValue
* instead of null (true); or serialized as nulls (false)
* If FORCE_LAZY_LOADING is false, this feature serializes uninitialized lazy loading proxies as
* <code>{"identifierName":"identifierValue"}</code> rather than <code>null</code>.
* <p>
* Default value is false.
* Default value is false.
* <p>
* Note that the name of the identifier property can only be determined if
* <ul>
* <li>the {@link Mapping} is provided to the Hibernate4Module, or </li>
* <li>the persistence context that loaded the proxy has not yet been closed, or</li>
* <li>the id property is mapped with property access (for instance because the {@code @Id}
* annotation is applied to a method rather than a field)</li>
* </ul>
* Otherwise, the entity name will be used instead.
*/
SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS(false),

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.fasterxml.jackson.datatype.hibernate4;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

import com.fasterxml.jackson.core.*;
Expand All @@ -18,6 +20,7 @@
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.proxy.pojo.BasicLazyInitializer;

/**
* Serializer to use for values proxied using {@link org.hibernate.proxy.HibernateProxy}.
Expand Down Expand Up @@ -174,15 +177,18 @@ protected Object findProxied(HibernateProxy proxy)
LazyInitializer init = proxy.getHibernateLazyInitializer();
if (!_forceLazyLoading && init.isUninitialized()) {
if (_serializeIdentifier) {
final String idName;
String idName;
if (_mapping != null) {
idName = _mapping.getIdentifierPropertyName(init.getEntityName());
} else {
final SessionImplementor session = init.getSession();
if (session != null) {
idName = session.getFactory().getIdentifierPropertyName(init.getEntityName());
} else {
idName = init.getEntityName();
idName = ProxyReader.getIdentifierPropertyName(init);
if (idName == null) {
idName = init.getEntityName();
}
}
}
final Object idValue = init.getIdentifier();
Expand All @@ -194,4 +200,40 @@ protected Object findProxied(HibernateProxy proxy)
}
return init.getImplementation();
}

// Alas, hibernate offers no public api to access this information, so we must resort to ugly hacks ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to expand just slightly on logic for accessing information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What to you mean by "expand on logic"? (How can I document the absence of a suitable API? Should I list what I tried, and why that didn't work?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was just to explain what is being done; mention that there's no public API is fine, but explain what information, and for what reason. If that is covered in other parts that may be fine.
There's no need (IMO) to explain what else has been tried; just what the intent is.

protected static class ProxyReader {

// static final so the JVM can inline the lookup
private static final Field getIdentifierMethod;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit confusing to call field a method? Should name be something bit more descriptive? Also, might make sense to explain what the intent is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getIdentifierMethod is the name of the field being reflected ... should I rename to getIdentifierMethodField?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmh. Maybe it'd be best to just add a Javadoc comment to explain that?


static {
try {
getIdentifierMethod = BasicLazyInitializer.class.getDeclaredField("getIdentifierMethod");
getIdentifierMethod.setAccessible(true);
} catch (Exception e) {
// should never happen: the field exists in all versions of hibernate 4 and 5
throw new RuntimeException(e);
}
}

/**
* @return the name of the identifier property, or null if the name could not be determined
*/
static String getIdentifierPropertyName(LazyInitializer init) {
try {
Method idGetter = (Method) getIdentifierMethod.get(init);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So.... Method is assigned to a field of LazyInitializer class which is presumably generated?

Copy link
Contributor Author

@bedag-moo bedag-moo Aug 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LazyInitializer itself not generated (it is the Javassist MethodHandler the generated proxy class delegates to. The LazyInitializer has special handling for invocations of the getter of the persistent identity, and therefore stores the corresponding Method.

I use reflection here because the field is private and has no getter :-(

if (idGetter == null) {
return null;
}
String name = idGetter.getName();
if (name.startsWith("get")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably has to remain here; ideally BeanUtil from jackson-databind could be used but unfortunately okNameForIsGetter requires AnnotatedMethod, not base Method so it can not be used.

But there is Introspector.decapitalize():

http://docs.oracle.com/javase/7/docs/api/java/beans/Introspector.html#decapitalize(java.lang.String)

which is what you can call, so:

String name = Introspector.decapitalize(name.substring(3));

if name starts with "get". And the reason to call the method is that getURL() should result in URL, and NOT uRL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, thanks :-)

name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
}
return name;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module.Feature;
import com.fasterxml.jackson.datatype.hibernate4.data.Customer;
import com.fasterxml.jackson.datatype.hibernate4.data.Payment;

Expand Down Expand Up @@ -54,4 +56,25 @@ public void testGetCustomerJson() throws Exception
emf.close();
}
}

@Test
public void testSerializeIdentifierFeature() throws JsonProcessingException {
Hibernate4Module module = new Hibernate4Module();
module.enable(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);
ObjectMapper objectMapper = new ObjectMapper().registerModule(module);

EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistenceUnit");
try {
EntityManager em = emf.createEntityManager();
Customer customerRef = em.getReference(Customer.class, 103);
em.close();
assertFalse(Hibernate.isInitialized(customerRef));

String json = objectMapper.writeValueAsString(customerRef);
assertFalse(Hibernate.isInitialized(customerRef));
assertEquals("{\"customerNumber\":103}", json);
} finally {
emf.close();
}
}
}