Salvando no banco de dados no pipeline de fluxo

8

De acordo com a documentação no site da Oracle :

Os efeitos colaterais nos parâmetros comportamentais das operações de fluxo são, em geral, desencorajados, pois muitas vezes podem levar a violações involuntárias do requisito de apatridia, bem como a outros riscos de segurança da thread.

Isso inclui salvar elementos do fluxo em um banco de dados?

Imagine o seguinte código (pseudo):

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  return cars.stream()
           .map(this::saveCar)
           .collect(Collectors.toList());
}

Quais são os efeitos indesejados opostos a esta implementação:

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  List<SavedCars> savedCars = new ArrayList<>();
  for (Cat car : cars) {
    savedCars.add(this.saveCar(car));
  }
  return savedCars.
}
Titulum
fonte
1
Sim , isso é ruim e, sob certas condições, você sentirá dor.
Eugene
Como assim? Qual é a diferença em escrever isso como um forloop regular ?
Titulum 24/01
Embora isso seja óbvio, se você usar parallelStream, certamente perderá o contexto da transação.
Glains 24/01
Uma dúvida ao criar esse código - Por que um método que grava em seu banco de dados retorna e modelo atualizado? Isso não pode ser separado? Quero dizer, mapear objetos de banco de dados para outro objeto em uma fase e gravá-lo no banco de dados em outra.
Naman 27/01
4
A documentação afirma que o efeito colateral é " em geral desanimado ". Então você está perguntando "e esse exemplo específico", mas quando obtém uma resposta observando os problemas do exemplo específico, você diz "mas este é apenas um exemplo". Então, se sua pergunta não é sobre esse exemplo específico, qual é a sua pergunta real? Você realmente espera que a documentação oficial faça uma declaração para cada caso de uso hipotético, quando já fez uma declaração geral?
Holger

Respostas:

4

Conforme a documentação no site da Oracle [...]

Esse link é para o Java 8. Você pode ler a documentação para o Java 9 (lançado em 2017) e versões posteriores, pois elas são mais explícitas a esse respeito. Especificamente:

Uma implementação de fluxo é permitida latitude significativa na otimização do cálculo do resultado. Por exemplo, uma implementação de fluxo é livre para eliminar operações (ou estágios inteiros) de um pipeline de fluxo - e, portanto, eliminar a invocação de parâmetros comportamentais - se puder provar que isso não afetaria o resultado da computação. Isso significa que os efeitos colaterais dos parâmetros comportamentais nem sempre podem ser executados e não devem ser invocados, a menos que seja especificado de outra forma (como nas operações do terminal forEache forEachOrdered). (Para um exemplo específico dessa otimização, consulte a nota da API documentada na count()operação. Para obter mais detalhes, consulte a seção de efeitos colaterais da documentação do pacote de fluxo.)

Fonte: Javadoc do Java 9 para a Streaminterface .

E também a versão atualizada do documento que você citou:

Efeitos colaterais

Os efeitos colaterais nos parâmetros comportamentais das operações de fluxo são, em geral, desencorajados, pois muitas vezes podem levar a violações involuntárias do requisito de apatridia, além de outros riscos à segurança do encadeamento.
Se os parâmetros comportamentais tiverem efeitos colaterais, a menos que declarado explicitamente, não há garantias quanto a :

  • a visibilidade desses efeitos colaterais para outros threads;
  • que operações diferentes no "mesmo" elemento dentro do mesmo pipeline de fluxo são executadas no mesmo encadeamento; e
  • que os parâmetros comportamentais são sempre invocados, uma vez que uma implementação de fluxo é livre para eliminar operações (ou estágios inteiros) de um pipeline de fluxo, se for possível provar que isso não afetaria o resultado da computação.

A ordem dos efeitos colaterais pode ser surpreendente. Mesmo quando um pipeline é restrito a produzir um resultado consistente com a ordem de encontro da origem do fluxo (por exemplo, IntStream.range(0,5).parallel().map(x -> x*2).toArray()deve produzir [0, 2, 4, 6, 8]), não há garantias quanto à ordem na qual a função do mapeador é aplicada a elementos individuais ou em qual thread qualquer parâmetro comportamental é executado para um determinado elemento.

