Using Spring Cache Abstraction with entities that have one-to-many relations

Viewed 76

I would like to add some caching in place but so fare all the examples I saw only show examples of caching simple entities (like the Spring book example) which works perfectly when using @Cacheable, @CachePut etc..

However, given this simple User - Roles examples:

public class User {
    private String username
    private List<Role> roles
}

Database side, 3 tables

user
id | username
1  | bob


user_role
user_id | role_id
1       | 1
1       | 2
1       | 3

role
id  | name
 1  | role1
 2  | role2
 3  | role3

a simple service that returns a user (with its roles), and this is what I would like to cache

@Cacheable(....)
public User getUserByUsername(username) {
   // SQL that joins the 3 tables and return a user with its roles
   User user = dao.getUserByusername(username)
}

An other service that will add a new role to a user (so it indirectly modifies the user object), hence the cache needs to be updated

//@CachePut(Yep.. this isn't going to work is it)
public void addRoleToUser(username, role_id) {
  // SQL to insert a row into the user_role table
  dao.addRoleToUser(username, role)
}

My question is: Is there a pattern / solution to have the cache directly updated after a role is added (like when using the @CachePut on simple entities) ? Or is the only solution to have the @CacheEvict on addRoleToUser forcing the next call to getUserByUsername retrieve the data from the database ?

I'm asking this because, performance wise, I would much prefer to have the cache updated directly when a role is added instead of clearing the cache and forcing a new database hit.

2 Answers

I'd say this is a design problem with the service method signature, addRoleToUser(..).

First, the service method simply delegates directly, or passes control immediately to the DAO, and the signature of the DAO bubbled up to the service layer where the caching concern is aptly applied, thereby affecting your ability to effectively cache changes to your domain model.

Second, the API of the service class should only be concerned with the domain (when thinking in DDD) and not how the relationship between a User and a Role (model classes of the domain) is represented and stored (i.e. username to role_id), which has been represented on your domain model class (User) with List of Role, anyhow.

Lastly, this type of data operation (storing only the new Role with ID for a User by name) is data store dependent, and an optimization at that, and therefore should be appropriately encapsulated in the DAO. It would be easy enough to determine the new Roles for User without having to query the persistent state of the data store.

It is not even that uncommon for the DAO (Repository) to handle this relationship and hide the implementation detail from the service layer. It would be essential if you were to change persistent stores (e.g. relational to document store like MongoDB) without adversely affecting the service layer of your application. Of course, we all know how often that happens, ;)

1 approach might be to, in calling code (e.g. Web MVC Controller) do the following:

@Controller
public class UserController {

  private UserService userService;

  public ResponseEntity<User> assignUserToRole(User user, Role role) {

    // do any input validation

    user = userService.grantRoleToUser(role, user);

    // return an appropriate response
  }
}

Then, in the UserService:

class UserService {

  private UserDAO userDao;

  @CachePut("Users")
  public User grantRoleToUser(Role role, User user) {

    // validate the assignment is allowed

    userDao.addRoleToUser(user.getName(), role.getId());

    user.addRole(role);

    return user;
  }

Obviously, there could be other variations on this design.

For example, the User DAO / Repository could simply have a save(:User) method and the User could be assigned a Role (user.addRole(role)) before the DAO/Repository save operation is invoked from the service class.

Either way, this handles your caching needs.

Based on your current design, then using @CacheEvict(cacheNames = "Users", key = "#username") on the addRoleToUser(..) service method is your best bet. Yes, the User will need to be reloaded the next time your getUserByUsername(..) service method is invoked.

It doesn't look like you can do this just with annotations, but you definitely can by using CacheManager directly:

@Autowired
private CacheManager cacheManager;

...

public void addRoleToUser(username, role_id) {
  // SQL to insert a row into the user_role table
  dao.addRoleToUser(username, role)
  
  Cache userCache = cacheManager.getCache("<your_User_cache_name>");
  User user = userCache.getIfPresent(username);
  if (user != null) {
    // add role to the cached User
  }
}

Of course, for this to work your cache should not have copy-on-read feature enabled (if it's supported at all).

Related