mardi 21 juin 2016

Weird java foreign key constraint failure when running unit tests

So i've recently been hitting my head against the wall because of a problem I have when I try to unit test my services.

I have a User which has multiple unidirectional relatshionships to the Product, Customer and Quotation classes. I use SpringBoot with the Spring JPA repositories to perform opperations to the MySQL database using Hibernate as ORM.

I also have different services which call the repositories to perform these actions to the database, and it are these that I am trying to unit test.

When I run my unit test classes seperately They all succeed without problems, but when I run them all together the QuotationService tests fail completely. Noteworthy however is that they always run after the CustomerService tests.

After looking at the stacktrace I found out that every test fails because of a foreign key constraint when I try to save a quotation to the database in the testclass.

Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (`kmobeheerdb`.`quotation`, CONSTRAINT `FK_fq5x6je1sicnskcj35yqhp85g` FOREIGN KEY (`quotation_id`) REFERENCES `user` (`user_id`))
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:683)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:663)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:653)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:108)
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2061)
at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1819)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2033)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:1969)
at com.mysql.cj.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:4953)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1954)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:208)
... 105 more

The part of the tests where they always fail is in the @Before section, when I try to save a new quotation.

@Autowired private QuotationService quotationService;
    @Autowired private UserService userService;

    private User user;
    private Quotation quotation;

    @Before
    public void setup() throws UserServiceException, QuotationServiceException {
        String userEmail = "tzestermail@mail.com";
        String userName = "testername";

        User newUser = new User(userEmail, userName);
        user = userService.register(newUser);

        //Tests fail on the createQuotation statement beneath
        Quotation newQuotation = new Quotation();
        quotation = quotationService.createQuotation(newQuotation, user.getUserId()); 

        user = userService.getUser(user.getUserId());

        assertNotNull(user);
        assertNotNull(quotation);
        assertEquals(1, user.getQuotations().size());
    }

I searched around a lot to try and solve this issue, including making the relations bidirectional, but nothing really worked and the error persisted.

For reference:

User class:

@Entity
@Table(name = "user", uniqueConstraints = {
        @UniqueConstraint(name = "UX_user_email", columnNames = {"email"})
})
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "user_id", nullable = false)
    private int userId;
    private String email;
    private String password;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "productId", orphanRemoval = true)
    private Set<Product> products;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "quotationId", orphanRemoval = true)
    private Set<Quotation> quotations;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "customerId", orphanRemoval = true)
    private Set<Customer> customers;

    public User(String email, String password) {
        this.email = email;
        this.password = password;
    }

    public User() {
    }

    public void addCustomer(Customer customer){
        if(customer != null){
            if(customers == null){
                customers = new HashSet<>();
            }
            customers.add(customer);
        }
    }

    public void removeCustomer(Customer customer){
        if(customer != null && customers.contains(customer)){
            customers.remove(customer);
        }
    }

    public void addQuotation(Quotation quotation){
        if(quotation != null){
            if(quotations == null){
                quotations = new HashSet<>();
            }
            quotations.add(quotation);
        }
    }

    public void removeQuotation(Quotation quotation){
        if(quotation != null && quotations.contains(quotation)){
            quotations.remove(quotation);
        }
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Set<Product> getProducts() {
        return products;
    }

    public void setProducts(Set<Product> products) {
        this.products = products;
    }

    public Set<Quotation> getQuotations() {
        return quotations;
    }

    public void setQuotations(Set<Quotation> quotations) {
        this.quotations = quotations;
    }

    public Set<Customer> getCustomers() {
        return customers;
    }

    public void setCustomers(Set<Customer> customers) {
        this.customers = customers;
    }
}

Customer class:

@Entity
@Table(name = "customer", uniqueConstraints = {
        @UniqueConstraint(name = "UX_customer_email", columnNames = {"email"})
})
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "customer_id", nullable = false)
    private int customerId;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "quotationId", cascade = CascadeType.REMOVE)
    private List<Quotation> quotations;

    @Column(nullable = false)
    private String email;
    private String name;

    public Customer() {
    }

    public Customer(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public int getCustomerId() {
        return customerId;
    }

    public void setCustomerId(int customerId) {
        this.customerId = customerId;
    }

    public List<Quotation> getQuotations() {
        return quotations;
    }

    public void setQuotations(List<Quotation> quotations) {
        this.quotations = quotations;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Quotation class:

@Entity
public class Quotation {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "quotation_id", nullable = false)
    private int quotationId;
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "productId", cascade = CascadeType.PERSIST)
    private Set<Product> products;

    private double routeTotal;
    private double total;
    private String memo;

    public Quotation() {
    }

    public int getQuotationId() {
        return quotationId;
    }

    public void setQuotationId(int quotationId) {
        this.quotationId = quotationId;
    }

    public Set<Product> getProducts() {
        return products;
    }

    public void setProducts(Set<Product> products) {
        this.products = products;
    }

    public double getRouteTotal() {
        return routeTotal;
    }

    public void setRouteTotal(double routeTotal) {
        this.routeTotal = routeTotal;
    }

    public double getTotal() {
        return total;
    }

    public void setTotal(double total) {
        this.total = total;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }
}

User Service

@Transactional(rollbackFor = UserServiceException.class)
@Service
public class UserServiceImpl implements UserService {
    private Logger logger = LogManager.getLogger();
    @Autowired private PasswordEncoder passwordEncoder;
    @Autowired private UserRepository userRepository;
    @Autowired private ExceptionStrings exceptionStrings;

