2

We use SpringCache a lot where I work. We have many methods that look something like that:

public class CitiesManager {

    /**
     * Get all cities, cached
     */
    @Cachable( CACHE_REGION_CITIES )
    public List<City> getCities() {
        return getCitiesTransactional();
    }

    /**
     * Gets all cities, cached
     * This method is splitted from getCities(), to spare the redundant Transaction when Cities are fetched from cache.
     */
    @Transactional
    private List<City> getCitiesTransactional() {
        return repository.getCities();
    }
}

It works great. Problem is - City is a mutable object. And it happens that the caller of getCities() takes one city and changes it, which corrupts the cache.

With one such cache, our solution was to create a read-only interface, e.g:

public interface ReadonlyCity {

    String getName();

    int getPopulation();
}

That works great, but It's a hassle to do that for every class we cache.

What I would like to do is one of the two: 1. Tell SpringCache to wrap the objects it caches with a proxy that makes them immutable. For instance, by making every setFoo() method throw an UnSupportedOperationException. I know it's not air-tight, but it's good enough for me. 2. Make SpringCache serialise and deserialise objects it returns.

Or if you have other suggestions, I would like to hear them.

weston
  • 54,145
  • 21
  • 145
  • 203
pinkasey
  • 143
  • 1
  • 10
  • Maybe return a new instance of city. Better yet, make the internal object not the same as the return object.... Something like a DTO (data transfert object) – JFPicard Feb 10 '17 at 23:56
  • Spring Cache Manages the cache for me. And SpringCache decides to return the cached instance, whenever the cache is hit. Which is reasonable default behaviour, I think. Plus, SpringCache doesn't know how to create new instances of my object - use clone()? serialise+deserialise? copy-constructor? – pinkasey Feb 10 '17 at 23:59
  • 1
    Are you caching JPA entities, or also other objects ? JPA has it's own second level cache, which caches every entity inside the JVM, and if you update an entity inside a transaction, the cache is updated when the transaction is committed (you can even propagate the change to other servers using JPA's cache coordination). – Klaus Groenbaek Feb 11 '17 at 00:01
  • We avoid using entities cached by Hibernate at all cost. We always evict after fetching. We had many bugs related to that. – pinkasey Feb 11 '17 at 00:06
  • 2
    There could be bugs in Hibernate, but in my experience there is a bigger chance that you are using JPA wrong, as there are thousands of projects that use it successfully. There are also many projects that disable second level caching (then you don't need to evict anything), but that is typically because their JPA application is not the only user of the database, and if another application modifies the database, the cache becomes stale without JPA knowing it. – Klaus Groenbaek Feb 11 '17 at 00:22
  • How is creating a readonly interface more work for you than creating a readonly proxy for each class? – weston Feb 11 '17 at 00:29
  • Just wondering, why the callers of your method change City object directly, instead of calling some kind of "update" method in service layer or DAO layer? – Igor Bljahhin Feb 11 '17 at 09:18
  • @klaus groenbaek - I shlould have been more clear - the bugs are on our code, as you say. It is hard to keep track of what instance is attached and what isnt. The instance passes different layers in our application, higher layers arent even aware it came from db. The chances of a bug are too high so we dont use this feature. – pinkasey Feb 11 '17 at 11:28
  • @igor bijahhin - most coomon case: ui askes to update city's population via nbi. NBI layer calles Manager layer (Business Logic layer), askes for city by name or by id, city is returned from cache, nbi layer updates city (an corrupts the cache), then askes Manager to update the chaneged entity. If this stage fails, for instance transaction rollback, cache is now out of sync. – pinkasey Feb 11 '17 at 11:35
  • @weston - i would write a single aspect that creates a generic proxy that disables all setters. – pinkasey Feb 11 '17 at 11:36
  • @pinkasey After being active on SO for the last 3 month, it looks like a lot of JPA developers have no idea how JPA actually works, so it does not surprise me if you have had problems with caching, I have been there myself 7 years ago. All I can say, is that when you know JPA, and understand what happens inside a Persistence Context (PC), then it works exactly like you would expect. In should be rather simple, when you close the EntityManager which loaded a City, it becomes detached, and any change will not be seen by JPA until you merge it into another PC and commit the transaction. – Klaus Groenbaek Feb 11 '17 at 14:35
  • @pinkasey I understand your flow now. This issue can be solved by rewriting your update logic to "update" methods, something like City city = cityService.findById(123L); long newPopulation = 999L; cityService.updatePopulation(123L, newPopulation); which is much cleaner and logical IMHO. – Igor Bljahhin Feb 11 '17 at 16:38
  • Thanks, @IgorBljahhin. That solution won't suite us - 1. We would need need a method for every field we want to update, and most of our entities have lots of fields 2. It won't prevent a careless developer from corrupting the cache. Something that a read-only proxy, created by the cache-mechanism, would. – pinkasey Feb 11 '17 at 18:40
  • Where you able to solve that issue? I think this has nothing to do with JPA/Hibernate, but is purely an issue due to the caching mechanism of Spring. That link gives some hints on how to solve the issue: https://stackoverflow.com/questions/10827267/how-to-prevent-a-return-value-from-cache-to-be-changed-in-spring-cacheable – rochb Jun 05 '20 at 11:55

0 Answers0