Search code examples
postgresqlspring-webfluxproject-reactorspring-data-r2dbcr2dbc

How to implement OneToMany, ManyToOne and ManyToMany with R2DBC in a project which previously used JPA?


I have to re-implement a few back-end services and one of the main requirement is to make the whole flow reactive. Previously the services used hibernate with PostgreSQL so the mentioned connections were provided by the framework.

Since I have to keep the original DB and just change the service implementation I have to use r2dbc-postgresql. I couldn't find any resource about this topic, but my best guess is to do something similar what I would do with JDBC and introduce some new connection tables between my entities.

  1. Is this would be a correct approach or should I consider some different solutions?
  2. What would be the steps to achieve the mentioned connections?

Solution

  • I am looking into something similar and have come to the same conclusion (1), since there is no support for relations in R2DBC. To migrate a one-to-many relation I first made the collection containing the "many" entities to @Transient in the "one" entity. Persisting the "one" entity is accomplished using the following steps in a reactive sequence:

    1. Persist the "many" entities. This in order to be able to update these in the "one" entity so that the "many" entities are assigned ids.
    2. Persist the "one" entity.
    3. Persist the relationships. I can do this at this stage since I now have the ids of all involved entities. For the relationships I have introduced a helper entity, something along the lines of OneManyRelation and a corresponding repository.

    In code it looks like this:

    public <S extends Drawing> Mono<S> save(final S inDrawing) {
        final List<Shape> theDrawingShapes = inDrawing.getShapes();
    
        return Mono.defer(() -> {
            return Flux.fromIterable(theDrawingShapes)
                .log()
                .flatMap(theDrawingShape -> {
                    /* Save the shapes contained in the drawing. */
                    if (theDrawingShape instanceof Circle) {
                        final Circle theUnsavedCircle = (Circle) theDrawingShape;
                        return mCircleRepository.save(theUnsavedCircle);
                    } else if (theDrawingShape instanceof Rectangle) {
                        final Rectangle theUnsavedRectangle = (Rectangle) theDrawingShape;
                        return mRectangleRepository.save(theUnsavedRectangle);
                    } else {
                        LOGGER.warn("Unrecognized entity type: {}",
                            theDrawingShape.getClass().getName());
                        return Mono.just(theDrawingShape);
                    }
                })
                /* Update the drawing, setting the shapes of the drawing to the saved shapes. */
                .collectList()
                .map(theSavedShapesList -> {
                    inDrawing.setShapes(new ArrayList<>(theSavedShapesList));
                    return inDrawing;
                })
                /* Save the drawing itself. */
                .flatMap(theDrawing -> super.save(theDrawing))
                .flatMap(theDrawing -> {
                    /* Save the relations between the drawing and the shapes of the drawing. */
                    return Flux.fromIterable(theDrawing.getShapes())
                        .flatMap(theDrawingShape -> {
                            final var theDrawingShapeRelation = new DrawingShapesRelation();
                            theDrawingShapeRelation.setDrawingId(theDrawing.getId());
                            theDrawingShapeRelation.setShapeId(theDrawingShape.getId());
                            theDrawingShapeRelation.setShapeType(theDrawingShape.getClass()
                                .getName());
                            return mDrawingShapesRelationRepository.save(theDrawingShapeRelation);
                        })
                        .collectList()
                        .map(theDrawingShapesRelationList -> theDrawing);
                });
        });
    }
    

    My conclusion this far is that unless you are certain that there are major gains to be made from switching to R2DBC, I would settle for using Spring Data JPA and executing calls to repositories in a separate thread using subscribeOn.
    Good luck and happy coding!