2

So I do simple operation of finding an entry in my repository. If the entry is not present, the throw an exception.

@NotNull
public static User getUserFromUuid(UUID userUuid) {
    Optional<User> userOptional = userRepository.findByUserIdentifier(userUuid);
    if (!userOptional.isPresent()) {
        if (logger.isInfoEnabled()) logger.info(String.format("Unable to find user with uuid %s", userUuid.toString()));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User Not Found");
    }
    return userOptional.get();
}
@NotNull
public static Group getGroupFromId(Long groupId) {
    Optional<Group> groupOptional = groupRepository.findById(groupId);
    if (!groupOptional.isPresent()) {
        if (logger.isInfoEnabled()) logger.info(String.format("Group with id %s does not exist", groupId));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group Not Found");
    }
    return groupOptional.get();
}

I realize I would end up doing this many times for all my find methods. And most of them will be doing a very similar task.

One way is to extend the CrudRepository with my version, but I want to implement this pattern for al by finds.

Another way would be to pass the class, method, parameters, and the error message to search with. Lambda method seems to be the way, but I was unable to understand how I would be able to apply that to my problem.

This comes close to solving the problem, but the return type is changing. I would be passing a variable number of arguments as well.

Is there any approach I can take to do this?

EDIT:

I would also like to handle this case

Optional<GroupUser> groupUser = groupUserRepository.findByUserAndGroup(user, group

Where I could end up having more than one find parameters.

Something similar in python would be

def perform( fun, *args ):
    fun( *args )

def action1( args ):
    something

def action2( args ):
    something

perform( action1 )
perform( action2, p )
perform( action3, p, r )
  • Hi, would you instead consider using a Spring AOP aspect, in order to wrap around your repositories' methods returning an Optional, with the proper pointcut? – Thomas Escolan May 05 '20 at 12:41
  • @ThomasEscolan I am actually not aware of how it works. But I can look into it and see how it would apply to this. Any help would be appreciated! – Akshat Malik May 05 '20 at 14:19
  • Here's a good start about Aspect-Oriented Programming with Spring https://www.baeldung.com/spring-aop the idea being that you can weave some behaviors by intercepting public method calls within your Spring beans – Thomas Escolan May 06 '20 at 07:53

3 Answers3

1

Are you looking for something like that?

public static <T, ID>  T process(Class<T> cls, CrudRepository<T,ID> r, ID id, String errTemplate){
    Optional<T> groupOptional = r.findById(id);
    if (groupOptional.isEmpty()) {
        if (logger.isInfoEnabled()) logger.info(String.format(errTemplate, id));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, cls.getName() + " Not Found");
    }
    return groupOptional.get();
}
Giga Kokaia
  • 879
  • 8
  • 15
  • I would like something like this, but the crud repository method also is changing. And in these cases I am using just one id param. I have updated the question where I would want multiple params. – Akshat Malik May 05 '20 at 14:22
1

Since all your values might be represented by a String you could do the following:

public static Object getAccountDetails(String primaryKey, Class<?> targetClass) {   
        Optional<?> result;

        switch(targetClass.getSimpleName())
        {
        case "Group":
             result = Test.dummyFind(primaryKey);
             break;
        case "User":
            result =  Test.dummyFind(primaryKey);
            break;
        default:
            throw new IllegalArgumentException("The provided class: "+targetClass.getCanonicalName()+" was not a valid class to be resolved by this method.");      
        }

        if(result.isPresent())
        {
            logger.info(String.format("Group with id %s does not exist", groupId));
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group Not Found"); 
        }

        return result.get();
    }

As far as I know, there is no need to check if the logger has the info level enabled, since this is defined by your logback configuration. So if there's no logging for info, it won't get logged.

The use of the function would be something like that (Classes are random since I wanted to have my syntax highlighting):

UserDataHandler data = (UserDataHandler) getAccountDetails("1234", UserDataHandler.class);

This is regardless of any functionality of Spring since I don't work with it. The <?> is a wildcard operator. Since your result however is the content of an Optional, you have to return an Object which then has to be parsed to the according type.

Two ways: Either you use a registry like that:

