Почему @Transactional не откатывает сохранение сущности в этом интеграционном тесте? - PullRequest
0 голосов
/ 12 января 2020

У меня есть контроллер, который получает некоторые данные формы и должен

  • сохранить некоторые объекты в базу данных
  • сохранить все изображения, отправленные в файловую систему

Контроллер аннотирован @Transactional (хотя я читал, что не стоит создавать эту аннотацию на уровне контроллера ...) с rollbackFor = Exception.class, потому что, если возникает какое-либо исключение, я хочу Изменения отката, внесенные в любые объекты.

Когда я запускаю тест и проверяю, есть ли объект, которого я ожидал бы пропустить, есть или нет, он все еще там. Таким образом, @Transactional, похоже, не работает так, как я намеревался.

StringController. java, в src / main / java / com / example / controllers:

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<String> 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);
        }
    }
}

классифицированных контроллеров. java в src / test / java / com / example / controllers:

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<Classified> classifiedOptional = classifiedRepository.findById((long)5);
        assertFalse(classifiedOptional.isPresent()); // This is the assertion that is failing
        Optional<Place> 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());
    }
}

Результат теста:

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(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
[some lines omitted for brevity]
expected: <false> but was: <true>
org.opentest4j.AssertionFailedError: expected: <false> but was: <true>
    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)

Ответы [ 4 ]

2 голосов
/ 12 января 2020

Метод никогда не генерирует исключение, поэтому нет никаких причин, по которым Spring должен откатить транзакцию.

Если он действительно выдает исключение (например, добавив throw new RuntimeException(e); в конце блока catch), тогда Spring откатит транзакцию.

1 голос
/ 12 января 2020

Сгенерированное исключение

 if (true) {
  throw new Exception("The saved Classified should be deleted because of the *@Transactional* annotation");
 }

перехватывается:

 } catch (Exception e) {
  e.printStackTrace();
  // ...
 }

и не покидает метод addClassified , т.е. исключение распространяться не будет. Поэтому Spring не будет ничего делать.

На высоком уровне аннотация @transactional оборачивает ваш код в нечто вроде:

UserTransaction utx = entityManager.getTransaction();

try {
 utx.begin();

 addClassified(); // your actual method invocation

 utx.commit();
} catch (Exception ex) {
 utx.rollback();
 throw ex;
}

TL; DR : вы может удалить try-catch или (повторно) создать новое исключение внутри вашего блока catch.

1 голос
/ 12 января 2020

Вы ловите всплывающее исключение в блоке catch метода addClassified (@RequestPart (name = "data").

Вы должны выбросить исключение в блоке catch или удалить блок catch, чтобы Перехватчик Spring может узнать, что было сгенерировано исключение, и откатить транзакцию.

0 голосов
/ 12 января 2020

@ На самом деле вы не должны обрабатывать исключение, так как эта пружина не знает об исключении, поэтому не может выполнить откат. У вас есть требование, чтобы в случае возникновения исключения файл был удален. чем вы могли бы следовать.

public void addClassified(@RequestPart(name="data") ClassifiedFormData classifiedFormData,  @RequestPart(name="images") MultipartFile[] images) {

        //  to delete file  
        boolean flag = true;    

        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);

            flag = false;
        } finally {

             if(flag){
                storageService.deleteRecursively(pathToImagesForThisClassified);
                storageService.deleteRecursively(pathToThumbnailsForThisClassified);
             }
        }
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...