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.
- Do not use equals() and hashCode() methods in hibernate entities.
- Do not use Set on hibernate entities.
- Keep the business logic out of hibernate classes.
WOW!!! Hibernate!!! blast from the past!
ReplyDelete