简体   繁体   中英

How to include if-else statements into reactive flow

I have a Spring Webflux reactive service which receives a DTO and inserts it into multiple table. Sometimes we may need to skip inserting into some tables based on the incoming DTO.

These are the requirements:

  1. Create a new Client
  2. Create a new Client referral if the client referral is present in the DTO.
  3. Create a client Secondary contact if present in the DTO
  4. Create client phones if present in the DTO

Questions:-

  1. Not sure how to apply the if condition in the reactive flow.
  2. Is there any better way of doing this?
  3. Here, except the first one all other operation can run in parallel.
public Mono<ServerResponse> createClientProfile(ServerRequest request) {
        return secContext.retrieveUser().flatMap(usr -> {
            return request.bodyToMono(ClientDto.class).flatMap(client -> {
                return toNewClient(client, usr).flatMap(clientRepository::save).flatMap(clientRes -> {
                    return toNewClientReferral(clientRes.getClientId(), client.getDiscount(), usr)
                            .flatMap(clientReferralRepository::save).flatMap(clientReferralRes -> {
                                return toNewClientSyContact(clientRes.getClientId(), client.getSecondary(), usr)
                                        .flatMap(clientSyContactRepository::save).flatMap(clientSyContactRes -> {
                                            return clientPhoneRepository
                                                    .saveAll(toNewClientPhone(clientRes.getClientId(), client.getPhones(), usr))
                                                    .collectList().flatMap(phoneRes -> {
                                                        return ServerResponse
                                                                .created(URI.create(String.format(CLIENT_URI_FORMAT,
                                                                        clientRes.getClientId())))
                                                                .contentType(APPLICATION_JSON).build();
                                                    });
                                        });
                            });
                });
            });
        });

    }

private Mono<Referral> toNewClientReferral(final long clientId, final Discount dto) {
        Referral referral = Referral.of(clientId, 
                dto.getName(), dto.getType(), dto.getAmount(), dto.getStatus());

        return Mono.just(referral);
    }

client.getDiscount() can be null,
client.getSecondary() can be null, client.getPhones() can be empty.

I separated the flow with 3 different methods.

public void createSyContact(ServerRequest request, long clientId) {
        secContext.retrieveUser().flatMap(usr -> {
            return request.bodyToMono(ClientDto.class).flatMap(client -> {
                if (client.getSecondary() != null) {
                    return toNewClientSyContact(clientId, client.getSecondary(), usr)
                            .flatMap(clientSyContactRepository::save).flatMap(clientRes -> {
                                return Mono.just(clientRes.getClientId());
                            });
                } else {
                    return Mono.empty();
                }
            });
        });
    }

    public void createReferral(ServerRequest request, long clientId) {
        secContext.retrieveUser().flatMap(usr -> {
            return request.bodyToMono(ClientDto.class).flatMap(client -> {
                if (client.getDiscount() != null) {
                    return toNewClientReferral(clientId, client.getDiscount(), usr)
                            .flatMap(clientReferralRepository::save).flatMap(clientRes -> {
                                return Mono.just(clientRes.getClientId());
                            });
                } else {
                    return Mono.empty();
                }
            });
        });
    }

    public Mono<Long> createClientWithPhones(ServerRequest request) {
        return secContext.retrieveUser().flatMap(usr -> {
            return request.bodyToMono(ClientDto.class).flatMap(client -> {
                return toNewClient(client, usr).flatMap(clientRepository::save).flatMap(clientRes -> {
                    return clientPhoneRepository
                            .saveAll(toNewClientPhone(clientRes.getClientId(), client.getPhones(), usr)).collectList()
                            .flatMap(phoneRes -> {
                                return Mono.just(clientRes.getClientId());
                            });
                });
            });
        });
    }

Here, createClientWithPhones is mandatory, so no if check there. But the other 2 methods createReferral & createSyContact have if checks. Need to execute createClientWithPhones first and it will return clientId. This clientId should be used in createReferral & createSyContact.

public Mono<ServerResponse> createClientProfile(ServerRequest request) {
        final List<Long> clinetIdList = new ArrayList<>();
        createClientWithPhones(request).subscribe(result -> {
            clinetIdList.add(result.longValue());
            createSyContact(request, result.longValue());
            createReferral(request, result.longValue());
        });
        return ServerResponse
                .created(URI.create(String.format(CLIENT_URI_FORMAT,
                        clinetIdList.get(0))))
                .contentType(APPLICATION_JSON).build();
        
    }

Is this the way to handle this?

Well, I don't think there is a good understanding in general of the reactive library. What I mean is that generally people approach like Java 8 streams in that they are trying to do functional programming. Of course the reactive library is based in functional programming, but I think the purpose is to be asynchronous around blocking I/O. Consider the (current) front page of the WebFlux project.