    @Override
    public User register(User user) throws UserServiceException{
        if(user == null || Strings.isNullOrEmpty(user.getEmail()) || Strings.isNullOrEmpty(user.getPassword())){
            logger.error(exceptionStrings.registerInputError);
            throw new UserServiceException(exceptionStrings.registerInputError);
        }
        try{
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            return userRepository.save(user);
        } catch (DataIntegrityViolationException e){
            logger.error(exceptionStrings.registerError, e);
            throw new UserServiceException(exceptionStrings.registerError, e);
        }
    }

    @Override
    public User login(User user) throws UserServiceException {
        if(user == null || Strings.isNullOrEmpty(user.getEmail()) || Strings.isNullOrEmpty(user.getPassword())){
            logger.error(exceptionStrings.loginInputError);
            throw new UserServiceException(exceptionStrings.loginInputError);
        }
        User checkUser = userRepository.findByEmail(user.getEmail());

        if(checkUser == null || !passwordEncoder.matches(user.getPassword(), checkUser.getPassword())){
            logger.error(exceptionStrings.loginError);
            throw new UserServiceException(exceptionStrings.loginError);
        }
        checkUser.setPassword(null);
        return checkUser;
    }

    @Override
    public User updateUser(User user) throws UserServiceException {
        if(user == null || user.getUserId() == 0 || userRepository.findOne(user.getUserId()) == null){
            logger.error(exceptionStrings.updateUserInputError);
            throw new UserServiceException(exceptionStrings.updateUserInputError);
        }
        return userRepository.save(user);
    }

    @Override
    public void deleteUser(User user) throws UserServiceException {
        if(user == null || user.getUserId() == 0 || userRepository.findOne(user.getUserId()) == null){
            logger.error(exceptionStrings.deleteUserInputError);
            throw new UserServiceException(exceptionStrings.deleteUserInputError);
        }
        userRepository.delete(user);
    }

    @Override
    public User getUser(int userId) {
        return userRepository.findOne(userId);
    }
}

Quotation Service:

@Transactional(rollbackFor = QuotationServiceException.class)
@Service
public class QuotationServiceImpl implements QuotationService {
    private Logger logger = LogManager.getLogger();
    @Autowired private ExceptionStrings exceptionStrings;
    @Autowired private UserService userService;
    @Autowired private QuotationRepository quotationRepository;

    @Override
    public Quotation createQuotation(Quotation quotation, int userId) throws QuotationServiceException {
        if (quotation == null || userId == 0 || userService.getUser(userId) == null) {
            logger.error(exceptionStrings.createQuotationInputError);
            throw new QuotationServiceException(exceptionStrings.createQuotationInputError);
        }
        try {
            User user = userService.getUser(userId);
            user.addQuotation(quotation);
            return quotationRepository.save(quotation);
        } catch (DataIntegrityViolationException e) {
            logger.error(exceptionStrings.createQuotationError);
            throw new QuotationServiceException(exceptionStrings.createQuotationError, e);
        }
    }

    @Override
    public Quotation getQuotation(int quotationId) {
        return quotationRepository.findOne(quotationId);
    }

    @Override
    public Quotation updateQuotation(Quotation quotation) throws QuotationServiceException {
        if (quotation == null || quotation.getQuotationId() == 0 || quotationRepository.findOne(quotation.getQuotationId()) == null) {
            logger.error(exceptionStrings.updateQuotationInputError);
            throw new QuotationServiceException(exceptionStrings.updateQuotationInputError);
        }
        return quotationRepository.save(quotation);
    }

    @Override
    public void deleteQuotation(Quotation quotation) throws QuotationServiceException {
        if (quotation == null || quotation.getQuotationId() == 0 || quotationRepository.findOne(quotation.getQuotationId()) == null) {
            logger.error(exceptionStrings.updateQuotationInputError);
            throw new QuotationServiceException(exceptionStrings.updateQuotationInputError);
        }
        quotationRepository.delete(quotation);
    }
}

Customer Service:

@Transactional(rollbackFor = CustomerServiceException.class)
@Service
public class CustomerServiceImpl implements CustomerService {
    private Logger logger = LogManager.getLogger();
    @Autowired private CustomerRepository customerRepository;
    @Autowired private UserService userService;
    @Autowired private ExceptionStrings exceptionStrings;

    @Override
    public Customer createCustomer(Customer customer, int userId) throws CustomerServiceException {
        if(customer == null || userService.getUser(userId) == null ||
                customerRepository.findByEmail(customer.getEmail()) != null){
            logger.error(exceptionStrings.createCustomerInputError);
            throw new CustomerServiceException(exceptionStrings.createCustomerInputError);
        }

        try {
            User user = userService.getUser(userId);
            user.addCustomer(customer);
            return customerRepository.save(customer);
        } catch (DataIntegrityViolationException e){
            logger.error(exceptionStrings.createCustomerSaveError);
            throw new CustomerServiceException(exceptionStrings.createCustomerSaveError, e);
        }
    }

    @Override
    public Customer getCustomer(int customerId) {
        return customerRepository.findOne(customerId);
    }

    @Override
    public Customer updateCustomer(Customer customer) throws CustomerServiceException {
        if(customer == null || customer.getCustomerId() == 0 || customerRepository.findOne(customer.getCustomerId()) == null){
            logger.error(exceptionStrings.updateCustomerInputError);
            throw new CustomerServiceException(exceptionStrings.updateCustomerInputError);
        }
        return customerRepository.save(customer);
    }

    @Override
    public void deleteCustomer(Customer customer) throws CustomerServiceException {
        if(customer == null || customer.getCustomerId() == 0 || customerRepository.findOne(customer.getCustomerId()) == null){
            logger.error(exceptionStrings.deleteCustomerInputError);
            throw new CustomerServiceException(exceptionStrings.deleteCustomerInputError);
        }
        customerRepository.delete(customer);
    }
}

Aucun commentaire:

Enregistrer un commentaire