"Write operations are not allowed in read-only mode" error : confused with Spring @Service @transaction @Repository and Hibernate

13,742

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.

Share:
13,742
Flop000
Author by

Flop000

Updated on August 21, 2022

Comments

  • Flop000
    Flop000 over 1 year

    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.

    • M. Deinum
      M. Deinum about 9 years
      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.
    • Flop000
      Flop000 about 9 years
      @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.
    • M. Deinum
      M. Deinum about 9 years
      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.
    • Flop000
      Flop000 about 9 years
      @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
      Flop000 about 9 years
      @M.Deinum Also, if you write your comment as an answer I will mark it as solution since it has solved my problem.
  • Tan Jinfu
    Tan Jinfu about 8 years
    But the method setSessionFactory is final in srping 4. Could you please tell how to resolve the problem?
  • M. Deinum
    M. Deinum about 8 years
    Create a constructor, autowire that, and call setSessionFactory from that. Of simply don't use HibernateDaoSupport as that isn't recommended anymore,