Announcement Announcement Module
Collapse
No announcement yet.
WARNING: Hibernate caching and AbstractTransactionalDataSourceSpringContextTests Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • WARNING: Hibernate caching and AbstractTransactionalDataSourceSpringContextTests

    We've just struggled through troubleshooting a "complication" of using the AbstractTransactionalDataSourceSpringContextTests class.

    First off, let me say that the AbstractTransactionalDataSourceSpringContextTests class (and it's brethren) absolutely rock. I love, love, love that we can do integration tests without firing up anything but junit. It makes testing our DAO and service layer an absolute pleasure.

    But here's the complication...

    Hibernates default cache (EHCache) is not transactional. That can easily lead to getting bitten if your tests look like this:

    Code:
    	public void test_getModuleById() {
    		String newModuleId1 = "test_module_1";
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.MODULE (ID, DISPLAYNAME, CLASSNAME) VALUES (?, 'module_displayname1', 'module_classname1')", new Object[] {newModuleId1});
    		
    		Module module = this.moduleDao.getModuleById(newModuleId1);
    		assertEquals(newModuleId1, module.getId());
    		assertEquals("module_displayname1", module.getDisplayName());
    		assertEquals("module_classname1", module.getClassName());
    	}
    	
    	public void test_getModuleById_QuestionsCollection() {		
    		String newModuleId1 = "test_module_1";
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.MODULE (ID, DISPLAYNAME, CLASSNAME) VALUES (?, 'module_displayname1', 'module_classname1')", new Object[] {newModuleId1});
    		
    		// create a question and associate it with the module
    		Long newQuestionId1 = jdbcTemplate.queryForLong("SELECT ABHC_DATA.ID_HI_SEQUENCE.NEXTVAL FROM DUAL");
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.QUESTION (ID, RESULTSFIELDNAME, QUESTIONNUMBER, LABEL, MODULE, UBERQRESPONSETYPE, UBERQWIDGETCLASS) VALUES (?, 'test_resultfield1', 1, 'test_label1', ?, 'BOOLEAN', 'test_uberqwidgetclass')", new Object[] {newQuestionId1, newModuleId1});		
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.ANSWER_OPTION (ID, DESCRIMINATOR, LABEL, ANSWERINDEX, QUESTION, VALUE, MORE, UBERQVALUE) VALUES (ABHC_DATA.ID_HI_SEQUENCE.NEXTVAL, 'abhto.abhc.core.SingleSelectAnswerOption', 'test_label', 0, ?, 'test_value', 0, 'test_uberqvalue')", new Object[] {newQuestionId1});
    		
    		// flush the jdbc statements
    		hibernateSessionFactory.getCurrentSession().flush();
    		
    		Module module = this.moduleDao.getModuleById(newModuleId1);
    		
                    // COMPLICATION: this assert fails with a size of 0 reported because the module object cached from the first test method is what's actually asserted against here
    		assertEquals(1, module.getQuestions().size());		
    	}
    Spring will properly roll back the db-related events when each test method fails. But Spring (more accurately, Hibernate's EHCache) will *NOT* rollback its cache entries that occur as a result of those db events

    In hindsight this is all obvious. Hibernate's EHCache is not transactional, so the above behavior is exactly what you expect.

    But it can come as a bit of surprise if EHCache isn't at the forefront of your mind as you go about writing tests like the above.

    Can I suggest that the docs for AbstractTransactionalDataSourceSpringContextTests (and other appropriate Spring test classes) be updated with a warning to be sure to consider caching when writing tests like this?

    It's awesome that Spring makes each test method an isolated unit and rolls-back all db-related events at the end of each method.

    But it also made us lazy, we assumed that it was doing a complete rollback of all state prior to the test-method call. A warning to developers that it probably won't roll back *all* db-related state with each method (unless you're using a transactional cache) would give developers (like us) some warning that we need to stay on our toes.

    I'd be happy to make the modifications to the documentation if that's allowed, just point me in the right direction for getting that done.

    Thanks!

    - Gary

    P.S. Our solution to the above problem has been to evict the cached entries before each method run. That would make the two tests above look like this:

    Code:
    	public void test_getModuleById() {
    		String newModuleId1 = "test_module_1";
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.MODULE (ID, DISPLAYNAME, CLASSNAME) VALUES (?, 'module_displayname1', 'module_classname1')", new Object[] {newModuleId1});
    		
    		Module module = this.moduleDao.getModuleById(newModuleId1);
    		assertEquals(newModuleId1, module.getId());
    		assertEquals("module_displayname1", module.getDisplayName());
    		assertEquals("module_classname1", module.getClassName());
    	}
    	
    	public void test_getModuleById_QuestionsCollection() {
                    // SOLUTION: evict cached entries
    		hibernateSessionFactory.evict(Module.class);
    		hibernateSessionFactory.evict(Question.class);
    		hibernateSessionFactory.evictCollection(Module.class.getName() + ".questions");
    		
    		String newModuleId1 = "test_module_1";
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.MODULE (ID, DISPLAYNAME, CLASSNAME) VALUES (?, 'module_displayname1', 'module_classname1')", new Object[] {newModuleId1});
    		
    		// create a question and associate it with the module
    		Long newQuestionId1 = jdbcTemplate.queryForLong("SELECT ABHC_DATA.ID_HI_SEQUENCE.NEXTVAL FROM DUAL");
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.QUESTION (ID, RESULTSFIELDNAME, QUESTIONNUMBER, LABEL, MODULE, UBERQRESPONSETYPE, UBERQWIDGETCLASS) VALUES (?, 'test_resultfield1', 1, 'test_label1', ?, 'BOOLEAN', 'test_uberqwidgetclass')", new Object[] {newQuestionId1, newModuleId1});		
    		jdbcTemplate.update("INSERT INTO ABHC_DATA.ANSWER_OPTION (ID, DESCRIMINATOR, LABEL, ANSWERINDEX, QUESTION, VALUE, MORE, UBERQVALUE) VALUES (ABHC_DATA.ID_HI_SEQUENCE.NEXTVAL, 'abhto.abhc.core.SingleSelectAnswerOption', 'test_label', 0, ?, 'test_value', 0, 'test_uberqvalue')", new Object[] {newQuestionId1});
    		
    		// flush the jdbc statements
    		hibernateSessionFactory.getCurrentSession().flush();
    		
    		Module module = this.moduleDao.getModuleById(newModuleId1);
    		
    		assertEquals(1, module.getQuestions().size());		
    	}

  • #2
    I would not necessarily expect a roll-back but an invalidation of the entry in the cache. How is that working in applications?

    Joerg

    Comment


    • #3
      > I would not necessarily expect a roll-back but an invalidation

      With regard to the cache, I'm not sure I understand the distinction between "roll-back" and "invalidation".

      For a transactional cache, I would expect objects cached as a result of a transaction to be evicted if that transaction rolls-back.

      This is definitely *not* the behavior displayed by EHCache. Which makes sense to me since it doesn't advertise itself as "transactional".

      But maybe I misunderstanding the difference between a transactional and non-transactional cache? I only have experience with non-transactional caches.

      > How is that working in applications?

      The application-behavior is exactly the same as we see in our unit tests. Objects cached in a particular transaction remain in the cache even if the transaction is rolled-back. Those cached objects are returned during future hibernate queries.

      Which is very problematic if your database-access looks like the test code above.

      Bu luckily, in our particular app, the kind of situation demonstrated in the test is pretty rare.

      Or did I misunderstand your question?

      - Gary

      Comment


      • #4
        Originally posted by gaffonso View Post
        With regard to the cache, I'm not sure I understand the distinction between "roll-back" and "invalidation".
        Transactional means it goes back to the last state before beginning the transaction since a transaction has to be atomic. That would mean it restores the last version of the object. Invalidation just removes it from the cache so that it gets refetched from the database.

        Originally posted by gaffonso View Post
        Or did I misunderstand your question?
        No, that's exactly what I wondered about. How can one use transactions and second-level cache together?

        Joerg

        Comment

        Working...
        X