Search code examples
javareactive-programmingquarkusmutinyquarkus-reactive

Combine multiple resultsets into one object with hibernate reactive and mutiny


I have a database representing the accesslog of my webserver with columns such as ip-address, method, requested path, etc. I want to create an endpoint which returns a json similar to this:

{
    "addresses": [
        {
            "address": "1.2.3.4",
            "requests": 10
        },
        {
            "address": "5.6.7.8",
            "requests": 5
        }
    ],
    "methods": [
        {
            "method": "GET",
            "requests": 14
        },
        {
            "method": "POST",
            "requests": 1
        }
    ],
    "paths": [
        {
            "path": "/index",
            "requests": 15
        }
    ]
}

select address, count(address) as requests
from logs
group by address
order by requests desc
select method, count(method) as requests
from logs
group by method
order by requests desc
select path, count(path) as requests
from logs
group by path
order by requests desc

So this endpoint should execute 3 queries to get these 3 resultsets.

This is my first reactive/mutiny project so ive come as far as the tutorials go:

    @Inject
    io.vertx.mutiny.pgclient.PgPool client;

    @GET
    @Path("/getOne")
    public Multi<Map<String, Integer>> getOne() {

        String select = "select address, count(address) as requests" 
                     + " from logs group by address order by requests";

        Uni<RowSet<Row>> set1 = client.query(select).execute();

        return set1.onItem().transformToMulti(set -> Multi.createFrom().iterable(set)).onItem()
                .transform(row -> Map.of(row.getString("address"), row.getInteger("requests")));
    }

This already returns the array of ips and their requests.

[
  {
    "1.2.3.4": 10
  },
  {
    "5.6.7.8": 5
  }
]

I am now struggling to combine 2 resultsets into one response object:


    @Inject
    io.vertx.mutiny.pgclient.PgPool client;

    @GET
    @Path("/getCombined")
    public Uni<Tuple2<Map<String, Integer>, Map<String, Integer>>> getCombined() {

        String selectIps = "select address, count(address) as requests"
                + " from logs group by address order by requests";

        String selectMethods = "select method, count(method) as requests"
                + " from logs group by method order by requests";

        Uni<RowSet<Row>> set1 = client.query(selectIps).execute();
        Uni<RowSet<Row>> set2 = client.query(selectMethods).execute();

        Multi<Map<String, Integer>> multi1 = set1.onItem().transformToMulti(set -> Multi.createFrom().iterable(set))
                .onItem().transform(row -> Map.of(row.getString("address"), row.getInteger("requests")));

        Multi<Map<String, Integer>> multi2 = set2.onItem().transformToMulti(set -> Multi.createFrom().iterable(set))
                .onItem().transform(row -> Map.of(row.getString("method"), row.getInteger("requests")));

        return Uni.combine().all().unis(multi1.toUni(), multi2.toUni()).asTuple();
    }

this seems to go in the right direction but i end up having my arrays converted to objects:

{
  "item1": {
    "1.2.3.4": 10
  },
  "item2": {
    "GET": 7
  }
}

How can i combine multiple resultsets into one, to return one response object like the one at the top?


Solution

  • Instead of returning it as a Tuple, you could return it as a Map:

    
            Multi<Map<String, Integer>> multi1 = ... // Same as in the quesion
            Multi<Map<String, Integer>> multi2 = ... // Same as in the quesion
    
            Uni<Map<String, Map<String, Integer>>> uni1 = multi1
                    .toUni()
                    .map( map -> Map.of( "addresses", map ) );
    
            Uni<Map<String, Map<String, Integer>>> uni2 = multi2
                    .toUni()
                    .map( map -> Map.of( "methods", map ) );
    
            return Uni.combine().all().unis( uni1, uni2 )
                    .combinedWith( objects -> {
                        final Map<String, Object> resultMap = new HashMap<>();
                        ((List<Map<String, Object>>) objects).forEach( resultMap::putAll );
                        return resultMap;
                    } ); 
    

    But the result will look like this (different than the example in the question):

    {
      "addresses": [
        { "1.2.3.4": 10},
        { "5.6.7.3": 15},
      ],
      "methods": [
        { "GET": 7 }
      ]
    }
    
    

    I don't think I would convert everything to a Multi and then convert it back to a Uni. This will return the JSON you mention in the question:

        public Uni<Map<String, Object>> getCombined() {
            ...
            Uni<RowSet<Row>> set1 = client.query(selectIps).execute();
            Uni<RowSet<Row>> set2 = client.query(selectMethods).execute();
    
            Uni<Map<String, Object>> addressesUni = convertRowSet("addresses", "address", set1);
            Uni<Map<String, Object>> methodsUni = convertRowSet("methods", "method", set2);
    
            return Uni.combine().all().unis( addressesUni, methodsUni )
                    .combinedWith( objects -> {
                        final Map<String, Object> resultMap = new HashMap<>();
                        ((List<Map<String, Object>>) objects).forEach( resultMap::putAll );
                        return resultMap;
                    } );
        }
    
        private static Uni<Map<String, Object>> convertRowSet(String plural, String singular, Uni<RowSet<Row>> rowSetUni) {
            return rowSetUni.map( rowSet -> {
                if ( rowSet.size() > 0 ) {
                    List<Map<String, Object>> resultList = new ArrayList<>();
                    rowSet.forEach( row -> {
                        resultList.add( Map.of(
                                singular, row.getString( singular ),
                                "requests", row.getInteger( "requests" )
                        ) );
                    } );
                    return Map.of( plural, resultList );
                }
                return Collections.emptyMap();
            } );
        }
    

    Assuming that all results from the queries look the same.

    Note that this approach works because you are using the Vert.x client and not the Hibernate Reactive session. Because the session cannot be used for parallel operations, you would need to make sure that each query is executed using a different session or that the two queries are executed in a sequence.