seanhodges
seanhodges

Reputation: 17524

How to fetch a <map> in Hibernate

Update: I've created an example on GitHub to demonstrate my problem; HibernateMapTest currently fails due to the fact the HashMap key is a proxy object. I'm hoping someone can suggest a way I can query for the entity and fetch the map so that the test passes...

I'm simply trying to fetch the contents of a HashMap persisted in Hibernate, but I'm having some trouble finding the correct way to do it...

The HBM mapping is as follows, I did not create this but from my research it appears to be a ternary association mapping with a many-to-many relation. (Update: to simplify my question I've forced the map to lazy="false" to avoid my join):

<hibernate-mapping>
    <class name="database.Document" table="document">
        ...
        <map name="documentbundles" table="document_bundles" lazy="false">
            <key column="id"/>
            <index-many-to-many column="pkgitemid" class="database.PkgItem"/>
            <many-to-many column="child" class="database.Document" />
        </map>
    </class>
</hibernate-mapping>

For simplicity I'm just currently just attempting to fetch all the records with this map data populated:

DetachedCriteria criteria = DetachedCriteria.forClass(Document.class);
criteria.add(Restrictions.eq("id", 1));
List<Document> result = hibernateTemplate.findByCriteria(criteria);

After last to false, I now get the contents of the Map without throwing a LazyInitializationException; but none of the key objects have been initialised properly. I've dumped a screenshot to clarify what I mean:

Hashmap key is empty

I know that the fields are populated in the database, and I suspect my fetching strategy is still to blame. How do you fetch a <map> in Hibernate correctly?

Upvotes: 2

Views: 2819

Answers (2)

seanhodges
seanhodges

Reputation: 17524

This is a supplement to jhadesdev's answer, since I needed to do a little more work to get exactly what I was looking for.

In summary, you can't fetch a PersistedMap with a Hibernate query and immediately start using it like a typical Java hash map. The keys are always proxies; eager fetching / joining only fetches the map values, not the keys.

This means any code that deals with the hash map needs to be wrapped in a Hibernate transaction, which caused me some architectural problems as my data and service layers are separate.

I worked around this by iterating the hash map within a single transaction and replacing the keys with the ones originally passed in. I kept performance up by batching up the keys I want to fetch and retrieving them in one go:

// Build a list of keys we want to fetch in one go
final List<PkgItem> pkgItems = Arrays.asList(pkgItem1, pkgItem2, ...);

Map<PkgItem, Document> bundles = transactionTemplate.execute(new TransactionCallback< Map<PkgItem, Document> >() {
    @Override
    public Map<PkgItem, Document> doInTransaction(TransactionStatus transactionStatus) {
        if (doc1.getId() == null) return null;

        // Merge the parent document into this transaction
        Document container = hibernateTemplate.merge(doc1);

        // Copy the original package items into the key set
        Map<PkgItem, Document> out = new HashMap<PkgItem, Document>();
        for (PkgItem dbKey : container.getDocumentbundles().keySet()) {
            int keyIndex = pkgItems.indexOf(dbKey);
            if (keyIndex > -1) out.put(pkgItems.get(keyIndex), container.getDocumentbundles().get(dbKey));
        }
        return out;
    }
});

// Now we can perform a standard lookup
assertEquals("doc2", result.get(pkgItem1).getName());

I can now use the map without Hibernate in the resulting code, with only a minimal performance hit. I've also updated the test in my example GitHub project to demonstrate how this can work.

Upvotes: 1

Angular University
Angular University

Reputation: 43087

The error is due to the HibernateTemplate opening a Hibernate session to execute this query:

List results = hibernateTemplate.find("from database.Document d where d.name = 'doc1'");

and then immediately closing the session after the query is run. Then when looping through the keys, the session to which the map was linked is closed so the data cannot be loaded anymore, causing the proxy to throw the LazyInitializationException.

This exception means that the proxy can no longer load the data transparently because the session to which is linked too is now closed.

One of the main goals of the HibernateTemplate is to know when to open and close sessions. The template will keep the session open if there is an ongoing transaction.

So the key here is to wrap the unit test in a TransactionTemplate (the template equivalent to @Transactional), which causes the session to be kept open by the HibernateTemplate. Because the session is kept open, no more lazy initialization exceptions occur.

Modifying the test like this will solve the problem (notice the use of TransactionTemplate):

import database.Document;
import database.PkgItem;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.HibernateTransactionManager;
import org.springframework.orm.hibernate3.LocalSessionFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Set;

public class HibernateMapTest {

    private static final String TEST_DIALECT = "org.hibernate.dialect.HSQLDialect";
    private static final String TEST_DRIVER = "org.hsqldb.jdbcDriver";
    private static final String TEST_URL = "jdbc:hsqldb:mem:adportal";
    private static final String TEST_USER = "sa";
    private static final String TEST_PASSWORD = "";

    private HibernateTemplate hibernateTemplate;
    private TransactionTemplate transactionTemplate;

    @Before
    public void setUp() throws Exception {
        hibernateTemplate = new HibernateTemplate();
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.getHibernateProperties().put("hibernate.dialect", TEST_DIALECT);
        sessionFactory.getHibernateProperties().put("hibernate.connection.driver_class", TEST_DRIVER);
        sessionFactory.getHibernateProperties().put("hibernate.connection.password", TEST_PASSWORD);
        sessionFactory.getHibernateProperties().put("hibernate.connection.url", TEST_URL);
        sessionFactory.getHibernateProperties().put("hibernate.connection.username", TEST_USER);
        sessionFactory.getHibernateProperties().put("hibernate.hbm2ddl.auto", "create");
        sessionFactory.getHibernateProperties().put("hibernate.show_sql", "true");
        sessionFactory.getHibernateProperties().put("hibernate.jdbc.batch_size", "0");
        sessionFactory.getHibernateProperties().put("hibernate.cache.use_second_level_cache", "false");

        sessionFactory.setMappingDirectoryLocations(new Resource[]{new ClassPathResource("database")});
        sessionFactory.afterPropertiesSet();

        hibernateTemplate.setSessionFactory(sessionFactory.getObject());

        transactionTemplate = new TransactionTemplate(new HibernateTransactionManager(sessionFactory.getObject()));
    }

    @After
    public void tearDown() throws Exception {
        hibernateTemplate.getSessionFactory().close();
    }

    @Test
    public void testFetchEntityWithMap() throws Exception {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                // Store the entities and mapping
                PkgItem key = new PkgItem();
                key.setName("pkgitem1");
                hibernateTemplate.persist(key);

                Document doc2 = new Document();
                doc2.setName("doc2");
                hibernateTemplate.persist(doc2);

                Document doc1 = new Document();
                doc1.setName("doc1");
                HashMap<PkgItem, Document> documentbundles = new HashMap<PkgItem, Document>();
                documentbundles.put(key, doc2);
                doc1.setDocumentbundles(documentbundles);
                hibernateTemplate.persist(doc1);

                // Now attempt a query
                List results = hibernateTemplate.find("from database.Document d where d.name = 'doc1'");
                Document result = (Document)results.get(0);

                // Check the doc was returned
                Assert.assertEquals("doc1", result.getName());

                key = (PkgItem)hibernateTemplate.find("from database.PkgItem").get(0);
                Set<PkgItem> bundleKeys = result.getDocumentbundles().keySet();

                // Check the key is still present in the map. At this point the test fails because
                // the map contains a proxy object of the key...
                Assert.assertEquals(key, bundleKeys.iterator().next());
            }
        });

    }
}

and these are the test results and the log after the change:

enter image description here

Upvotes: 3

Related Questions