Wednesday, February 26, 2020

Don't Mess With The Hibernate


Last week I've assist a friend solving a Hibernate voodoo.
He was getting an "integrity constraint violation" trying to delete a table entry.
The root cause for the problem was that he was messing with the hibernate entities.
This is an example of what you should NOT do: do not mix business logic into hibernate layer.
Let's review a simplified example of this problem.


The Data Model


We want to create a recipe book, so we keep a list of recipes and ingredients.
Each recipe includes a list of ingredients, so we need two entities.



The recipe:

package org.demo.hibernate.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.HashSet;
import java.util.Set;

@Entity
public class Recipe {
    @Id
    @GeneratedValue
    @Column
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "recipe")
    private Set<Ingredient> ingredients = new HashSet<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public Set<Ingredient> getIngredients() {
        return ingredients;
    }

    public void setIngredients(Set<Ingredient> ingredients) {
        this.ingredients = ingredients;
    }
}


The Ingredient:


package org.demo.hibernate.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.util.Objects;

@Entity
public class Ingredient {
    @Id
    @GeneratedValue
    @Column
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false)
    private Recipe recipe;

    @Column(nullable = false)
    private String name;

    public Recipe getRecipe() {
        return recipe;
    }

    public void setRecipe(Recipe recipe) {
        this.recipe = recipe;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Ingredient ingredient = (Ingredient) o;
        return name.equals(ingredient.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}



Notice that we want to avoid a recipe having the same ingredient twice, so we keep a set of ingredients, and implement equals() and hashCode() methods in the ingredient class.


The Repositories


To simplify database access, we use JPA, and implement two repositories.

The Recipe Repository:


package org.demo.hibernate.repositories;

import org.demo.hibernate.entities.Recipe;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface RecipeRepository extends JpaRepository<Recipe, Integer> {
    @Transactional
    Recipe findByName(String name);
}


The Ingredient Repository:


package org.demo.hibernate.repositories;

import org.demo.hibernate.entities.Ingredient;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface IngredientRepository extends JpaRepository<Ingredient, Integer> {
}


The Service


We implement a service to add, delete, and update ingredient name, as well as printing all of the recipes.



package org.demo.hibernate;

import org.demo.hibernate.entities.Ingredient;
import org.demo.hibernate.entities.Recipe;
import org.demo.hibernate.repositories.IngredientRepository;
import org.demo.hibernate.repositories.RecipeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
public class RecipeService {

    @Autowired
    private IngredientRepository ingredientRepository;

    @Autowired
    private RecipeRepository recipeRepository;

    @Transactional
    public void createRecipe(String name, String... ingredients) {
        Recipe recipe = new Recipe();
        recipe.setName(name);
        recipeRepository.save(recipe);

        for (String ingredient : ingredients) {
            Ingredient i = new Ingredient();
            i.setName(ingredient);
            i.setRecipe(recipe);
            ingredientRepository.save(i);
        }
    }

    @Transactional
    public void updateRecipe(String name, String oldIngredientName, String newIngredientName) {
        Recipe recipe = recipeRepository.findByName(name);
        for (Ingredient ingredient : recipe.getIngredients()) {
            if (ingredient.getName().equals(oldIngredientName)){
                ingredient.setName(newIngredientName);
                ingredientRepository.save(ingredient);
            }
        }

    }

    @Transactional
    public void deleteRecipe(String name) {
        Recipe recipe = recipeRepository.findByName(name);
        for (Ingredient ingredient : recipe.getIngredients()) {
            ingredientRepository.delete(ingredient);
        }

        recipeRepository.delete(recipe);
    }

    @Transactional(readOnly = true)
    public void printAll() {
        System.out.println("=== all recipes ===");
        for (Recipe recipe : recipeRepository.findAll()) {
            System.out.println(recipe.getName());
            for (Ingredient ingredient : recipe.getIngredients()) {
                System.out.println("   " + ingredient.getName());
            }

        }

    }
}


The Business Logic


Next, we add recipes, update, and print them:


package org.demo.hibernate;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MyApplication.class, args);
        RecipeService service = context.getBean(RecipeService.class);

        service.createRecipe("soup", "onion", "oil", "salt");
        service.createRecipe("salad", "cucumber", "tomato");
        service.printAll();

        service.updateRecipe("soup", "oil", "salt");
        service.printAll();

        service.deleteRecipe("soup");
    }
}


The first printing of the recipes looks fine:


=== all recipes ===
soup
   oil
   salt
   onion
salad
   cucumber
   tomato


But then we update int the soup recipe the oil to be salt.

That's not nice, as we expects unique ingredients names, but hey, we've added the business logic into hibernate, so it should be automatically solved, right?

The recipes print now shows:

=== all recipes ===
soup
   oil
   onion
salad
   cucumber
   tomato


Great, right?

Well, actually not.
The database still has 3 ingredients, but we've made hibernate aware of only two of them.
That's the next line, deleting the recipe, fails with error:


Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: 
could not execute statement; SQL [n/a]; constraint ["FKJ0S4YWMQQQW4H5IOMMIGH5YJA: PUBLIC.INGREDIENT FOREIGN KEY(RECIPE_ID) REFERENCES PUBLIC.RECIPE(ID) (1)"; SQL statement:
delete from recipe where id=? [23503-200]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:298)
 at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
 at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:538)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:744)
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:712)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:631)
 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385)
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
 at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
 at org.demo.hibernate.RecipeService$$EnhancerBySpringCGLIB$$57709a2e.deleteRecipe()
 at org.demo.hibernate.MyApplication.main(MyApplication.java:20)
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
 at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:59)
 at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
 at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
 at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
 at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:200)
 at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45)
 at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3542)
 at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3801)
 at org.hibernate.action.internal.EntityDeleteAction.execute(EntityDeleteAction.java:100)
 at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
 at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
 at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
 at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
 at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348)
 at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
 at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:108)
 at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1344)
 at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:435)
 at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3221)
 at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2389)
 at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:447)
 at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183)
 at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40)
 at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281)
 at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
 at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:534)
 ... 10 more
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: 
Referential integrity constraint violation: "FKJ0S4YWMQQQW4H5IOMMIGH5YJA: PUBLIC.INGREDIENT FOREIGN KEY(RECIPE_ID) REFERENCES PUBLIC.RECIPE(ID) (1)"; 
SQL statement:
delete from recipe where id=? [23503-200]
 at org.h2.message.DbException.getJdbcSQLException(DbException.java:459)
 at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
 at org.h2.message.DbException.get(DbException.java:205)
 at org.h2.message.DbException.get(DbException.java:181)
 at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:373)
 at org.h2.constraint.ConstraintReferential.checkRowRefTable(ConstraintReferential.java:390)
 at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:265)
 at org.h2.table.Table.fireConstraints(Table.java:1057)
 at org.h2.table.Table.fireAfterRow(Table.java:1075)
 at org.h2.command.dml.Delete.update(Delete.java:153)
 at org.h2.command.CommandContainer.update(CommandContainer.java:198)
 at org.h2.command.Command.executeUpdate(Command.java:251)
 at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:191)
 at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:152)
 at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
 at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
 at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
 ... 31 more



Final Thoughts


The root cause of this problem is mixing business logic into hibernate.

  1. Do not use  equals() and hashCode() methods in hibernate entities.
  2. Do not use Set on hibernate entities.
  3. Keep the business logic out of hibernate classes.







1 comment: