4
votes

I am working on an existing project using Spring and Hibernate and am getting confused because I get a

org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.

error when trying to save objects but I still can't find what exactly is wrong.

There is a service layer that is annotated using @Service and a save method that it should be transactional so it is annotated with @Transactional(readOnly = false). To me that means that spring should handle transactions itself.

@Service
public class LadyService {
    Logger log = Logger.getLogger(LadyService.class);
    @Autowired
    private PictureDAO pictureDao;
    @Autowired
    private LadyDAO ladyDao;
    @Autowired
    private AddressDAO addressDao;

    @Transactional(readOnly = false)
    public void save(Lady lady) {
        Address a = addressDao.getExistingAddress(lady.getAddress());
        if (a == null) {
            a = addressDao.save(lady.getAddress());
        }
        lady.setAddress(a);
        ladyDao.save(lady);
        pictureDao.savePictures(lady.getPictures());
    }

The error happens when doing a save in the AddressDAO. It is annotated as @Repository.

@Repository
public class AddressDAO extends HibernateDaoSupport {

    public Address save(Address address) {
        getHibernateTemplate().save(address);  <-- write not permitted error happens here
        return address;
    }

    @SuppressWarnings({ "unchecked" })
    public Address getExistingAddress(Address address) {
        DetachedCriteria cd = DetachedCriteria.forClass(Address.class);
        cd.add(Restrictions.eqOrIsNull("administrative_area_level_1",
                address.getAdministrative_area_level_1()));
        cd.add(Restrictions.eqOrIsNull("administrative_area_level_2",
                address.getAdministrative_area_level_2()));
        List<Address> result = (List<Address>) getHibernateTemplate()
                .findByCriteria(cd);

        if (result.isEmpty()) {
            return null;
        } else {
            return (Address) result.get(0);
        }
    }
}

What I thought would happen was that @Transactional makes spring create a session and a transaction for the save on the service layer, and that in the DAOs, the hibernate template would get the current session and transaction that spring manages and use it to save the objects.

The error message, though, makes me think that my service method and dao methods are not in the same transaction.

In the servlet-context.xml there are these statements:

<annotation-driven />

<context:component-scan base-package="com.kog.fable" />

<beans:bean id="mySessionFactory"
    class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <beans:property name="dataSource" ref="myDataSource" />
    <beans:property name="packagesToScan">
        <beans:array>
            <beans:value>com.kog.fable.**.*</beans:value>
        </beans:array>
    </beans:property>
    <beans:property name="hibernateProperties">
        <beans:props>
            <beans:prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect
            </beans:prop>
            <!-- create, validate, update -->
            <beans:prop key="hibernate.hbm2ddl.auto">create</beans:prop>
            <beans:prop key="hibernate.show_sql">false</beans:prop>
            <beans:prop key="hibernate.connection.pool_size">10</beans:prop>
            <beans:prop key="hibernate.connection.autocommit ">false</beans:prop>
        </beans:props>
    </beans:property>
</beans:bean>

<tx:annotation-driven transaction-manager="transactionManager" />

<beans:bean id="transactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <beans:property name="sessionFactory" ref="mySessionFactory" />
</beans:bean>

<beans:bean id="addressDAO" class="com.kog.fable.dao.AddressDAO">
    <beans:property name="sessionFactory" ref="mySessionFactory" />
</beans:bean>

<beans:bean id="ladyDAO" class="com.kog.fable.dao.LadyDAO">
    <beans:property name="sessionFactory" ref="mySessionFactory" />
</beans:bean>

<beans:bean id="pictureDAO" class="com.kog.fable.dao.PictureDAO">
    <beans:property name="sessionFactory" ref="mySessionFactory" />
</beans:bean>

Here I don't understand why, if component scan is used, the DAO beans are still declared explicitly. Shouldn't the component scan feature be able to create those by itself since the DAO classes are annotated with @Repository? Since I thought this configuration could create duplicate beans, I tried deleting the xml entries, but then I started getting:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'addressController': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.kog.fable.dao.AddressDAO com.kog.fable.controller.AddressController.addressDAO; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'addressDAO' defined in file [***\com\kog\fable\dao\AddressDAO.class]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: 'sessionFactory' or 'hibernateTemplate' is required

Here I thought that the extension of HibernateDaoSupport for my DAOs would make them inherit the sessionFactory and related methods so I don't understand what happens.

I have read that I could set the flush mode to AUTO or set the setCheckWriteOperations on the template to FALSE to solve that kind of problems and it seems to work, but I guess that this would not ensure the transaction coherence in all cases as I would like it.

Any help would be appreciated as I am quite new to Spring and Hibernate and am a bit stuck here.

3
When extending HibernateDaoSupport you will not benefit from autowiring, you will have to override the setSessionFactory method and put a @Autowired on it. Else it won't work. I would also expect a <tx:annotation-driven /> without that the @Transactional is pretty much useless and doesn't do anything. - M. Deinum
@M. Deinum thank you for your input. Indeed I missed the override, and now that it is done I can remove the dao beans entries in the xml without errors so I guess now I am sure the beans are instantiated once. Concerning the annotation-driven tag, it is an error on my part, I forgot to copy it here. I have this entry in the config file: <tx:annotation-driven transaction-manager="transactionManager" /> so it wasn't actually missing. The "write operations not allowed" error is still there. - Flop000
Make sure you aren't scanning for the same components twice. If you have an applicationContext.xml or whatever file that is loaded by the ContextLoaderListener make sure that you don't have the same <context:component-scan /> in there. - M. Deinum
@M.Deinum Again thank you for your input. I checked and the configuration seems to be correct. I did some more tests and actually after the modifications of your first comment it had solved my problem when calling the save method already. The problem was still there when I used a upsert method without the @transactional annotation that would then call the save method. And then I found out that this is normal and that this upsert method should have the @transactional annotation in order to work. So your comment solved the problem. - Flop000
@M.Deinum Also, if you write your comment as an answer I will mark it as solution since it has solved my problem. - Flop000

3 Answers

4
votes

When extending HibernateDaoSupport you will not benefit from autowiring, you will have to override the setSessionFactory method and put an @Autowired annotation on it. Else it won't work.

I would also expect a <tx:annotation-driven /> without that the @Transactional is pretty much useless and doesn't do anything.

0
votes

if your app is Spring MVC based...

in application-context try this..

 <bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
  <property name="sessionFactory" ref="yourSessionFactory"></property>
        </bean>

but in dispatcher-servlet (not in appContext !!!)

<tx:annotation-driven />

Dont forget namespaces for tx, and jar libraries spring-tx,spring-orm,hibernate-core

0
votes

In configuration file

do the change:-

@Configuration
@EnableTransactionManagement   <-----Put this line

public PersistenceConfig{
//your code
}

(OR)

@Bean
@Autowired
public HibernateTemplate getHibernateTemplate(SessionFactory session) {
        HibernateTemplate hb = new HibernateTemplate();
        hb.setCheckWriteOperations(false);
        hb.setSessionFactory(session);
        return hb;
    }