title: Why is @Transactional not rolling back saving an entity in this integration test?
body: <p>I have a controller that receives some form data, and is supposed to</p>
  
  <ul>
  <li>save some entities into a database</li>
  <li>save any images submitted into the file system</li>
  </ul>
  
  <p>The controller is annotated with <code>@Transactional</code> (though I've read it's not a great idea to have this annotation on a controller level...) with <code>rollbackFor = Exception.class</code>, because if any exception occurs, I want to rollback changes made to any entities.</p>
  
  <p>When I run the test and check that the entity I would expect to be gone is there or not, it's still there. So, <code>@Transactional</code> doesn't seem to be working as I intended it to.</p>
  
  <p>ClassifiedController.java, in src/main/java/com/example/controllers:</p>
  
  <pre><code>package com.example.controllers;
  
  import com.example.services.DefaultImageManipulationService;
  import com.example.services.ImageManipulationService;
  import com.example.entities.Classified;
  import com.example.entities.Place;
  import com.example.inbound.ClassifiedFormData;
  import com.example.repositories.ClassifiedRepository;
  import com.example.repositories.PlaceRepository;
  import com.example.services.StorageService;
  import org.springframework.http.MediaType;
  import org.springframework.transaction.annotation.Transactional;
  import org.springframework.web.bind.annotation.*;
  import org.springframework.web.multipart.MultipartFile;
  
  import java.awt.*;
  import java.io.File;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.util.List;
  
  @RestController
  @CrossOrigin(origins = "http://localhost:4200")
  public class ClassifiedController {
      private final ClassifiedRepository classifiedRepository;
      private final PlaceRepository placeRepository;
      private final StorageService storageService;
      private final ImageManipulationService imageManipulationService;
  
      public ClassifiedController(ClassifiedRepository classifiedRepository,
                                  PlaceRepository placeRepository,
                                  StorageService storageService,
                                  DefaultImageManipulationService imageManipulationService) {
          this.classifiedRepository = classifiedRepository;
          this.placeRepository = placeRepository;
          this.storageService = storageService;
          this.imageManipulationService = imageManipulationService;
      }
  
      @Transactional(rollbackFor = Exception.class)
      @PostMapping(path = "/classifieds", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE })
      public void addClassified(@RequestPart(name="data") ClassifiedFormData classifiedFormData,
                                @RequestPart(name="images") MultipartFile[] images) {
  
          /* The end goal here is to get a classified and a place into the DB.
          If anything goes wrong, the transaction should be rolled back, and any saved images and thumbnails
          should be deleted. */
          List&lt;String&gt; filePaths = null;
          Path pathToImagesForThisClassified = null;
          String thumbnailPath = null;
          Path pathToThumbnailsForThisClassified = null;
  
          try {
              Classified classified = new Classified();
              classified.setSummary(classifiedFormData.getSummary());
              classified.setDescription(classifiedFormData.getDescription());
              classified.setPrice(classifiedFormData.getPrice());
              classified.setCurrency(classifiedFormData.getCurrency());
              classifiedRepository.save(classified);
  
              if (true) {
                  throw new Exception("The saved Classified should be deleted because of the @Transactional annotation");
              }
  
              String idAsStr = String.valueOf(classified.getId());
              pathToImagesForThisClassified = Paths.get("images", idAsStr);
              filePaths = storageService.storeAll(pathToImagesForThisClassified, images);
              File thumbnail = imageManipulationService.resize(filePaths.get(classifiedFormData.getThumbnailIndex()),
                      new Dimension(255, 255));
              pathToThumbnailsForThisClassified = Paths.get("thumbnails", idAsStr);
              thumbnailPath = storageService.store(pathToThumbnailsForThisClassified, thumbnail);
              classified.setImagePaths(filePaths);
              classified.setThumbnailImagePath(thumbnailPath);
              classifiedRepository.save(classified);
  
              Place place = new Place(classified);
              place.setCountry(classifiedFormData.getCountry());
              place.setLabel(classifiedFormData.getLabel());
              place.setLatitude(Double.valueOf(classifiedFormData.getLat()));
              place.setLongitude(Double.valueOf(classifiedFormData.getLon()));
              placeRepository.save(place);
          } catch (Exception e) {
              e.printStackTrace();
              storageService.deleteRecursively(pathToImagesForThisClassified);
              storageService.deleteRecursively(pathToThumbnailsForThisClassified);
          }
      }
  }
  </code></pre>
  
  <p>ClassifiedControllerTest.java in src/test/java/com/example/controllers:</p>
  
  <pre><code>package com.example.controllers;
  
  import com.example.entities.Classified;
  import com.example.entities.Place;
  import com.example.inbound.ClassifiedFormData;
  import com.example.repositories.ClassifiedRepository;
  import com.example.repositories.PlaceRepository;
  import com.example.services.StorageService;
  import org.junit.jupiter.api.DisplayName;
  import org.junit.jupiter.api.Test;
  import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  import org.springframework.boot.test.context.SpringBootTest;
  import org.springframework.core.io.Resource;
  import org.springframework.http.MediaType;
  import org.springframework.mock.web.MockMultipartFile;
  import org.springframework.test.context.TestConstructor;
  import org.springframework.test.web.servlet.MockMvc;
  import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
  
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.util.Optional;
  
  import static org.junit.jupiter.api.Assertions.assertFalse;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  
  @SpringBootTest
  @AutoConfigureMockMvc
  @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
  @DisplayName("ClassifiedController")
  public class ClassifiedControllerTest {
  
      private final MockMvc mvc;
      private final ClassifiedRepository classifiedRepository;
      private final PlaceRepository placeRepository;
      private final StorageService storageService;
  
      public ClassifiedControllerTest(MockMvc mvc, ClassifiedRepository classifiedRepository,
                                      PlaceRepository placeRepository, StorageService storageService) {
          this.mvc = mvc;
          this.classifiedRepository = classifiedRepository;
          this.placeRepository = placeRepository;
          this.storageService = storageService;
      }
  
      @DisplayName("Any saved entities and files are deleted if an exception is encountered")
      @Test
      public void givenInvalidFormData_whenPosted_thenStatus400AndClean() throws Exception {
          // GIVEN
          ClassifiedFormData classifiedFormData = new ClassifiedFormData();
          classifiedFormData.setCountry("Spain");
          classifiedFormData.setCurrency("EUR");
          classifiedFormData.setSummary("Test");
          classifiedFormData.setDescription("Test");
          classifiedFormData.setLabel("Test");
          classifiedFormData.setPrice(32.45);
          classifiedFormData.setThumbnailIndex((byte)1);
          classifiedFormData.setLat("42.688630");
          classifiedFormData.setLon("-2.945620");
  
          MockMultipartFile classified = new MockMultipartFile("data", "", "application/json",
                  ("{\"summary\":\"feefwfewfew\",\"description\":\"fewfewfewfewfwe\",\"price\":\"34\"," +
                          "\"currency\":\"CAD\",\"thumbnailIndex\":0,\"lat\":\"52.2460367\",\"lon\":\"0.7125173\"," +
                          "\"label\":\"Bury St Edmunds, Suffolk, East of England, England, IP33 1BZ, United Kingdom\"," +
                          "\"country\":\"United Kingdom\"}").getBytes());
  
          byte[] image1Bytes = getClass().getClassLoader().getResourceAsStream("test_image.jpg").readAllBytes();
          byte[] image2Bytes = getClass().getClassLoader().getResourceAsStream("test_image.jpg").readAllBytes();
  
          String image1Filename = "image1.jpg";
          String image2Filename = "image2.jpg";
  
          MockMultipartFile image1 =
                  new MockMultipartFile("images", image1Filename,"image/jpeg", image1Bytes);
          MockMultipartFile image2 =
                  new MockMultipartFile("images", image2Filename, "image/jpeg", image2Bytes);
  
          Path expectedImagePath = Paths.get("images", "5");
          Path expectedThumbnailPath = Paths.get("thumbnails", "5");
  
          // WHEN-THEN
          mvc.perform(MockMvcRequestBuilders.multipart("/classifieds")
                  .file(classified)
                  .file(image1)
                  .file(image2)
                  .contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
                  .andExpect(status().isOk());
  
          Optional&lt;Classified&gt; classifiedOptional = classifiedRepository.findById((long)5);
          assertFalse(classifiedOptional.isPresent()); // This is the assertion that is failing
          Optional&lt;Place&gt; placeOptional = placeRepository.findByClassifiedId(5);
          assertFalse(placeOptional.isPresent());
  
          Resource image1AsResource = storageService.loadAsResource(expectedImagePath, image1Filename);
          Resource image2AsResource = storageService.loadAsResource(expectedImagePath, image2Filename);
          Resource thumbnailAsResource = storageService.loadAsResource(expectedThumbnailPath, "thumbnail.jpg");
  
          assertFalse(image1AsResource.exists());
          assertFalse(image2AsResource.exists());
          assertFalse(thumbnailAsResource.exists());
      }
  }
  </code></pre>
  
  <p>Test result:</p>
  
  <pre><code>java.lang.Exception: The saved Classified should be deleted because of the @Transactional annotation
      at com.example.controllers.ClassifiedController.addClassified(ClassifiedController.java:67)
      at com.example.controllers.ClassifiedController$$FastClassBySpringCGLIB$$7850f537.invoke(&lt;generated&gt;)
      at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
  [some lines omitted for brevity]
  expected: &lt;false&gt; but was: &lt;true&gt;
  org.opentest4j.AssertionFailedError: expected: &lt;false&gt; but was: &lt;true&gt;
      at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
      at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:40)
      at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:35)
      at org.junit.jupiter.api.Assertions.assertFalse(Assertions.java:210)
      at com.example.controllers.ClassifiedControllerTest.givenInvalidFormData_whenPosted_thenStatus400AndClean(ClassifiedControllerTest.java:148)
  </code></pre>