Guardar en la base de datos en la tubería de flujo

8

Según la documentación en el sitio web de Oracle :

En general, se desaconsejan los efectos secundarios en los parámetros de comportamiento para transmitir las operaciones, ya que a menudo pueden conducir a violaciones involuntarias del requisito de apatridia, así como a otros riesgos de seguridad de hilos.

¿Esto incluye guardar elementos de la secuencia en una base de datos?

Imagine el siguiente (pseudo) código:

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());
}

¿Cuáles son los efectos no deseados opuestos a esta implementación?

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.
}
Titulo
fuente
1
, esto es malo y, bajo ciertas condiciones, tendrá dolor.
Eugene
¿Cómo es eso? ¿Cuál es la diferencia con escribir esto como un forbucle regular ?
Título
Aunque esto sería obvio, si lo usa parallelStream, ciertamente perdería el contexto de la transacción.
Glains
Una duda sobre el diseño de este código: ¿por qué un método que escribe en su base de datos devuelve un modelo actualizado? ¿No podría eso estar separado? Me refiero a asignar objetos de la base de datos a algún otro objeto en una fase y escribirlo en la base de datos en otra.
Naman
44
La documentación establece que los efectos secundarios son " en general, desalentados ". Luego pregunta "qué pasa con este ejemplo específico", pero cuando obtiene una respuesta que señala los problemas del ejemplo específico, dice "pero esto es solo un ejemplo". Entonces, si su pregunta no es sobre este ejemplo específico, ¿cuál es su pregunta real? ¿Realmente espera que la documentación oficial haga una declaración para cada caso de uso hipotético, cuando ya hizo una declaración general?
Holger

Respuestas:

4

Según la documentación en el sitio web de Oracle [...]

Ese enlace es para Java 8. Es posible que desee leer la documentación para Java 9 (que salió en 2017) y versiones posteriores, ya que son más explícitos a este respecto. Específicamente:

Se permite una implementación de flujo latitud significativa en la optimización del cálculo del resultado. Por ejemplo, una implementación de flujo es libre de eludir operaciones (o etapas completas) de una tubería de flujo, y por lo tanto, elude la invocación de parámetros de comportamiento, si puede probar que no afectaría el resultado del cálculo. Esto significa que los efectos secundarios de los parámetros de comportamiento no siempre se pueden ejecutar y no se debe confiar en ellos, a menos que se especifique lo contrario (como por ejemplo las operaciones del terminal forEachy forEachOrdered). (Para ver un ejemplo específico de dicha optimización, consulte la nota API documentada sobre la count()operación. Para obtener más detalles, consulte la sección de efectos secundarios de la documentación del paquete de transmisión).

Fuente: Javadoc de Java 9 para la Streaminterfaz .

Y también la versión actualizada del documento que citó:

Efectos secundarios

En general, se desaconsejan los efectos secundarios en los parámetros de comportamiento para transmitir las operaciones, ya que a menudo pueden conducir a violaciones involuntarias del requisito de apatridia, así como a otros riesgos de seguridad de hilos.
Si los parámetros de comportamiento tienen efectos secundarios, a menos que se indique explícitamente, no hay garantías en cuanto a :

  • la visibilidad de esos efectos secundarios a otros hilos;
  • que diferentes operaciones en el "mismo" elemento dentro de la misma tubería de flujo se ejecutan en el mismo hilo; y
  • que los parámetros de comportamiento siempre se invocan, ya que una implementación de flujo es libre de eludir operaciones (o etapas completas) de una tubería de flujo si puede probar que no afectaría el resultado del cálculo.

El orden de los efectos secundarios puede ser sorprendente. Incluso cuando una tubería está restringida a producir un resultado que sea consistente con el orden de encuentro de la fuente de flujo (por ejemplo, IntStream.range(0,5).parallel().map(x -> x*2).toArray()debe producir [0, 2, 4, 6, 8]), no se garantiza el orden en que se aplica la función de mapeador a elementos individuales, o en qué hilo se ejecuta cualquier parámetro de comportamiento para un elemento dado.

