Load and map recursively on reactive streams

Viewed 439

Let's say we have a store, a catalog and categories. Each store can have catalogs and each category can have subcategories.

The documents are coming from MongoDB:

@Data
@Document
@NoArgsConstructor
@AllArgsConstructor
public class Catalog {
    @Id
    private String id;
    private Set<String> stores;
    @Transient
    private List<Category> tree;
}

@Data
@Document
@NoArgsConstructor
@AllArgsConstructor
public class Category {
    @Id
    private String id;
    private List<CatalogMember> catalogs;
    private List<CategoryMember> children;
    @Transient
    private List<Category> subCategories;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CatalogMember {
    private String id;
    private Long sequence;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CategoryMember {
    private String id;
    private Long sequence;
}

Now I have other API models to further work with in frontend:

@Getter
@Builder
public class CatalogByStoreIdResponse {
    private final String id;
    private final List<CategoryResponse> tree;
}

@Getter
@Builder
public class CategoryResponse {
    private final String id;
    private final List<CategoryResponse> children;
}

Currently I have the following code to retrieve all documents and set Catalog::tree as well as Category::subCategories:

@Slf4j
@Component
@RequiredArgsConstructor
public class CatalogByStoreIdHandler implements HandlerFunction<ServerResponse> {
    private final CatalogReactiveRepository catalogReactiveRepository;
    private final CategoryReactiveRepository categoryReactiveRepository;
    private final CatalogByStoreIdResponseMapper catalogByStoreIdResponseMapper;

    @Override
    public Mono<ServerResponse> handle(ServerRequest request) {
        String storeId = request.pathVariable("storeId");

        return ok()
                .body(catalogReactiveRepository.findFirstByStoresContains(storeId)
                        .doOnNext(catalog -> categoryReactiveRepository.findAllByCatalogsIdContainsOrderByCatalogsSequenceAsc(catalog.getId())
                                .collectList()
                                .doOnNext(tree -> catalog.setTree(tree))
                                .flatMapMany(Flux::fromIterable)
                                .expandDeep(category -> Flux.fromIterable(category.getChildren())
                                        .map(CategoryMember::getId)
                                        .collectList()
                                        .flatMapMany(categoryReactiveRepository::findAllByIdIn)
                                        .collectList()
                                        .doOnNext(subCategories -> category.setSubCategories(subCategories))
                                        .flatMapMany(Flux::fromIterable))
                                .subscribe())
                        .map(catalogByStoreIdResponseMapper), CatalogByStoreIdResponse.class);
    }
}

This is the essential mapper:

@Component
@RequiredArgsConstructor
public class CatalogByStoreIdResponseMapper implements Function<Catalog, CatalogByStoreIdResponse> {
    private final CategoryResponseMapper categoryResponseMapper;

    @Override
    public CatalogByStoreIdResponse apply(Catalog catalog) {
        return CatalogByStoreIdResponse.builder()
                .id(catalog.getId())
                .tree(Stream.ofNullable(catalog.getTree()) // catalog.getTree() is always null
                        .flatMap(List::stream)
                        .map(categoryResponseMapper)
                        .collect(Collectors.toList()))
                .build();
    }
}

Now my problem is that the mapping function is getting called before the tree is actually set and thus the tree in my response is empty.

I know that this is the correct behavior in reactive programming, but I need to know how to solve my problem so that my tree field isn't empty.

1 Answers

With the following code I get the result I needed:

return ok()
        .body(catalogReactiveRepository.findFirstByStoresContains(storeId)
                .zipWhen(catalog -> categoryReactiveRepository.findAllByCatalogsIdContainsOrderByCatalogsSequenceAsc(catalog.getId())
                        .collectList()
                        .doOnNext(catalog::setTree)
                        .flatMapMany(Flux::fromIterable)
                        .expand(category -> Flux.fromIterable(Optional.ofNullable(category.getChildren()).orElse(Collections.emptyList()))
                                .map(CategoryMember::getId)
                                .collectList()
                                .flatMapMany(categoryReactiveRepository::findAllByIdIn)
                                .collectList()
                                .doOnNext(category::setSubCategories)
                                .flatMapMany(Flux::fromIterable))
                        .collectList())
                .map(Tuple2::getT1)
                .map(catalogByStoreIdResponseMapper), CatalogByStoreIdResponse.class);

Result:

{

    "id": "DEMO_CATALOG",
    "tree": [
        {
            "id": "DEMO_CATEGORY_1",
            "children": [
                {
                    "id": "DEMO_CATEGORY_1_1",
                    "children": [ ]
                }
            ]
        },
        {
            "id": "DEMO_CATEGORY_2",
            "children": [
                {
                    "id": "DEMO_CATEGORY_2_1",
                    "children": [ ]
                }
            ]
        }
    ]

}

If there's some room for improvement just let me know.

Related