A eliminação de efeitos colaterais também pode ser surpreendente. Com exceção das operações do terminal forEacheforEachOrdered , os efeitos colaterais dos parâmetros comportamentais nem sempre podem ser executados quando a implementação do fluxo pode otimizar a execução dos parâmetros comportamentais sem afetar o resultado da computação. (Para um exemplo específico, consulte a nota da API documentada na countoperação.)

Fonte: Javadoc do Java 9 para o java.util.streampacote .

Toda ênfase é minha.

Como você pode ver, a documentação oficial atual entra em mais detalhes sobre os problemas que você pode encontrar se decidir usar efeitos colaterais em suas operações de fluxo. Também é muito claro forEache forEachOrderedsão as únicas operações de terminal em que a execução de efeitos colaterais é garantida (lembre-se, os problemas de segurança de threads ainda se aplicam, como mostram os exemplos oficiais).


Dito isto, e em relação ao seu código específico, e apenas ao código mencionado:

public List<SavedCars> saveCars(List<Car> cars) {
  return cars.stream()
           .map(this::saveCar)
           .collect(Collectors.toList());
}

Não vejo problemas relacionados ao Streams com o código em questão.

  • A .map()etapa será executada porque .collect()(uma operação de redução mutável , que é o que o documento oficial recomenda, em vez de outras coisas .forEach(list::add)) depende .map()da saída da e, como essa saída (ou seja saveCar()) é diferente da entrada, o fluxo não pode "provar" que [eleger] isso não afetaria o resultado do cálculo " .
  • Não é um problema, parallelStream()por isso não deve apresentar nenhum problema de simultaneidade que não existia anteriormente (é claro, se alguém adicionar .parallel()mais tarde, poderão surgir problemas - muito como se alguém decidisse paralelizar um forloop disparando novos threads para os cálculos internos )

Isso não significa que o código nesse exemplo seja Good Code ™. A sequência .stream.map(::someSideEffect()).collect()como uma maneira de executar operações de efeitos colaterais para cada item de uma coleção pode parecer mais simples / curta / elegante? do que sua forcontraparte, e às vezes pode ser. No entanto, como Eugene, Holger e alguns outros lhe disseram, há maneiras melhores de abordar isso.
Como um pensamento rápido: o custo de disparar um Streamiterativo versus um simples fornão é desprezível, a menos que você tenha muitos itens, e se você tiver muitos itens, então você: a) provavelmente não deseja fazer um novo acesso ao banco de dados para cada um, então uma saveAll(List items)API seria melhor; eb) provavelmente não quer levar muito o desempenho ao processar muito de itens sequencialmente, para que você acabe usando paralelização e, em seguida, surja um novo conjunto de problemas.

Walen
fonte
1
Veja, esta é a resposta que eu estava procurando. Uma boa explicação com links para a documentação que confirma o comportamento.
Titulum 30/01
7

O exemplo mais fácil absoluto é:

cars.stream()
    .map(this:saveCar)
    .count()

Nesse caso, do java-9 e superior, mapnão será executado; já que você não precisa saber countnada.

Existem outros casos múltiplos em que os efeitos colaterais causam muita dor; sob certas condições.

Eugene
fonte
1
Eu acho count()que ainda seria executado, mas a implementação pode pular etapas intermediárias se puder produzir o resultado a partir da fonte (mas existem muitos ifs no nível de implementação )
ernest_k
2
@Titulum que é uma pergunta totalmente diferente e depende da implementação; mas sim, essas coisas devem ser implementadas na operação do terminal, como um costume Collector.
Eugene
2
@ Título: isso não será documentado, em lugar algum. Esses são detalhes de implementação; mas se você seguir a documentação (como a parte dos efeitos colaterais) - não se importará com eles, não é?
Eugene
2
Os recursos do Java 8 do @Titulum não transformaram o Java em uma linguagem funcional e o Streams etc. não é um substituto , é uma ferramenta adicional. Salvar coisas em um banco de dados é um enorme efeito colateral, então você está tentando calçar algo que não se encaixa apenas porque gosta da ideia de programação funcional. Você pode querer olhar para o Clojure se quiser ficar funcional.
Kayaman 24/01
1
FWIW, o que pode tornar esse efeito colateral ainda pior é que ele seria realmente worknas versões antigas do Java 8, mas não no Java 9 ou posterior. Essa otimização específica para fluxos de tamanho foi introduzida no JDK-8067969
Stefan Zobel