How do you test Spring @Transactional without just hitting hibernate level 1 cache or doing manual session flush?

雨燕双飞 提交于 2019-11-30 15:17:09

问题


Using Spring + Hibernate and transactional annotations.

I'm trying to test the following:

  1. call a method that changes a User object then calls a @Transactional service method to persist it
  2. read the object back from the DB and insure it's values are correct after the method

The first problem I had was reading the User object in step 2 just returned the one in the Hibernate level 1 cache and did not actually read from the database.

I therefore manually evicted the object from the cache using the Session to force a read from the database. However, when I do this, the object values are never persisted within the unit test (I know it rolls back after the test is complete because of the settings I specified).

I tried manually flushing the session after the call to the @Transactional service method, and that DID commit the changes. However, that was not what I expected. I thought that a @Transactional service method would insure the transaction was committed and session flushed before it returned. I know in general Spring will decide when to do this management, but I thought the "unit of work" in a @Transactional method was that method.

In any case, now I'm trying to figure out how I would test a @Transactional method in general.

Here's a junit test method that is failing:

@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
@TransactionConfiguration(transactionManager = "userTransactionManager", defaultRollback = true)
@WebAppConfiguration()
@ContextConfiguration(locations = { "classpath:test-applicationContext.xml",
        "classpath:test-spring-servlet.xml",
        "classpath:test-applicationContext-security.xml" })
public class HibernateTest {

    @Autowired
    @Qualifier("userSessionFactory")
    private SessionFactory sessionFactory;

    @Autowired
    private UserService userService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private QueryService queryService;

    @Autowired
    private NodeService nodeService;

    @Autowired
    private UserUtils userUtils;

    @Autowired
    private UserContext userContext;

  @Test
    public void testTransactions() {
        // read the user
        User user1 = userService.readUser(new Long(77));
        // change the display name
        user1.setDisplayName("somethingNew");
        // update the user using service method that is marked @Transactional
        userService.updateUserSamePassword(user1);
        // when I manually flush the session everything works, suggesting the
        // @Transactional has not flushed it at the end of the method marked
        // @Transactional, which implies it is leaving the transaction open?
        // session.flush();
        // evict the user from hibernate level 1 cache to insure we are reading
        // raw from the database on next read
        sessionFactory.getCurrentSession().evict(user1);
        // try to read the user again
        User user2 = userService.readUser(new Long(77));
        System.out.println("user1 displayName is " + user1.getDisplayName());
        System.out.println("user2 displayName is " + user2.getDisplayName());
        assertEquals(user1.getDisplayName(), user2.getDisplayName());
    }
}

If I manually flush the session, then the test succeeds. However, I would have expected the @Transactional method to take care of committing and flushing the session.

The service method for updateUserSamePassword is here:

@Transactional("userTransactionManager")
@Override
public void updateUserSamePassword(User user) {
    userDAO.updateUser(user);
}

The DAO method is here:

@Override
public void updateUser(User user) {
    Session session = sessionFactory.getCurrentSession();
    session.update(user);
}

SesssionFactory is autowired:

@Autowired
@Qualifier("userSessionFactory")
private SessionFactory sessionFactory;

I'm using XML application context configuration. I have:

<context:annotation-config />
<tx:annotation-driven transaction-manager="userTransactionManager" />

And

<bean id="userDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> 
    <property name="driverClass" value="${user.jdbc.driverClass}"/>
    <property name="jdbcUrl" value="${user.jdbc.jdbcUrl}" />
    <property name="user" value="${user.jdbc.user}" />
    <property name="password" value="${user.jdbc.password}" />
    <property name="initialPoolSize" value="3" />
    <property name="minPoolSize" value="1" />
    <property name="maxPoolSize" value="17" />
</bean>

<bean id="userSessionFactory"
    class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="userDataSource" />
    <property name="configLocation" value="classpath:user.hibernate.cfg.xml" />
</bean>

<bean id="userTransactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="dataSource" ref="userDataSource" />
    <property name="sessionFactory" ref="userSessionFactory" />
</bean>

There is also a component scan on the services and dao classes. As I said, this is working in production.

