9
votes

I'm using Spring + Hibernate + JPA and I have a situation where I can't get my entities to persist to the database. I've set up a service class that is annotated with @Transactional. It uses a DAO that contains an injected EntityManager. When I call the function on the service object I see a bunch of selects for the reads the DAO is doing, but no updates/deletes as a result of merges and removes issued by my DAO. Surely there's something wrong with my setup, but I can't see it.

persistence.xml

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
    version="1.0">
    <persistence-unit name="pu">
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.InformixDialect" />
            <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider" />
            <property name="hibernate.showsql" value="true" />
            <property name="hibernate.cache.use_second_level_cache"
                value="false" />
        </properties>
    </persistence-unit>

config.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security-3.0.3.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost:5432/testdb" />  
        <property name="username" value="username" />
        <property name="password" value="password" />
    </bean>

    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="pu" />
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true" />
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
        p:entityManagerFactory-ref="entityManagerFactory" />

    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

    <context:annotation-config/>
    </beans>

AccountService.java

@Service("accountService")
@Transactional(propagation=Propagation.REQUIRED)
public class AccountService {

    private static final Logger log = Logger.getLogger(AccountService.class);

    @Autowired
    private UserDAO userDAO;

    public void activateUser(String username, String activationCode) {
        PendingActivation pendingActivation = userDAO.getPendingActivation(
                username, activationCode);

            Client client = pendingActivation.getClient();

            if (!userDAO.removePendingActivation(pendingActivation)) {
                log.warn("Unable to remove pending activation");
            }

            if (!userDAO.enableUser(client)) {
                log.error("Unable to enable client");
                return;
            }

            return;
        }
    }

UserDAOImpl.java

@Repository("userDAO")
public class UserDAOImpl implements UserDAO, Serializable {

    private static final long serialVersionUID = 1L;

    private static Logger log = Logger.getLogger(UserDAOImpl.class);

    @PersistenceContext
    EntityManager em;

    @Override 
    public PendingActivation getPendingActivation(String username, String activationCode) {
        Query q = em.createNamedQuery("getActivationCode")
            .setParameter("activationCode", activationCode);

        PendingActivation pendingActivation = null;
        try {
            pendingActivation = (PendingActivation)q.getSingleResult();
            return pendingActivation;
        }
        catch (Exception e) {
            log.warn("Could not retrieve activation code " + activationCode + " for user " + username, e);
            return null;
        }
    }

    @Override
    public boolean enableUser(Client client) {

        try {
            client.setEnabled(true);
            client = em.merge(client);   // this never generates an update
        }
        catch(Exception e) {
            log.error("Unable to enable client: " + client.getUsername(), e);
            return false;
        }

        return true;
    }

    @Override
    public boolean removePendingActivation(PendingActivation pendingActivation) {
        try {
            pendingActivation = (PendingActivation)em.getReference(PendingActivation.class, pendingActivation.getPendingActivationId());
            em.remove(pendingActivation);  // this never generates a delete
        }
        catch(Exception e) {
            log.warn("Unable to remove activation: " + pendingActivation.getActivationCode(), e);
            return false;
        }

        return true;
    }
}

AccountActivationController.java

@Controller
public class AccountActivationController {

    @Autowired
    @Qualifier("accountService")
    AccountService accountService;

    @RequestMapping("activate.do")
    public String doActivate(
            @RequestParam("activationCode") String activationCode,
            @RequestParam("username") String username,
            ModelMap model) {

        UnitCriteria unitCriteria = accountService.activateUser(username, activationCode);

        if (unitCriteria == null) {
            return "account/activationError";
        }

        model.addAttribute("fromActivation", true);
        return "forward:search.do?" + unitCriteria.toUrlParams(true);
    }

}
2
Where are you calling activateUser from?abalogh
I'm calling it from a servlet. I've added the code to the original post. Thanks!threejeez

2 Answers

20
votes

Ok, I figured out the problem. It took me forever to figure it out and had nothing to do with my database config, so I want to help people who have similar issues.

The Spring documentation states the following:

<tx:annotation-driven/> only looks for @Transactional on beans in the same application context it is defined in. This means that, if you put <tx:annotation-driven/> in a WebApplicationContext for a DispatcherServlet, it only checks for @Transactional beans in your controllers, and not your services. See Section 15.2, “The DispatcherServlet” for more information.

What's not posted in my original post is my servlet definition, which has the following lines of configuration code:

myServlet.xml

<context:annotation-config /> 
<context:component-scan base-package="com.myDomain.*" /> 

This brings all annotated beans, including Controllers, Services and Repositories, into the servlet context instead of the application context. And therein lies the problem. When Spring looks for beans annotated with @Transactional (due to the existence of <tx:annotation-driven/> in my config.xml file) it is looking for them in the application context. And, based on my config that was posted in my previous thread, there are no beans being loaded into my application context... they're all in the servlet context. Therefore, when my servlet was calling the beans annotated with @Service & @Transactional it was using beans that were not wrapped by transaction proxies. Thus, no transactions. The trick (rather, the correct way) was to change my config files in the following way:

myServlet.xml

<context:annotation-config /> 
<context:component-scan base-package="com.myDomain.servlets" /> 

config.xml

<context:annotation-config /> 
<context:component-scan base-package="com.myDomain.dao" /> 
<context:component-scan base-package="com.myDomain.services" /> 

This configuration ensures that all Controllers exist in the servlet context and Transactional Services and Repositories exist in the application context, which is where they belong. And finally, after many sleepless nights, my database writes are persisting.

1
votes

We can provide the Controller in servlet-context.xml as follows

    <context:component-scan base-package="com.myDomain" use-default-filters="false" >
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

This will ensure only Controller exists only in servlet-context. In root-context.xml use the following

    <context:component-scan base-package="com.myDomain">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

This will ensure the components other than Controller exists in application context. I searched for this solution a lot, as without this JPA was not updating the database, hope this will help someone