Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Expose Datastore LazyUtil #2418

Open
BenDol opened this issue Jun 6, 2020 · 5 comments
Open

Expose Datastore LazyUtil #2418

BenDol opened this issue Jun 6, 2020 · 5 comments
Labels
datastore GCP Datastore P3

Comments

@BenDol
Copy link

BenDol commented Jun 6, 2020

Expose spring datastore LazyUtil or an explaination why this shouldn't be exposed for use? Some utility methods like LazyUtil.isLazyAndNotLoaded would be very useful for me to process data for transactions, etc.

Basic example use case:

User user = userRepository.findByName("John Doe");
Set<Group> groupProxy = user.getGroups();
if(LazyUtil.isLazyAndNotLoaded(groupProxy)) {
	LazyUtil.SimpleLazyDynamicInvocationHandler ih = LazyUtil.getProxy(groupProxy);
	Class type = ih.getType();
	// override proxy set before serialization (avoid lazy loading all data upon serialization)
	user.setGroups((Set<Group>) type.newInstance());
}

This would mean exposing the invocation handler and making use the SimpleDynamicIncovationHandler stored the proxies type. From my testing I was unable to get the proxies underlying type any other way.

LazyUtil.java

/**
 * Utilities used to support lazy loaded properties.
 *
 * @author Dmitry Solomakha
 *
 * @since 1.2.2
 */
public final class LazyUtil {

	static private final ObjenesisStd objenesis = new ObjenesisStd();

	private LazyUtil() {
	}

	/**
	 * Returns a proxy that lazily loads the value provided by a supplier. The proxy also
	 * stores the key(s) that can be used in case the value was not loaded. If the type of the
	 * value is interface, {@link java.lang.reflect.Proxy} is used, otherwise cglib proxy is
	 * used (creates a sub-class of the original type; the original class can't be final and
	 * can't have final methods).
	 * @param supplierFunc a function that provides the value
	 * @param type the type of the value
	 * @param keys Datastore key(s) that can be used when the parent entity is saved
	 * @return true if the object is a proxy that was not evaluated
	 */
	static <T> T wrapSimpleLazyProxy(Supplier<T> supplierFunc, Class<T> type, Value keys) {
		if (type.isInterface()) {
			return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type},
				new SimpleLazyDynamicInvocationHandler<>(type, supplierFunc, keys));
		}
		Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(type));
		factory.setCallbacks(new Callback[] { new SimpleLazyDynamicInvocationHandler<>(type, supplierFunc, keys) });

		return (T) factory;
	}

	private static Class<?> getEnhancedTypeFor(Class<?> type) {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(type);
		enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);

		return enhancer.createClass();
	}

	/**
	 * Check if the object is a lazy loaded proxy that hasn't been evaluated.
	 * @param object an object
	 * @return true if the object is a proxy that was not evaluated
	 */
	public static boolean isLazyAndNotLoaded(Object object) {
		SimpleLazyDynamicInvocationHandler handler = getProxy(object);
		if (handler != null) {
			return !handler.isEvaluated() && handler.getKeys() != null;
		}
		return false;
	}

	/**
	 * Extract keys from a proxy object.
	 * @param object a proxy object
	 * @return list of keys if the object is a proxy, null otherwise
	 */
	public static Value getKeys(Object object) {
		SimpleLazyDynamicInvocationHandler handler = getProxy(object);
		if (handler != null) {
			if (!handler.isEvaluated()) {
				return handler.getKeys();
			}
		}
		return null;
	}

	public static SimpleLazyDynamicInvocationHandler getProxy(Object object) {
		if (Proxy.isProxyClass(object.getClass())
				&& (Proxy.getInvocationHandler(object) instanceof SimpleLazyDynamicInvocationHandler)) {
			return (SimpleLazyDynamicInvocationHandler) Proxy
					.getInvocationHandler(object);
		}
		else if (object instanceof Factory) {
			Callback[] callbacks = ((Factory) object).getCallbacks();
			if (callbacks != null && callbacks.length == 1
					&& callbacks[0] instanceof SimpleLazyDynamicInvocationHandler) {
				return (SimpleLazyDynamicInvocationHandler) callbacks[0];
			}
		}
		return null;
	}

	/**
	 * Proxy class used for lazy loading.
	 */
	public static final class SimpleLazyDynamicInvocationHandler<T> implements InvocationHandler, MethodInterceptor {

		private final Supplier<T> supplierFunc;

		private final Value keys;

		private boolean isEvaluated = false;

		private Class<T> type;
		private T value;

		private SimpleLazyDynamicInvocationHandler(Class<T> type, Supplier<T> supplierFunc, Value keys) {
			Assert.notNull(supplierFunc, "A non-null supplier function is required for a lazy proxy.");
			Assert.notNull(supplierFunc, "A non-null class type is required for a lazy proxy.");
			this.type = type;
			this.supplierFunc = supplierFunc;
			this.keys = keys;
		}

		private boolean isEvaluated() {
			return this.isEvaluated;
		}

		public Value getKeys() {
			return this.keys;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if (!this.isEvaluated) {
				T value = this.supplierFunc.get();
				if (value == null) {
					throw new DatastoreDataException("Can't load referenced entity");
				}
				this.value = value;

				this.isEvaluated = true;
			}
			return method.invoke(this.value, args);
		}

		@Override
		public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
			return invoke(o, method, objects);
		}

		public Class<T> getType() {
			return type;
		}
	}
}
@dzou dzou added datastore GCP Datastore P3 labels Jun 8, 2020
@dmitry-s
Copy link
Contributor

dmitry-s commented Jun 8, 2020

@BenDol could you clarify your use case? I think we will be able to help you better if we have more info.

Thanks

@BenDol
Copy link
Author

BenDol commented Jun 8, 2020

@dmitry-s sure, so the main use case I have at the moment is being able to determine a beans loaded state on its properties for serializing the object. When the @LazyReference is used the serializer invokes a chain load of all the properties while it's serializing. So I want to be able to run a check on the lazy properties before serializing the bean to make sure that doesn't happen. For example (as I show above):

User user = userRepository.findByName("John Doe");
Set<Group> groupProxy = user.getGroups();
if(LazyUtil.isLazyAndNotLoaded(groupProxy)) {
	LazyUtil.SimpleLazyDynamicInvocationHandler ih = LazyUtil.getProxy(groupProxy);
	Class type = ih.getType();
	// override proxy set before serialization (avoid lazy loading all data upon serialization)
	user.setGroups((Set<Group>) type.newInstance());
}

Here I am replacing the user groups with a new placeholder instance to void invoking the lazy load when its serialized.

Does that help? Let me know if there is anything else I can clarify. Most appreciated!

@dmitry-s
Copy link
Contributor

@BenDol we think we can add a method to DatastoreOperations to support this.

Just to clarify, what do you think should happen when you deserialize such object?

@BenDol
Copy link
Author

BenDol commented Jul 14, 2020

Some serialization tools we use will invoke the lazy load on our objects causing an n+1 scenario. This is simply to avoid that, we check if the field was already lazy loaded and if so we can include it in the serialization otherwise we ignore it. It would be helpful to just open up the LazyUtil since it solves my issue. No need to add something to the DatastoreOperations for my usecase. Unless there is more to it.

@meltsufin
Copy link
Contributor

Just going through some old unresponded issues....
Maybe something like DatastoreOperations.getLazyLoadedFields(entity) would work better?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
datastore GCP Datastore P3
Development

No branches or pull requests

4 participants