public static Object getAccountDetails(Object primaryKey, Class<?> targetClass) {   

    Optional<?> result;

    // This map should be acquired from a Singleton where you register these classes once in @PostConstruct.
    Map<String, Method> methodMap = new TreeMap<>();
    try {
        methodMap.put("UserDataHandler", Test.class.getMethod("dummyFind"));
    } catch (NoSuchMethodException | SecurityException e) {}


    if(methodMap.containsKey(targetClass.getName()))
    {
        Method method = methodMap.get(targetClass.getName());
        try {
            result = (Optional<?>) method.invoke(primaryKey);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            // do some more error handling
            return null;
        }
    }
    else throw new IllegalArgumentException("The provided class: "+targetClass.getCanonicalName()+" was not a valid class to be resolved by this method.");


    if(!result.isPresent())
    {
        logger.info(String.format("Group with id %s does not exist", groupId));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group Not Found"); 
    }

    return result.get();
}

or a even smarter method would be just to always name the methods in the same pattern any acquire the target repository by Class.forName(String) :

public static Object getAccountDetails(Object primaryKey, Class<?> targetClass) {   

    Optional<?> result;

    Class<?> myRepository = Class.forName(targetClass.getSimpleName()+"Repository");

    String methodName = "findBy"+targetClass.getName();

    try
    {
        Method findMethod = targetClass.getMethod(methodName);
        result = (Optional<?>) findMethod.invoke(primaryKey);
    }
    catch (NoSuchMethodException e){throw new IllegalArgumentException("The method "+methodName+" couldn't be found in the repository");}
    catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {return null;}


    if(!result.isPresent())
    {
        logger.info(String.format("Group with id %s does not exist", groupId));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group Not Found"); 
    }

    return result.get();
}

For both methods, your find function has to take an Object as argument which has to be casted in order to use it:

public static Optional<Long> dummyFind(Object primaryKey)
{
    long typedPrimaryKey = (long) primaryKey;
    return Optional.of(typedPrimaryKey);
}

But as I thought about this twice, everything you want already exists: EntityManager.find(Class<T> entityClass,Object primaryKey)

maio290
  • 6,440
  • 1
  • 21
  • 38
  • This is the case for now, but I see it being expanded to many more find's and of many types. I would have a very large switch. – Akshat Malik May 05 '20 at 14:26
  • I've added two more variants to avoid the switch / case. The last one might be the best one though. – maio290 May 05 '20 at 14:56
  • I think this what I wanted. Thanks a lot. Just another small question. Do you think this would have performance implications? Because lot of this would happen at runtime. – Akshat Malik May 05 '20 at 15:17
  • 1
    I wouldn't say it's a huge impact, but there will be one for sure. Although I'd encourage you just to use the EntityManager's find method as long as you're using the JPA. This is way easier too read, no self-programmed black reflection magic and well tested. The find() method is also pretty fast from what I know. – maio290 May 05 '20 at 15:19
  • The find method wont allow to pass multiple args. In the solution you suggested the invoke method would need the class as well where to instantiate the action, it is unable to get that for the repository. I am stuck at that part – Akshat Malik May 06 '20 at 08:30
  • You don't need any additional args in the find method? I mean somewhere you have to define which class you actually want returned. That's something you'll have in every solution... – maio290 May 06 '20 at 08:59
1

You can create generic method that accepts Optional of any type and string for log message. It will returns the object if present, or else it will exception

public <T> T returnIfPresent(Optional<T> optional, String id){

    if (!optional.isPresent()) {
        if (logger.isInfoEnabled()) logger.info(String.format("Group with id %s does not exist", id));
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group Not Found");
    }
    return optional.get();
}

And you can call this method from every method

@NotNull
public static User getUserFromUuid(UUID userUuid) {

    Optional<User> userOptional = userRepository.findByUserIdentifier(userUuid);
    return returnIfPresent(userOptional, userUuid.toString());
 }

 @NotNull
 public static Group getGroupFromId(Long groupId) {
     Optional<Group> groupOptional = groupRepository.findById(groupId);

     return returnIfPresent(groupOptional, groupId.toString());
 }

The another suggestion i would recommend is, having message as second parameter so you can build the message in original method and pass it through

public <T> T returnIfPresent(Optional<T> optional, String message){

    if (!optional.isPresent()) {
        if (logger.isInfoEnabled()) logger.info(message);
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, message);
    }
    return optional.get();
}
Ryuzaki L
  • 37,302
  • 12
  • 68
  • 98
  • I think I like the fact that it helps me handle `Optional` elegantly, but it would be great if I could not have a function for each `getSomethingFromSomething`. – Akshat Malik May 05 '20 at 14:29
  • correct me if i'm wrong, the purpose of additional parameters passing into `perform` method is just for logging, so in that case build the log messages in original method and just pass that message as second parameter @AkshatMalik – Ryuzaki L May 05 '20 at 14:35