I thought that if I have a method marked @Transactional that by the end of that method (e.g. the update method here), Spring would have forced the Session to commit and flush.

I can see only a few options:

  1. I misconfigured something, even though this is working for me in general (just not the unit tests). Any guesses? Any ideas for how to test this?

  2. Something about the unit test config themselves is not behaving the way the app would.

  3. Transactions and sessions don't work like that. My only deduction is that Spring is leaving the transaction and/or the session open after calling that update method. So when I manually evict the user on the Session object, those changes haven't been committed yet.

Can anyone confirm if this is expected behavior? Shouldn't @Transaction have forced commit and flush on session? If not, then how would one test a method marked @Transactional and that the methods actually work with transactions?

I.e., how should I rewrite my Unit test here?

Any other ideas?


回答1:


Here's what I was running into. Consider this code in a test method:

    String testDisplayNameChange = "ThisIsATest";
    User user = userService.readUser(new Long(77));
    user.setDisplayName(testDisplayNameChange);
    user = userService.readUser(new Long(77));
    assertNotEquals(user.getDisplayName(), testDisplayNameChange);

Note that the method userService.readUser is marked @Transactional in the service class.

If that test method is marked @Transactional the test fails. If it is NOT, it succeeds. Now I'm not sure if/when the Hibernate cache is actually getting involved. If the test method is transactional then each read happens in one transaction, and I believe they only hit the Hibernate level 1 cache (and don't actually read from the database). However, if the test method is NOT transactional, then each read happens in it's own transaction, and each does hit the database. Thus, the hibernate level 1 cache is tied to the session / transaction management.

Take aways:

  1. Even if a test method is calling multiple transactional methods in another class, if that test method itself is transactional, all of those calls happen in one transaction. The test method is the "unit of work". However, if the test method is NOT transactional, then each call to a transactional method within that test executes within it's own transaction.

  2. My test class was marked @Transactional, therefore every method will be transactional unless marked with an overriding annotation such as @AfterTransaction. I could just as easily NOT mark the class @Transactional and mark each method @Transactional

  3. Hibernate level 1 cache seems tied to the transaction when using Spring @Transactional. I.e. subsequent reads of an object within the same transaction will hit the hibernate level 1 cache and not the database. Note there is a level 2 cache and other mechanisms that you can tweak.

I was going to have a @Transactional test method then use @AfterTransaction on another method in the test class and submit raw SQL to evaluate values in the database. This would circumvent the ORM and hibernate level 1 cache altogether, insuring you were comparing actual values in the DB.

The simple answer was to just take @Transactional off of my test class. Yay.




回答2:


Q

Can anyone confirm if this is expected behavior? Shouldn't @Transaction have forced commit and flush on session? If not, then how would one test a method marked @Transactional and that the methods actually work with transactions?

A

It is the expected behavior. The spring aware transactional unit test support by design rollsback the transaction after the test is finished. This is by design.

An implicit transaction boundary is created per test (each method with @Test) and once the test is finished a rollback is done.

The consequence of this is after all tests are finished there is no data that is actually changed. That is the goal is to be more "unit" like and less "integration" like. You should read the spring documentation as to why this is advantageous.

If you really want to test data being persisted and to view that data outside of the transaction boundary I recommend a more end-to-end test like a functional/integration test such as selenium or hitting your external WS/REST API if you have one.

Q

I.e., how should I rewrite my Unit test here?

A

Your unit test does not work because you session.evict and not flush. Evicting will cause the changes not to be synchronized even though you already called session.update your in a transaction and hibernate batches operations. This is more clear with raw SQL as hibernate defers persisting to batch up all the operations so it waits until the session is flushed or closed to communicate with the database for performance. If you session.flush though the SQL will be executed right away (ie update will really happen) and then you can evict if you like to force a reread but your rereading within the transaction. I'm actually pretty sure flush will cause eviction so there is no need to call evict to force a reread but I might be wrong.



来源:https://stackoverflow.com/questions/26597440/how-do-you-test-spring-transactional-without-just-hitting-hibernate-level-1-cac

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!