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