La elusión de los efectos secundarios también puede ser sorprendente. Con la excepción de las operaciones de terminal forEachyforEachOrdered , los efectos secundarios de los parámetros de comportamiento pueden no siempre ejecutarse cuando la implementación del flujo puede optimizar la ejecución de los parámetros de comportamiento sin afectar el resultado del cálculo. (Para ver un ejemplo específico, consulte la nota API documentada sobre la countoperación).

Fuente: Javadoc de Java 9 para el java.util.streampaquete .

Todo el énfasis es mío.

Como puede ver, la documentación oficial actual entra en más detalles sobre los problemas que puede encontrar si decide usar efectos secundarios en sus operaciones de transmisión. También es muy claro forEachy forEachOrderedes la única operación de terminal en la que se garantiza la ejecución de efectos secundarios (tenga en cuenta que los problemas de seguridad de subprocesos aún se aplican, como lo muestran los ejemplos oficiales).


Dicho esto, y con respecto a su código específico, y solo dicho código:

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

No veo problemas relacionados con Streams con dicho código tal como está.

  • El .map()paso se ejecutará porque .collect()(una operación de reducción mutable , que es lo que recomienda el documento oficial en lugar de cosas como .forEach(list::add)) se basa en .map()la salida y, dado que esta (es decir saveCar(), la salida) es diferente de su entrada, el flujo no puede "probar que [elidándose] no afectaría el resultado del cálculo " .
  • No es un problema, parallelStream()por lo que no debería presentar ningún problema de concurrencia que no existiera anteriormente (por supuesto, si alguien agregó .parallel()más tarde, entonces pueden surgir problemas, como si alguien decidiera paralelizar un forbucle activando nuevos hilos para los cálculos internos )

Eso no significa que el código en ese ejemplo sea Good Code ™. ¿La secuencia .stream.map(::someSideEffect()).collect()como una forma de realizar operaciones de efectos secundarios para cada elemento de una colección puede parecer más simple / corta / elegante? que su forcontraparte, y a veces puede ser. Sin embargo, como Eugene, Holger y algunos otros le dijeron, hay mejores maneras de abordar esto.
Como una idea rápida: el costo de activar un Streamvs iterar un simple forno es insignificante a menos que tenga muchos artículos, y si tiene muchos artículos, entonces: a) probablemente no quiera hacer un nuevo acceso a la base de datos para cada uno, entonces una saveAll(List items)API sería mejor; y b) probablemente no quieren tomar el impacto en el rendimiento del procesamiento de una gran cantidad de elementos de forma secuencial, por lo que terminaría usando la paralelización y luego surgiría un nuevo conjunto de problemas.

walen
fuente
1
Mira, esta es la respuesta que he estado buscando. Una buena explicación con enlaces a documentación que confirman el comportamiento.
Títulos
7

El ejemplo más sencillo es:

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

En este caso, desde java-9 en adelante, mapno se ejecutará; ya que no lo necesitas para saber el count, en absoluto.

Hay otros casos múltiples en los que los efectos secundarios le causarían mucho dolor; bajo ciertas condiciones.

Eugene
fuente
1
Creo count()que aún se ejecutará, pero la implementación puede omitir los pasos intermedios si puede producir el resultado de la fuente (pero hay muchos ifs a nivel de implementación )
ernest_k
2
@Titulum que es una pregunta completamente diferente y depende de la implementación; pero sí, tales cosas deberían implementarse en la operación del terminal, como una costumbre Collector.
Eugene
2
@Titulum esto no se documentará, en ningún lado. Estos son detalles de implementación; pero si sigue la documentación (como su parte de efectos secundarios), no le importará, ¿verdad?
Eugene
2
Las características de @Titulum Java 8 no convirtieron a Java en un lenguaje funcional y Streams, etc. no son un reemplazo , son una herramienta adicional. Guardar cosas en una base de datos es un gran efecto secundario, por lo que está tratando de calzar algo que no encaja solo porque le gusta la idea de la programación funcional. Es posible que desee ver Clojure si desea que todo funcione.
Kayaman
1
FWIW, lo que puede empeorar aún más este efecto secundario es que realmente lo haría worken versiones antiguas de Java 8 pero no en Java 9 o posterior. Esa optimización particular para transmisiones de tamaño se introdujo en JDK-8067969
Stefan Zobel