3

I have a Song class containing a collection of CoverArts

e.g

@OneToMany(fetch=FetchType.LAZY, cascade={CascadeType.ALL})
@JoinColumn(name = "recNo")
private List<CoverArt> coverArts;

and am using Hibernate 4.3.11 and DB2 database and I have this query for retrieving a list of Songs by their primary key together with their coverArt.

public static List<Song> getSongsWithCoverArtFromDatabase(Session session, List<Integer> ids)
    {
        try
        {
            Criteria c = session
                    .createCriteria(Song.class)
                    .setFetchMode("coverArts", FetchMode.JOIN)
                    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                    .add(Restrictions.in("recNo", ids));
            List<Song> songs = c.list();
            return songs;
        }
        catch (Exception e)
        {
            MainWindow.logger.log(Level.SEVERE, "Failed LoadSongToDatabase:" + e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

Note we have set fetch mode to JOIN on the coverArts collection, and that we need to set setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY), otherwise if we had a song with two coverart records we would get two Song objects returned back. But when using Criteria.DISTINCT_ROOT_ENTITY Hibernate would correctly return one Song containing two coverArts.

However I have just tried to do the same thing but using a StatelessSession. The reasoning being I am just trying to select data for creating a report and I want maximize speed and minimize memory consumption, however

   public static List<Song> getSongsWithCoverArtFromDatabase(StatelessSession session, List<Integer> ids)
    {
        try
        {
            Criteria c = session
                    .createCriteria(Song.class)
                    .setFetchMode("coverArts", FetchMode.JOIN)
                    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                    .add(Restrictions.in("recNo", ids));
            List<Song> songs = c.list();
            return songs;
        }
        catch (Exception e)
        {
            MainWindow.logger.log(Level.SEVERE, "Failed LoadSongToDatabase:" + e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

this seems to ignore the .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) ands returns duplicates rows.

Is this a known bug, how it meant to behave ?

Paul Taylor
  • 13,411
  • 42
  • 184
  • 351

1 Answers1

2

Looks like this is shortcoming in the way StatelessSessionImpl is implemented in Hibernate, but a fix might be on the way too...

Clearly with the FetchMode.JOIN, the SQL query will be (left outer) joining across the two tables, so may return several rows per song. Normally Hibernate resolves each row returned via its PersistenceContext.

If interested, you can see this in the Hibernate source for Loader here. Then, depending on the type of Session, SessionImpl.getEntityUsingInterceptor() talks to the PersistenceContext, but StatelessSessionImpl.getEntityUsingInterceptor() just returns null. However there is a later commit to this method that looks to do the right thing. The commit is part of HHH-11147, which says the fix versions are Hibernate 5.3.11 and 5.4.4 - not showing in the Maven repo at the time of writing.

In the meantime, one fix would be to roll your own ResultTransformer. This is a fairly 'to the point' example:

public class DistinctSongResultTransformer implements ResultTransformer {
    private ResultTransformer defaultTransformer = Criteria.DISTINCT_ROOT_ENTITY;

    @Override
    public Object transformTuple(Object[] tuple, String[] aliases) {
        return defaultTransformer.transformTuple(tuple, aliases);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public List transformList(List collection) {
        Map<Integer, Song> distinctSongs = new LinkedHashMap<>();
        for (Object object : collection) {
            Song song = (Song) object;
            distinctSongs.putIfAbsent(song.getId(), song);
        }
        return new ArrayList<>(distinctSongs.values());
    }
}

The difference is that the normal DistinctRootEntityResultTransformer assumes there will only be a unique instance of the entity in the session - you can see the compare here.

Clearly there's room to make that example more reuseable too, particularly to abstract the getId().

df778899
  • 10,703
  • 1
  • 24
  • 36
  • Thankyou for the detailed answer, what I dont understand is since HIbernate is meant to hide the database complexity if I have a object that has a one-many association why it doesnt just handle it by default anyway, the more I use Hibernate the more I am disapointed by it. – Paul Taylor Jun 17 '19 at 07:48
  • @PaulTaylor - In my experience most frameworks make easy things easier, but deviating from supported functionality a lot more difficult (probably we expect too much because the simple stuff is so easy). Hibernate is a classic example of this. For the same reason, most people i know have a love-hate relationship with it. :) – Aditya Jun 17 '19 at 09:08
  • Yeah, but Im confused if Im doing something weird here since retrieving a list of objects with their associated 1:M using EAGER seems a pretty standard use case to me ? – Paul Taylor Jun 17 '19 at 09:25