2

I have a super simple table test e.g.

create table test (
    id serial primary key,
    status varchar (10)
);

insert into test (status) 
     values ('ready'), ('ready'), 
            ('steady'), 
            ('go'), ('go'), ('go'), 
            ('new');

To get the aggregated counts I can run: -

1) Simple multi-row result using group by

select status, 
       count(id) as count        
  from test
 group by status

... which returns ...

-------+-------
status | counts
-------+-------
go     |      3
ready  |      2
new    |      1
steady |      1
-------+-------

2) Single-Row Result using jsonb_object_agg

    with stats as (
       select status, 
              count(id) as count        
         from test
     group by status
    )
    select jsonb_object_agg (status, count) as status_counts from stats

... which returns ...

--------------------------------------------------
status_counts
--------------------------------------------------
{ "go" : 3, "new" : 1, "ready" : 2, "steady" : 1 }
--------------------------------------------------

Mybatis Interface method.

In my Java code (via MyBatis) I have a method: -

public Map<String, Integer> selectStatusCounts();

What I'm keen to figure out is how to map either query to the Map<String, Integer> Java object via MyBatis?


Update (1)

On a_horse_with_no_name advice and this stackover article I've come up with this: -

3) Single-Row Result using hstore

select hstore(array_agg(hs_key), array_agg(hs_value::text))
from (
    select 
        status, 
        count(id) as count        
    from test
    group by status    
) x(hs_key,hs_value)

... which returns ...

--------------------------------------------------
status_counts
--------------------------------------------------
"go"=>"3", "new"=>"1", "ready"=>"2", "steady"=>"1"
--------------------------------------------------

Using something like this could maybe work: -

https://github.com/gbif/checklistbank/blob/master/checklistbank-mybatis-service/src/main/java/org/gbif/checklistbank/service/mybatis/postgres/HstoreCountTypeHandler.java

Will test now! :-)


Update (2)

Thanks a_horse_with_no_name again for your contributions - I'm very close now but still weirdness with MyBatis. Here is a type handler I created (so I can reuse the aggregations elsewhere): -

@MappedTypes(LinkedHashMap.class)
@MappedJdbcTypes(JdbcType.OTHER)
public class MyBatisMapHstoreToStringIntegerMap implements TypeHandler<Map<String, Integer>> {

    public MyBatisMapHstoreToStringIntegerMap() {}

    public void setParameter(PreparedStatement ps, int i, Map<String, Integer> map, JdbcType jdbcType) throws SQLException {
        ps.setString(i, HStoreConverter.toString(map));
    }

    public Map<String, Integer> getResult(ResultSet rs, String columnName) throws SQLException {
        return readMap(rs.getString(columnName));
    }

    public Map<String, Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
        return readMap(rs.getString(columnIndex));
    }

    public Map<String, Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return readMap(cs.getString(columnIndex));
    }

    private Map<String, Integer> readMap(String hstring) throws SQLException {
        if (hstring != null) {
            Map<String, Integer> map = new LinkedHashMap<String, Integer>();
            Map<String, String> rawMap = HStoreConverter.fromString(hstring);
            for (Map.Entry<String, String> entry : rawMap.entrySet()) {
                map.put(entry.getKey(), Integer.parseInt(entry.getValue())); // convert from <String, String> to <String,Integer>
            }

            return map;
        }
        return null;
    }

}

... and here's the mapper interface ...

public interface TestMapper {

    public Map<String, Integer> selectStatusCounts();

}

... and here is the <select> inside the XML mapper file ...

<select id="selectStatusCounts" resultType="java.util.LinkedHashMap">
    select hstore(array_agg(hs_key), array_agg(hs_value::text)) as status_counts
    from (
        select 
            status, 
            count(id) as count        
        from test
        group by status    
    ) x(hs_key,hs_value)
</select>

However, it returns a Map with one entry called status_counts the value of which is the actual map I want i.e. {status_counts={new=1, ready=2, go=3, steady=1}}

The following is my maven dependencies with regards PostgreSQL / MyBatis: -

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.3.1</version>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>9.4-1201-jdbc41</version>
    </dependency>
Community
  • 1
  • 1
bobmarksie
  • 3,282
  • 1
  • 41
  • 54
  • Why not use `hstore`? That is returned as a `Map` from the JDBC driver –  Jan 20 '17 at 11:40
  • Never though of using `hstore` - I can of skipped that and went straight to JSONB :-). What would the aggregation query look like? – bobmarksie Jan 20 '17 at 11:48
  • I've went with the `hstore` solution and it returned `{status_counts={new=1, ready=2, go=3, steady=1}}` i.e.a map containing 1 entry i.e. `status_counts` which in turn held another map of `` - really what I want is the `Map` to align with what the column returns. – bobmarksie Jan 20 '17 at 13:11

2 Answers2

0

The easiest way is to define a hstore_agg() function:

CREATE AGGREGATE hstore_agg(hstore) 
(
    SFUNC = hs_concat(hstore, hstore),
    STYPE = hstore
);

Then you can do this:

select hstore_agg(hstore(status, cnt::text))
from (
  select status, count(*) cnt
  from test
  group by status
) t;

With the current JDBC driver Statement.getObject() will return a Map<String, String>.

As hstore only stores strings, it can't return a Map<String, Integer>

bobmarksie
  • 3,282
  • 1
  • 41
  • 54
  • I've tested that and it works nicely. However, the issue is with how we connect MyBatis up. Will update your answer. – bobmarksie Jan 20 '17 at 14:14
  • @bobmarksie: please do not put new questions into an answer. Either extend your question or ask a new question. –  Jan 20 '17 at 14:26
0

Posting an answer (which isn't perfect) but keen to see if anyone else has a better solution.

My answer is based on this stackoverflow answer: -

Return HashMap in mybatis and use it as ModelAttribute in spring MVC

I've created a POJO class called KeyValue: -

public class KeyValue<K, V> {

    private K key;
    private V value;

    public KeyValue() {
    }

    public KeyValue(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }

}

... and changed the test mapper method to ...

@MapKey("key")
public Map<String, KeyValue<String, Integer>> selectStatusCounts();

Note use of @MapKey parameter

I'm using the "1) Simple multi-row result using group by" SQL from original question and changed the result columns to key + value (so it maps to the new KeyValue object) as follows: -

<select id="selectStatusCounts" resultType="test.KeyValue">
   select status    as key, 
          count(id) as value        
     from bulk_upload
 group by status
</select>

Accessing this in Java is achieved as follows: -

Map<String, KeyValue<String, Integer>> statusCounts = mapper.selectStatusCounts();

And to retrieve e.g. the value from map of new items we simply do: -

int numberOfstatusCounts = statusCounts.get("new").getValue();

I'm fairly happy with this solution but I would still prefer a Map<String, Integer> as opposed to a Map<String, KeyValue<String, Integer>> so not going to accept my solution - it's here purely just to show how I've got something working (for now).

Community
  • 1
  • 1
bobmarksie
  • 3,282
  • 1
  • 41
  • 54