What is reactive processing? Reactive processing is a paradigm that enables developers build non-blocking, asynchronous applications that can handle back-pressure (flow control).

So, this is a longwinded way of saying I think it is better to focus on where the I/O is happening rather than creating functional code. If you need if statements, then you need if statements. Instead of trying to figure out how to do if statements with functional programming try to figure out where the I/O is taking place and handle it in an asynchronous fashion. One "trick" I like to use is Mono::zip or Flux::zip . These functions combine many I/O calls into one publisher to be returned to the client. So, consider this example code.

Let's make some reactive r2dbc functions:

Mono<Client> save(Client client) {
    client.id = 1L;
    System.out.println("Save client: " + client.id);
    return Mono.just(client);
}
Mono<Phone> save(Phone phone) {
    System.out.println("Save phone: " + phone.clientId);
    return Mono.just(phone);
}
Mono<Referral> save(Referral referral) {
    System.out.println("Save referral: " + referral.clientId);
    return Mono.just(referral);
}
Mono<Contact> save(Contact contact) {
    System.out.println("Save contact: " + contact.clientId);
    return Mono.just(contact);
}

We need some example classes to use:

class DTO {
    Client client;
    List<Phone> phones;
    Optional<Contact> contact;
    Optional<Referral> referral;
}

class Client {
    Long id;
}

class Contact {
    Long clientId;
}

class Referral {
    Long clientId;
}

class Phone {
    Long clientId;
}

Our input is probably a Mono<DTO> since that is what the Request should supply, so our Service layer needs to start with that and return a Mono<Long> of the client id.

Mono<Long> doWork(Mono<DTO> monoDto) {
    return monoDto.flatMap(dto->{
        return save(dto.client).flatMap(client->{
            List<Mono<?>> publishers = new ArrayList<>();
            dto.phones.forEach(phone->{
                phone.clientId = client.id;
                publishers.add(save(phone));    
            });
            if ( dto.contact.isPresent()) {
                Contact c = dto.contact.get();
                c.clientId = client.id;
                publishers.add(save(c));
            }
            if ( dto.referral.isPresent()) {
                Referral r = dto.referral.get();
                r.clientId = client.id;
                publishers.add(save(r));
            }
            if ( publishers.size() > 0 )
                return Mono.zip(publishers, obs->client.id);
            else
                return Mono.just(client.id);
        });
    });
}

I ran this with the following example code:

@Override
public void run(ApplicationArguments args) throws Exception {
    saveClient(new Client(), null, null, null).subscribe(System.out::println);
    saveClient(new Client(), new Phone(), null, null).subscribe(System.out::println);
    saveClient(new Client(), new Phone(), new Contact(), null).subscribe(System.out::println);
    saveClient(new Client(), new Phone(), new Contact(), new Referral()).subscribe(System.out::println);
}


private Mono<Long> saveClient(Client client, Phone phone, Contact contact,
        Referral referral) {
    // TODO Auto-generated method stub
    DTO dto = new DTO();
    dto.client = client;
    dto.phones = new ArrayList<>();
    if ( phone != null ) dto.phones.add(phone);     
    dto.contact = Optional.ofNullable(contact);
    dto.referral = Optional.ofNullable(referral);
    return doWork(Mono.just(dto));
}

So, this uses the Mono.zip trick. The saved client is flatmapped so that is done first. Then a list of monos is created for all subsequent saves that need to be done. These monos are all executed asynchronously by the Mono.zip function. The "combiner" function does nothing with the results, it just returns the clientId which is what is wanted for the client. The Mono.zip combines all the Monos into a single Mono to return to the client. In a sense this is just taking procedural code and wrapping it in the reactive library rather than getting overly focused on functional programming. This is easy to read and modify if the business "process" changes going forward.

This is a starting point if you like it. I didn't use Repository::saveAll so that could be an improvement.

It's important to be sure all your Flux and Mono publishers are chained together. In your final example you seemed to be dropping them. Simply creating them is not enough, they all have to be returned to the client somehow. Also, your code has a subscribe call which is a no-no. Only the client should have subscribe. I think you should have used a map there.

EDIT: Fixed a bug. Check your code carefully.

a plain if-statement can be done for instance in a flatMap and then acted upon.

public Mono<String> foobar() {
    return Mono.just("foo").flatMap(value -> {
        if(value != null)
            return Mono.just("Has value");
        else
            return Mono.empty();
    }
}

foobar()
    .switchIfEmpty(Mono.just("Is empty"))
    .subscribe(output -> System.out.println(output);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM