1

Given the following Function implementations...

Get User Credentials From Master Database

private Function<Long, Optional<UserCredentials>>
    getUserCredentialsFromMaster() {
        return userId -> Optional.ofNullable(userId)
            .flatMap(masterUserRepository::findById)
            .map(User::getCredentials);
}

Get User Credentials From Secondary Database

private Function<Long, Optional<UserCredentials>>
    getUserCredentialsFromSecondary() {
        return userId -> Optional.ofNullable(userId)
            .flatMap(secondaryUserRepository::findById)
            .map(User::getCredentials);
}

I need to execute either getUserCredentialsFromMaster or getUserCredentialsFromSecondary depending on where userId comes from.

Here below is my attempt:

Consider domain class UserProfile

public class UserProfile {
    Long id;
    Long internalUserId; // if internalUserId is null then externalUserId is not
    Long externalUserId; // and vice-versa
}

Attempt to obtain UserCredentials:

final UserProfile userProfile = userProfileRepository.findById(userProvileId);

final UserCredentials userCredentials =
    Optional.ofNullable(userProfile.internalUserId)
        .flatMap(getUserCredentialsFromMaster())
        .orElse(
            Optional.ofNullable(userProfile.externalUserId)
                .flatMap(getUserCredentialsFromSecondary())
                .orElseThrow(UserCredentialsNotFound::new));

internalUserId is not null but the statements above always throw UserCredentialsNotFound. I've tried to rewrite getUserCredentialsFromMaster and getUserCredentialsFromSecondary as plain Java methods invoked from an if-then-else block, and it worked as expected.

Am I missing something?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
j3d
  • 9,492
  • 22
  • 88
  • 172
  • Both `getImplementationStrategyByModelPortfolioId()` and `getImplementationStrategyByMandateId()` are parameterless, i.e. they don't depend on `internalUserId` and `externalUserId` (you just need them to be non-null) or it's an unintentional mistake? – Alexander Ivanchenko Dec 01 '22 at 11:07
  • ... `getImplementationStrategyByModelPortfolioId()` and `getImplementationStrategyByMandateId()` were just copy-past from my actual code. In the example code they should have been `getUserCredentialsFromMaster()` and `getUserCredentialsFromSecondary()`, respectively. Sorry – j3d Dec 01 '22 at 11:12
  • By the way, either `internalUserId` or `externalUserId` must have an actual value. – j3d Dec 01 '22 at 11:14
  • One more clarification, your repository `masterUserRepository::findById` returns an `Optional` or a nullable `User` instance? – Alexander Ivanchenko Dec 01 '22 at 12:05
  • It returns `Optional`... but I could rework it to return a nullable object. It doesn't matter for me :-) – j3d Dec 01 '22 at 12:13
  • I was asking because in the first functions it should be `.flatMap(secondaryUserRepository::findById)` if repository returns `Optional`. And there's a mismatch in the type of parameters consumed by Functions (`Long` vs `String` in the `UserProfile`). – Alexander Ivanchenko Dec 01 '22 at 12:45

1 Answers1

2

TL;DR

You're getting an exception because the argument of the Optional.orElse() is evaluated eagerly. I.e. it would be evaluated even if the Optional on which it was invoked is not empty.

But as you have said, either "internalUserId is null then externalUserId is not null and vice-versa" and orElseThrow() produces an exception.

Avoid using Optional to replace Null-checks

Firstly, note that Optional wasn't designed to perform null-checks. The design goal of the Optional is to serve as a return type, and its method ofNullable() is supposed to wrap a nullable return value, not to substitute a null-check.

You might be interested in reading:

And there's nothing wrong with implicit null-checks, it's a bit of a problem when there are a lot of them in the code, but it's rather an immediate consequence of how particular behavior in the application was implemented, then the issue related to the tools offered by the language.

The cleaner way to address this problem would be to get read of the functions and define a method producing a Supplier based on the provided id and UserRepository:

private Supplier<Optional<UserCredentials>> getUserCredentials(Long id, UserRepository repository) {
    
    return id == null ?
        Optional::empty :
        () -> repository.findById(id).map(User::getCredentials);
}

Which can be used in the following way:

UserCredentials userCredentials = Stream.of(
        getUserCredentials(userProfile.internalUserId, masterUserRepository),
        getUserCredentials(userProfile.internalUserId, secondaryUserRepository)
    )
    .map(Supplier::get)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst()
    .orElseThrow(UserCredentialsNotFound::new);

If you still want to keep using Optional

As a remedy you can use Java 9 Optional.or() which expects a Supplier of Optional which would be evaluated only if needed.

UserCredentials userCredentials = Optional.ofNullable(userProfile.internalUserId)
    .flatMap(getUserCredentialsFromMaster())
    .or(() ->
        Optional.ofNullable(userProfile.externalUserId)
            .flatMap(getUserCredentialsFromSecondary())
    )
    .orElseThrow(UserCredentialsNotFound::new);

Alternatively, you can make use Optional.orElseGet() which takes a Supplier of resulting value:

UserCredentials userCredentials = Optional.ofNullable(userProfile.internalUserId)
    .flatMap(getUserCredentialsFromMaster())
    .orElseGet(() ->
        Optional.ofNullable(userProfile.externalUserId)
            .flatMap(getUserCredentialsFromSecondary())
            .orElseThrow(UserCredentialsNotFound::new)
    );
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • This was also a solution I tried and it worked... but I'm wondering whether I could get rid of the lambda. – j3d Dec 01 '22 at 12:52
  • @j3d When you're writing something like `foo(bar)`, argument `bar` should be evaluated before in order to invoke `foo()`. When instead of `bar` you have `foo(baz())` then method `baz()` should be executed prior to the invocation of `foo()`. Hence in your case you have a choice between replacing `orElseThrow()` with `orElse(defaultValue)`, or if you need to throw then - you need to use a Function via `or()`, or `orElseGet()`. – Alexander Ivanchenko Dec 01 '22 at 13:04