From: Gustavo Martin Morcuende Date: Fri, 20 Mar 2015 02:50:28 +0000 (+0100) Subject: MyBatis: BATCH operations X-Git-Url: https://git.gumartinm.name/?a=commitdiff_plain;h=6fae7291e148b3080c9d0026fa69c0092d6e2e66;p=JavaForFun MyBatis: BATCH operations --- diff --git a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.java b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.java new file mode 100644 index 0000000..7204c19 --- /dev/null +++ b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.java @@ -0,0 +1,9 @@ +package de.example.mybatis.batch.repository.mapper; + +import de.example.mybatis.model.Ad; + + +public interface AdSpringBatchMapper /** extends MyBatisScanFilter NO SCAN BY MyBatis!! **/ { + + int insert(Ad record); +} diff --git a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/TestMain.java b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/TestMain.java index 11e3677..2f894af 100644 --- a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/TestMain.java +++ b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/TestMain.java @@ -1,7 +1,12 @@ package de.example.mybatis.spring; +import java.sql.SQLException; + import org.apache.log4j.Logger; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import de.example.mybatis.spring.service.BatchAndSimpleSameTrx; +import de.example.mybatis.spring.service.ExampleBatchService; import de.example.mybatis.spring.service.ExampleCustomService; import de.example.mybatis.spring.service.ExampleService; @@ -18,14 +23,35 @@ public class TestMain { // exampleService.insertNewAd(); // // exampleService.getAdsByCriteria(); - - - final ExampleCustomService exampleCustomService = (ExampleCustomService) SpringContextLocator - .getInstance().getBean("exampleCustomService"); - - exampleCustomService.getAds(); +// +// +// final ExampleCustomService exampleCustomService = (ExampleCustomService) SpringContextLocator +// .getInstance().getBean("exampleCustomService"); +// +// exampleCustomService.getAds(); +// +// exampleCustomService.updateAds(); + + +// final ExampleBatchService exampleBatchService = (ExampleBatchService) SpringContextLocator +// .getInstance().getBean("exampleBatchService"); +// +// exampleBatchService.insertNewAd(); +// +// exampleBatchService.insertBatchNewAd(); + + + final BatchAndSimpleSameTrx batchAndSimpleSameTrx = (BatchAndSimpleSameTrx) SpringContextLocator + .getInstance().getBean("batchAndSimpleSameTrx"); - exampleCustomService.updateAds(); + try { + batchAndSimpleSameTrx.insertNewAd(); + } catch (CannotGetJdbcConnectionException e) { + logger.error("Error exception: ", e); + } catch (SQLException e) { + logger.error("Error exception: ", e); + } + } } diff --git a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/BatchAndSimpleSameTrx.java b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/BatchAndSimpleSameTrx.java new file mode 100644 index 0000000..7a15a89 --- /dev/null +++ b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/BatchAndSimpleSameTrx.java @@ -0,0 +1,105 @@ +package de.example.mybatis.spring.service; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Date; + +import javax.sql.DataSource; + +import org.apache.ibatis.jdbc.SQL; +import org.apache.log4j.Logger; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.annotation.Transactional; + +import de.example.mybatis.model.Ad; +import de.example.mybatis.repository.mapper.AdMapper; + + +public class BatchAndSimpleSameTrx { + private static final Logger logger = Logger.getLogger(BatchAndSimpleSameTrx.class); + + private AdMapper adMapper; + private DataSource dataSource; + + @Transactional + /** + * JUST IN THIS VERY MOMENT Spring sends "set autocommit = 0" EVEN IF THERE ARE NO OPERATIONS ON THE DATABASE!!! + * So, Spring is picking up some thread from the connections pool and giving it to the current Java thread. + */ + public void insertNewAd() throws CannotGetJdbcConnectionException, SQLException { + logger.info("Insert new Ad"); + + final Ad adTest = new Ad(); + adTest.setAdMobileImage("bild.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + /** + * No batched inserts will be sent to data base just in this very moment. + */ + this.adMapper.insert(adTest); + + /** + * We want to use SIMPLE and BATCH operations but MyBatis complains with this exception: + * "Cannot change the ExecutorType when there is an existing transaction". + * + * So, we get the connection for the current transaction by means of the Spring DataSourceUtils + * and using JDBC we implement the BATCH operation in the current open transaction + * (it was open because of the @Transactional annotation) + */ + this.doBatch(DataSourceUtils.getConnection(this.dataSource)); + + /** + * No batched inserts will be sent to data base just in this very moment. + */ + this.adMapper.insert(adTest); + + /** + * WHEN RETURNING FROM THIS METHOD Spring SENDS "set autocommit = 1" AND TRANSACTION ENDS + * (OR IF THERE IS ERROR Spring SENDS "rollback" AS EXPECTED) + */ + } + + private void doBatch(final Connection connection) throws SQLException { + + final PreparedStatement preparedStatement = connection.prepareStatement(doStatement()); + try { + for (int i = 0; i < 10; i++) { + /** + * Batched statements are not yet sent to data base. + */ + preparedStatement.addBatch(); + } + + /** + * RIGHT HERE THE BATCH STATEMENTS WILL BE SENT TO THE DATA BASE. + */ + preparedStatement.executeBatch(); + + } finally { + preparedStatement.close(); + } + } + + private String doStatement() { + return new SQL() { + { + INSERT_INTO("ad"); + VALUES("company_categ_id, ad_mobile_image, created_at, updated_at", + "'200', 'batch.jpg', '2015-03-20 02:54:50.0', '2015-03-20 02:54:50.0'"); + } + }.toString(); + } + + public void setAdMapper(final AdMapper adMapper) { + this.adMapper = adMapper; + } + + public void setDataSource(final DataSource dataSource) { + this.dataSource = dataSource; + } +} diff --git a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleBatchService.java b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleBatchService.java new file mode 100644 index 0000000..6412ec6 --- /dev/null +++ b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleBatchService.java @@ -0,0 +1,117 @@ +package de.example.mybatis.spring.service; + +import java.util.Date; + +import org.apache.log4j.Logger; +import org.springframework.transaction.annotation.Transactional; + +import de.example.mybatis.batch.repository.mapper.AdSpringBatchMapper; +import de.example.mybatis.model.Ad; +import de.example.mybatis.repository.mapper.AdMapper; + + +public class ExampleBatchService { + private static final Logger logger = Logger.getLogger(ExampleBatchService.class); + + private AdSpringBatchMapper adSpringBatchMapper; + private AdMapper adMapper; + + @Transactional + /** + * JUST IN THIS VERY MOMENT Spring sends "set autocommit = 0" EVEN IF THERE ARE NO OPERATIONS ON THE DATABASE!!! + * So, Spring is picking up some thread from the connections pool and giving it to the current Java thread. + */ + public void insertBatchNewAd() { + logger.info("Insert new Batch Ad"); + + final Ad adTest = new Ad(); + adTest.setAdMobileImage("bild.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + /** + * BECAUSE THIS IS A BATCH MAPPER, OPERATIONS WILL NOT BE SENT TO THE DATABASE UNTIL COMMAND commit IS SENT TO DATABASE. + * BECAUSE THIS METHOD IS ANNOTATED WITH @Transactional, THE COMMAND commit WILL BE SENT WHEN RETURNING FROM THIS METHOD + * (WHEN TRANSACTION ENDS UP) + * + * WORK AROUND: using SqlSession (MyBatis) we have access to the flushStatements method!!! + * SO, WE MUST INJECT THE SqlSession BEAN AND USE IT HERE IF WE DON'T WANT TO WAIT UNTIL TRANSACTION IS FINISHED. + */ + this.adSpringBatchMapper.insert(adTest); + + /** + * There will not be new "set autocommit = 0" because Spring remembers that this thread already started some transaction + * (it must be using ThreadLocals) + */ + this.insertPrivateNewAd(); + } + + private void insertPrivateNewAd() { + logger.info("Insert new private Ad"); + + final Ad adTest = new Ad(); + adTest.setAdMobileImage("bild.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + // MyBatis complains: "Cannot change the ExecutorType when there is an existing transaction". + // Work around in BatchAndSimpleSameTrx. + this.adMapper.insert(adTest); + } + + @Transactional + /** + * JUST IN THIS VERY MOMENT Spring sends "set autocommit = 0" EVEN IF THERE ARE NO OPERATIONS ON THE DATABASE!!! + * So, Spring is picking up some thread from the connections pool and giving it to the current Java thread. + */ + public void insertNewAd() { + logger.info("Insert new Ad"); + + final Ad adTest = new Ad(); + adTest.setAdMobileImage("bild.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + this.adMapper.insert(adTest); + + //DataSourceUtils.getConnection(); + + this.insertPrivateBatchNewAd(); + } + + private void insertPrivateBatchNewAd() { + logger.info("Insert new private Batch Ad"); + + final Ad adTest = new Ad(); + adTest.setAdMobileImage("bild.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + /** + * BECAUSE THIS IS A BATCH MAPPER, OPERATIONS WILL NOT BE SENT TO THE DATABASE UNTIL COMMAND commit IS SENT TO DATABASE. + * BECAUSE THIS METHOD IS ANNOTATED WITH @Transactional, THE COMMAND commit WILL BE SENT WHEN RETURNING FROM THIS METHOD + * (WHEN TRANSACTION ENDS UP) + * + * WORK AROUND: using SqlSession (MyBatis) we have access to the flushStatements method!!! + * SO, WE MUST INJECT THE SqlSession BEAN AND USE IT HERE IF WE DON'T WANT TO WAIT UNTIL TRANSACTION IS FINISHED. + */ + // MyBatis complains: "Cannot change the ExecutorType when there is an existing transaction". + // Workaround in BatchAndSimpleSameTrx. + this.adSpringBatchMapper.insert(adTest); + } + + public void setAdSpringBatchMapper(final AdSpringBatchMapper adSpringBatchMapper) { + this.adSpringBatchMapper = adSpringBatchMapper; + } + + public void setAdMapper(final AdMapper adMapper) { + this.adMapper = adMapper; + } +} diff --git a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleService.java b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleService.java index 8d868ca..a08f2b0 100644 --- a/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleService.java +++ b/MyBatis/MyBatis-Spring/src/main/java/de/example/mybatis/spring/service/ExampleService.java @@ -64,5 +64,7 @@ public class ExampleService { adTest.setCompanyId(2L); adTest.setUpdatedAt(new Date()); this.adMapper.insert(adTest); + + this.adMapper.insert(adTest); } } \ No newline at end of file diff --git a/MyBatis/MyBatis-Spring/src/main/resources/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.xml b/MyBatis/MyBatis-Spring/src/main/resources/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.xml new file mode 100644 index 0000000..68d0a0a --- /dev/null +++ b/MyBatis/MyBatis-Spring/src/main/resources/de/example/mybatis/batch/repository/mapper/AdSpringBatchMapper.xml @@ -0,0 +1,17 @@ + + + + + + + SELECT LAST_INSERT_ID() + + insert into ad (company_id, company_categ_id, ad_mobile_image, + created_at, updated_at, ad_gps + ) + values (#{companyId,jdbcType=BIGINT}, #{companyCategId,jdbcType=BIGINT}, #{adMobileImage,jdbcType=VARCHAR}, + #{createdAt,jdbcType=TIMESTAMP}, #{updatedAt,jdbcType=TIMESTAMP}, #{adGps,jdbcType=LONGVARBINARY} + ) + + + \ No newline at end of file diff --git a/MyBatis/MyBatis-Spring/src/main/resources/spring-config.xml b/MyBatis/MyBatis-Spring/src/main/resources/spring-config.xml index 2f91317..4670947 100644 --- a/MyBatis/MyBatis-Spring/src/main/resources/spring-config.xml +++ b/MyBatis/MyBatis-Spring/src/main/resources/spring-config.xml @@ -25,7 +25,8 @@ See http://mybatis.org/schema/mybatis-spring.xsd for further information. --> + marker-interface="de.example.mybatis.mapper.filter.MyBatisScanFilter" + template-ref="sqlSimpleSession" /> @@ -57,11 +58,22 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyBatis/MyBatis/src/main/java/de/example/mybatis/TestMainBatch.java b/MyBatis/MyBatis/src/main/java/de/example/mybatis/TestMainBatch.java new file mode 100644 index 0000000..2e2c93e --- /dev/null +++ b/MyBatis/MyBatis/src/main/java/de/example/mybatis/TestMainBatch.java @@ -0,0 +1,247 @@ +package de.example.mybatis; + +import java.io.IOException; +import java.util.Date; +import java.util.List; + +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.log4j.Logger; + +import de.example.mybatis.model.Ad; +import de.example.mybatis.model.AdCriteria; +import de.example.mybatis.repository.mapper.AdMapper; + + +public class TestMainBatch { + private static final Logger logger = Logger.getLogger(TestMainBatch.class); + + public static void main(final String[] args) throws IOException { + + // From org.xml.sax.InputSource Javadoc: + // The SAX parser will use the InputSource object to determine how to + // read XML input. If there is a character stream available, the parser + // will read that stream directly, disregarding any text encoding + // declaration found in that stream. If there is no character stream, + // but there is a byte stream, the parser will use that byte stream, + // using the encoding specified in the InputSource or else (if no + // encoding is specified) autodetecting the character encoding using an + // algorithm such as the one in the XML specification. If neither a + // character stream nor a byte stream is available, the parser will + // attempt to open a URI connection to the resource identified by the + // system identifier. + + // Then if we use an InputStream (it is not a character stream) and + // we do not specify the encoding, the encoding should be autodetected + // reading the XML header. :) That is what I want. :) + + + + // Scope and Lifecycle + // + // 1. SqlSessionFactoryBuilder: + // This class can be instantiated, used and thrown away. There is no + // need to keep it around once you've created your SqlSessionFactory. + // + // 2. SqlSessionFactory: + // Once created, the SqlSessionFactory should exist for the duration of + // your application execution. + // + // 3. SqlSession: + // Each thread should have its own instance of SqlSession. Instances of + // SqlSession are not to be shared and are not thread safe. Therefore + // the best scope is request or method scope. You should always ensure + // that it's closed within a finally block. + // + // 4. Mapper Instances: + // Mappers are interfaces that you create to bind to your mapped + // statements. Instances of the mapper interfaces are acquired from the + // SqlSession. They do not need to be closed explicitly. + + final SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() + .build(/**TestMain.class.getResourceAsStream("sql-maps-config.xml")**/ + Resources.getResourceAsStream("mybatis-sql-maps-config.xml"), "mybatisexample"); + + // The default openSession() method that takes no parameters will create + // a SqlSession with the following characteristics: + // + // * A transaction scope will be started (i.e. NOT auto-commit). + // * A Connection object will be acquired from the DataSource instance + // configured by the active environment. + // * The transaction isolation level will be the default used by the + // driver or data source. + // * No PreparedStatements will be reused, and no updates will be + // batched. + + // MyBatis uses two caches: a local cache and a second level cache. + // + // Each time a new session is created MyBatis creates a local cache and + // attaches it to the session. Any query executed within the session + // will be stored in the local cache so further executions of the same + // query with the same input parameters will not hit the database. The + // local cache is cleared upon update, commit, rollback and close. + // + // By default local cache data is used for the whole session duration. + // This cache is needed to resolve circular references and to speed up + // repeated nested queries, so it can never be completely disabled but + // you can configure the local cache to be used just for the duration of + // an statement execution by setting localCacheScope=STATEMENT. + // + // Note that when the localCacheScope is set to SESSION, MyBatis returns + // references to the same objects which are stored in the local cache. + // Any modification of returned object (lists etc.) influences the local + // cache contents and subsequently the values which are returned from + // the cache in the lifetime of the session. Therefore, as best + // practice, do not to modify the objects returned by MyBatis. + /** + * OVERRIDE THE DECLARED EXECUTOR IN mybatis-sql-maps-config.xml + * BY DEFAULT autocommit = false (what means autocommit=0 until session.close is called) + */ + SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); + + try { + final AdMapper adMapper = session.getMapper(AdMapper.class); + final Ad adTest = new Ad(); + adTest.setAdMobileImage("mobileImage.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + // This first insert took ages because I was dropping IPV6 connections. + // That is because during the first socket connection, the JVM + // tries to find out if IPV6 is available by means of opening a random + // AF_INET6 POSIX socket. + /** + * WITH BATCH OPERATIONS, INSERT WILL NOT BE PEFORMED UNTIL CALLING COMMIT OR FLUSHSTATEMENTS + * BUT set autocommit=0 WILL BE PEFORMED JUST IN THIS VERY MOMENT EVEN IF WE ARE NOT + * YET SENDING DATA!!!!! So, it is as always but without sending data. + */ + adMapper.insert(adTest); + adMapper.insert(adTest); + /** + * BY MEANS OF THIS METHOD WE CAN SEND THE BATCHED DATA TO DATA BASE AND + * RETRIEVE THE RESULTS OF THE BATCH OPERATIONS. OTHERWISE WE HAVE TO WAIT + * UNTIL session.commit AND WE LOOSE THE RESULTS. + */ + session.flushStatements(); + + /** + * IF WE DIDN'T USE THE flushStatements METHOD, IN THIS VERY MOMENT BATCH DATA WOULD BE SENT TO SERVER + * (BUT BECAUSE WE USED THE flushStatements METHOD NO DATA WILL BE SENT) + * JUST THE commit COMMAND IS SENT RIGHT NOW!!! + */ + session.commit(); + + final List adLists = adMapper.selectByExample(null); + for (final Ad ad : adLists) { + logger.info("Ad id: " + ad.getId()); + if (ad.getAdGps() != null) { + logger.info("Ad GPS: " + new String(ad.getAdGps(), "UTF-8")); + } + logger.info("Ad mobileImage: " + ad.getAdMobileImage()); + logger.info("Ad companyCategId: " + ad.getCompanyCategId()); + logger.info("Ad companyId: " + ad.getCompanyId()); + logger.info("Ad createdAt: " + ad.getCreatedAt()); + logger.info("Ad updatedAt: " + ad.getUpdatedAt()); + logger.info("\n"); + } + } finally { + // Besides this will restore the auto-commit value. + /** + * JUST IN THIS VERY MOMENT set autocommit=1 IS SENT!!!!! + * + * This is what @Transactional (Spring annotation) do when + * returning from an annotated transactional method. + */ + session.close(); + } + + /** + * OVERRIDE THE DECLARED EXECUTOR IN mybatis-sql-maps-config.xml + * BY DEFAULT autocommit = false (what means autocommit=0 until session.close is called) + */ + session = sqlSessionFactory.openSession(ExecutorType.BATCH); + + try { + logger.info("Last insert"); + final AdMapper adMapper = session.getMapper(AdMapper.class); + final Ad adTest = new Ad(); + adTest.setAdMobileImage("mobileImage.jpg"); + adTest.setCompanyCategId(200L); + adTest.setCreatedAt(new Date()); + adTest.setCompanyId(2L); + adTest.setUpdatedAt(new Date()); + + /** + * WITH BATCH OPERATIONS, INSERT WILL NOT BE PEFORMED UNTIL CALLING COMMIT + * (IF WE DON'T USE THE flushStatements METHOD) + * BUT set autocommit=0 WILL BE PEFORMED JUST IN THIS VERY MOMENT EVEN IF WE ARE NOT + * YET SENDING DATA!!!!! So, it is as always but without sending data. + */ + adMapper.insert(adTest); + + /** + * JUST IN THIS VERY MOMENT DATA ARE SENT TO SERVER, BUT NOT BEFORE!!!! + * (BECAUSE WE DID NOT USE THE flushStatements METHOD) + * THE commit COMMAND IS SENT RIGHT NOW!!!!! + */ + session.commit(); + + } finally { + // Besides this will restore the auto-commit value. + /** + * JUST IN THIS VERY MOMENT set autocommit=1 IS SENT!!!!! + * + * This is what @Transactional (Spring annotation) do when + * returning from an annotated transactional method. + */ + session.close(); + } + + session = sqlSessionFactory.openSession(); + + try { + logger.info("Using criteria"); + + final AdCriteria adCriteria = new AdCriteria(); + + adCriteria.or().andAdMobileImageEqualTo("mobileImage.jpg") + .andCreatedAtNotEqualTo(new Date()); + + adCriteria.or().andAdMobileImageNotEqualTo("noMobileImage.jpg") + .andAdMobileImageIsNotNull(); + + // where (ad_mobile_image = "mobileImage.jpg" and created_at <> Now()) + // or (ad_mobile_image <> "noMobileImage.jpg" and ad_mobile_image is not null) + + final AdMapper adMapper = session.getMapper(AdMapper.class); + final List adLists = adMapper.selectByExampleWithBLOBs(adCriteria); + for (final Ad ad : adLists) { + logger.info("Ad id: " + ad.getId()); + if (ad.getAdGps() != null) { + logger.info("Ad GPS: " + new String(ad.getAdGps(), "UTF-8")); + } + logger.info("Ad mobileImage: " + ad.getAdMobileImage()); + logger.info("Ad companyCategId: " + ad.getCompanyCategId()); + logger.info("Ad companyId: " + ad.getCompanyId()); + logger.info("Ad createdAt: " + ad.getCreatedAt()); + logger.info("Ad updatedAt: " + ad.getUpdatedAt()); + logger.info("\n"); + } + } finally { + // Besides this will restore the auto-commit value. + /** + * JUST IN THIS VERY MOMENT set autocommit=1 IS SENT!!!!! + * + * This is what @Transactional (Spring annotation) do when + * returning from an annotated transactional method. + */ + session.close(); + } + } + +}