2

I have the following method -

 @Transactional
 public void savethis(){
    EntityObject t = entityManagerTreasury.getReference(EntityObject.class, 1);
    t.setAction("abc");
 }

Now, going in line with the following answer - https://stackoverflow.com/a/1608621/4881766

I should be only seeing an update query in my sql logs.

However the behaviour i've observed is as follows -

  1. Given code - select and then update
  2. commenting the t.setAction("abc"); line - No select and no update
  3. replacing getReference() with find() - select and then update

The behaviour i was expecting was that if i use any getter on the proxy, then a select should be issued, but when only using a setter, i wanted the changes to be committed at the end of the method with an update and no select being issued.

Turns out, no matter what i do with the proxy object, getter or setter, it issues a select.

I want to update selected fields of an entity for a given id. If there is any way to update any fields that i want without writing jpql or native query, I'd really appreciate it.

Thanks in advance!

Anil Kumar
  • 117
  • 8
  • If I understood correctly you want to see only un update with no prior select? –  Dec 17 '18 at 11:15
  • @EugenCovaci Yes, that's what i intended, and if you see the answer i've given link to, that's what it says SHOULD happen. So is it safe to say that the behaviour depicted there is incorrect and cannot be recreated? – Anil Kumar Dec 17 '18 at 11:36
  • I don't think that answer is **correct**, based on my very simple tests. –  Dec 17 '18 at 11:43
  • I think you forgot to accept my answer. –  Dec 17 '18 at 12:45

2 Answers2

1

From the EntityManager.getReference() documentation:

Get an instance, whose state may be lazily fetched.

Therefore, after entityManagerTreasury.getReference no select is issued.

Only after t.setAction("abc"), if the entity state is not already fetched, a select is issued to fetch the state.

The point is: the entity manager cannot save the state of an entity unless the entity state is fetched. Therefore you cannot skip the prior select, unless you use JPQL.

  • Thanks for the confirmation. I'll accept this as the answer. Although if it's not out of context to this question. Riddle me this - Let's say i have a detached entity with all the fields instead of the proxy object here, can i THEN update it into the database without issuing a select first(as in case of merge, it issues a complete select even though i already have the complete object because it is not in the current persistence context). – Anil Kumar Dec 17 '18 at 11:40
  • @AnilKumar No, you cannot. The same rule applies: the entity state has to be fetched for un update to take place. Again, the answer you linked to is, IMO, incorrect. –  Dec 17 '18 at 11:45
0

So what if JPA getReference() proxy doesn't provide that functionality. I can just write my own proxy.

Now, we can all argue that selects on primary keys are as fast as a query can get and it's not really something to go to great lengths to avoid. But for those of us who can't handle it due to one reason or another, below is an implementation of such a proxy. But before i you see the implementation, see it's usage and how simple it is to use.

USAGE

Order example = ProxyHandler.getReference(Order.class, 3);
example.setType("ABCD");
example.setCost(10);
PersistenceService.save(example);

And this would fire the following query -

UPDATE Order SET type = 'ABCD' and cost = 10 WHERE id = 3;

and even if you want to insert, you can still do PersistenceService.save(new Order("a", 2)); and it would fire an insert as it should.

IMPLEMENTATION

Add this to your pom.xml -

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>

Make this class to create dynamic proxy -

@SuppressWarnings("unchecked")
public class ProxyHandler {

public static <T> T getReference(Class<T> classType, Object id) {
    if (!classType.isAnnotationPresent(Entity.class)) {
        throw new ProxyInstantiationException("This is not an entity!");
    }

    try {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(classType);
        enhancer.setCallback(new ProxyMethodInterceptor(classType, id));
        enhancer.setInterfaces((new Class<?>[]{EnhancedProxy.class}));
        return (T) enhancer.create();
    } catch (Exception e) {
        throw new ProxyInstantiationException("Error creating proxy, cause :" + e.getCause());
    }
}

Make an interface with all the methods -

public interface EnhancedProxy {
    public String getJPQLUpdate();
    public HashMap<String, Object> getModifiedFields();
}

Now, make an interceptor which will allow you to implement these methods on your proxy -

import com.anil.app.exception.ProxyInstantiationException;
import javafx.util.Pair;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import javax.persistence.Id;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author Anil Kumar
*/
public class ProxyMethodInterceptor implements MethodInterceptor, EnhancedProxy {

private Object target;
private Object proxy;
private Class classType;
private Pair<String, Object> primaryKey;
private static HashSet<String> enhancedMethods;

ProxyMethodInterceptor(Class classType, Object id) throws IllegalAccessException, InstantiationException {
    this.classType = classType;
    this.target = classType.newInstance();
    this.primaryKey = new Pair<>(getPrimaryKeyField().getName(), id);
}

static {
    enhancedMethods = new HashSet<>();
    for (Method method : EnhancedProxy.class.getDeclaredMethods()) {
        enhancedMethods.add(method.getName());
    }
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    //intercept enhanced methods
    if (enhancedMethods.contains(method.getName())) {
        this.proxy = obj;
        return method.invoke(this, args);
    }
    //else invoke super class method
    else
        return proxy.invokeSuper(obj, args);
}

@Override
public HashMap<String, Object> getModifiedFields() {
    HashMap<String, Object> modifiedFields = new HashMap<>();
    try {
        for (Field field : classType.getDeclaredFields()) {

            field.setAccessible(true);

            Object initialValue = field.get(target);
            Object finalValue = field.get(proxy);

            //put if modified
            if (!Objects.equals(initialValue, finalValue)) {
                modifiedFields.put(field.getName(), finalValue);
            }
        }
    } catch (Exception e) {
        return null;
    }
    return modifiedFields;
}

@Override
public String getJPQLUpdate() {
    HashMap<String, Object> modifiedFields = getModifiedFields();
    if (modifiedFields == null || modifiedFields.isEmpty()) {
        return null;
    }
    StringBuilder fieldsToSet = new StringBuilder();
    for (String field : modifiedFields.keySet()) {
        fieldsToSet.append(field).append(" = :").append(field).append(" and ");
    }
    fieldsToSet.setLength(fieldsToSet.length() - 4);
    return "UPDATE "
            + classType.getSimpleName()
            + " SET "
            + fieldsToSet
            + "WHERE "
            + primaryKey.getKey() + " = " + primaryKey.getValue();
}

private Field getPrimaryKeyField() throws ProxyInstantiationException {
    for (Field field : classType.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(Id.class))
            return field;
    }
    throw new ProxyInstantiationException("Entity class doesn't have a primary key!");
}
}

And the exception class -

public class ProxyInstantiationException extends RuntimeException {
public ProxyInstantiationException(String message) {
    super(message);
}

A service to save using this proxy -

@Service
public class PersistenceService {

@PersistenceContext
private EntityManager em;

@Transactional
private void save(Object entity) {
    // update entity for proxies
    if (entity instanceof EnhancedProxy) {
        EnhancedProxy proxy = (EnhancedProxy) entity;
        Query updateQuery = em.createQuery(proxy.getJPQLUpdate());
        for (Entry<String, Object> entry : proxy.getModifiedFields().entrySet()) {
            updateQuery.setParameter(entry.getKey(), entry.getValue());
        }
        updateQuery.executeUpdate();
    // insert otherwise
    } else {
        em.persist(entity);
    }

}
}
Anil Kumar
  • 117
  • 8