Search code examples
javahibernatehibernate-4.x

In Hibernate Does StatelessSession prevent filtering out duplicates when have an EAGER JOIN


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 ?


Solution

  • 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().