Spring TransactionalTemplate не откатывается при исключении - PullRequest
1 голос
/ 15 февраля 2020

Постановка проблемы

Моя цель:

  1. Считать строку файла CSV -> Преобразовать в объект DTO ( Этот шаг не связанный с моим вопросом, включенный для полноты )
  2. Сбор 'n' объектов DTO -> Вызовите методы обслуживания save () / update (). ( В том же контексте транзакции )

Ожидается: При сбое одного вызова save () / update () -> Откатить весь пакет.

Факт: При сбое одного вызова save () / update () -> Spring фиксирует запись для предыдущего вызова (-ов).

Реализация

Ниже моя реализация кода. ( Пропущенные вспомогательные методы и нерелевантные атрибуты для краткости )

Класс импортера:

Список групп (пациент) и вызовы save () / update () в зависимости от типа операции.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.github.swapnil.hims.bo.importer.BulkImporter;
import com.github.swapnil.hims.bo.importer.Importer;
import com.github.swapnil.hims.dto.PatientDetail;
import com.github.swapnil.hims.service.PatientService;

@Component
public class PatientImporter extends Importer implements BulkImporter {
    @Autowired
    private PatientService patientSvc;

    private final TransactionTemplate txTmpl;

    public PatientImporter(PlatformTransactionManager transactionManager) {
        this.txTmpl = new TransactionTemplate(transactionManager);
    }

    @Override
    public PatientDetail rowToObject(CSVRecord record) {
        // Convert row to DTO object.
        return patientDetail;
    }

    @Override
    public void saveToDb(List<Object> patients) {
        List<PatientDetail> patientList = patients.stream().map(patient -> (PatientDetail) patient)
                .collect(Collectors.toList());
        saveAll(patientList, this.importType, BATCH_SIZE);
    }

    private void saveAll(List<PatientDetail> patients, Importer.Type action, Integer batchSize) {
        txTmpl.execute(new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                createPatients(patients, action, batchSize);
            }
        });
    }

    private void createPatients(List<PatientDetail> patients, Importer.Type action, Integer batchSize) {
        int startAt = 0;
        while (true) {
            List<PatientDetail> patientBatch = patients.stream().skip(startAt).limit(batchSize)
                    .collect(Collectors.toList());

            for (PatientDetail patient : patientBatch) {
                if (action == Importer.Type.CREATE) {
                    patientSvc.save(patient);
                } else {
                    patientSvc.update(patient);
                }
            }

            if (patientBatch.size() != batchSize) {
                break;
            }

            startAt += batchSize;
        }
    }
}

Класс обслуживания пациента:

Класс репозитория / обслуживания для пациента.

import javax.transaction.Transactional;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.github.swapnil.hims.dao.PatientDao;
import com.github.swapnil.hims.dto.PatientDetail;
import com.github.swapnil.hims.entities.Patient;
import com.github.swapnil.hims.exception.PatientException;
import com.github.swapnil.hims.service.PatientService;

@Service
public class PatientServiceImpl implements PatientService {
    private final static Log logger = LogFactory.getLog(PatientServiceImpl.class);

    @Autowired
    private PatientDao patientDao;

    @Override
    @Transactional
    public void save(PatientDetail patientDetail) {
        patientDao.save(getPatient(patientDetail));
    }

    @Override
    @Transactional
    public void update(Long id, PatientDetail patientDetail) {
        patientDao.save(getPatient(id, patientDetail));
    }

    @Override
    public void update(PatientDetail patientDetail) {
        update(patientDetail.getIdentifier(), patientDetail);
    }

    private Patient getPatient(PatientDetail patientDetail) {
        if (StringUtils.isEmpty(patientDetail.getPatientId())) {
            throw new PatientException("Patient ID is required!");
        }

        if (StringUtils.isEmpty(patientDetail.getName())) {
            throw new PatientException("Patient name is required!");
        }

        if (StringUtils.isEmpty(patientDetail.getEmail())) {
            throw new PatientException("Patient e-mail is required!");
        }

        Patient dbPatient = patientDao.findByPatientId(patientDetail.getPatientId());
        if (dbPatient != null) {
            throw new PatientException(String.format("Patient with the same pid '%s' already exists!", dbPatient.getPatientId()));
        }

        return patientDetail.toPatient();
    }

    private Patient getPatient(Long id, PatientDetail patientDetail) {
        Patient patient = patientDao.findById(id);

        if (patient == null) {
            throw new PatientException("Patient with id " + id + " does not exists.");
        }

        patient.setName(StringUtils.isEmpty(patientDetail.getName()) ? patient.getName() : patientDetail.getName());
        patient.setCity(StringUtils.isEmpty(patientDetail.getCity()) ? patient.getCity() : patientDetail.getCity());
        patient.setAddress(StringUtils.isEmpty(patientDetail.getAddress()) ? patient.getAddress() : patientDetail.getAddress());
        patient.setEmail(StringUtils.isEmpty(patientDetail.getEmail()) ? patient.getEmail() : patientDetail.getEmail());
        patient.setContactNumber(patientDetail.getContactNumber() != null ? patient.getContactNumber() : patientDetail.getContactNumber());
        patient.setPatientId(StringUtils.isEmpty(patientDetail.getPatientId()) ? patient.getPatientId() : patientDetail.getPatientId());

        return patient;
    }
}

PatientDao. java

import org.springframework.data.jpa.repository.JpaRepository;

import com.github.swapnil.hims.entities.Patient;

public interface PatientDao extends JpaRepository<Patient, String> {
    public Patient findById(Long id);

    public void deleteById(Long id);

    public Patient findByPatientId(String patientId);
}

Я пытался:

  • try {createPatients(patients, action, batchSize);} catch() {status.setRollbackOnly()}
  • Установка PropagationBehaviour на PROPAGATION_REQUIRED.

Я не могу понять, чего мне здесь не хватает, я потратил кучу времени на решение этой проблемы и очень, очень признателен за любую помощь , Я использую MySQL v8.0.16.

(GitHub repo URL )

EDIT

Во время отладки, Я обнаружил, что Patient dbPatient = patientDao.findByPatientId(patientDetail.getPatientId()); вызов вызывает гибернацию к сеансу sh и переводит предыдущий объект в БД. Я не уверен, как предотвратить это, хотя. Я попробовал кое-что, но ничего не вышло, буду искать и обновлять.

Ответы [ 2 ]

0 голосов
/ 16 февраля 2020

Чтобы выполнить откат всей партии, необходимо аннотировать метод saveToDb в импортере пациентов с помощью @Transactional.

0 голосов
/ 16 февраля 2020

Почему вы не использовали @Transactional следующим образом:

@Transactional(rollbackFor = Exception.class)
public yourFunctionThatHasAllOperations() {
    // do your logic here
}
...