JPA/Hibernate - How implement soft delete in a #OneToMany relationship - mysql

I'm trying implement soft delete in a #OneToMany relationship in an academic project. I need to mark as "deleted" a child entity in the following scenarios:
When the parent entity is deleted (soft deleted as well).
When the child entity is no longer referenced by his parent.
I've achieved this requirement using the #SqlDelete annotation, and the CascadeType.all / orphanRemoval = true options on the #OneToMany side as follow:
Parent
#Entity
#EntityListeners(AuditingEntityListener.class)
#SQLDelete(sql = "update Discount set deleted = true where id = ?")
#Where(clause = "deleted = false")
public class Discount extends BaseDao {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
/// More properties ...
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "discountId", nullable = false)
private List<PetrolStationDiscount> petrolStationDiscounts = new ArrayList<>();
public Discount() {}
// Getters and setters...
public List<PetrolStationDiscount> getPetrolStationDiscounts() {
return petrolStationDiscounts;
}
public void setPetrolStationDiscounts(List<PetrolStationDiscount> petrolStationDiscounts) {
this.petrolStationDiscounts = petrolStationDiscounts;
}
// HashCode & Equals methods
Child Entity
#Entity
#EntityListeners(AuditingEntityListener.class)
#SQLDelete(sql="UPDATE PetrolStationDiscount SET deleted = true WHERE id = ?")
public class PetrolStationDiscount extends BaseDao {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
#ManyToOne
#JoinColumn(name = "petrolStationId")
private PetrolStation petrolStation;
#Column(insertable = false, updatable = false)
private String discountId;
// Getters & Setters -- HashCode & Equals methods
BaseDAO
#MappedSuperclass
public abstract class BaseDao {
#CreationTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(nullable = false)
private Date createdAt;
#UpdateTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(nullable = false)
private Date updatedAt;
#Column(name = "deleted", nullable = false)
private boolean deleted = false;
// Getters and Setters
}
This works fine, but I'm not comfortable mixing the sql statement straight into the entities, so I've tried the approach I found in the following thread:
How to soft delete parent and child together (cascade) using spring boot with jpa
Following the thread, I've created a BaseRepository overriding the CrudRepository methods "delete" and "deleteById", but it doesn't work:
When the parent entity is deleted, only the parent deleted field is saved as true.
When a child entity is no longer referenced by his parent, if orphanRemoval is set to true, the child entity is hard deleted, and if false, nothing happened.
#NoRepositoryBean
public interface BaseRepository<T extends BaseDao> extends CrudRepository<T, Serializable> {
#Override
#Query("update #{#entityName} e set e.deleted = true where e.id = ?1")
#Transactional
#Modifying
void deleteById(Serializable id);
#Override
#Transactional
default void delete(T entity) {
deleteById(entity.getId());
}
¿Someone could help me?
A lot of thanks!

Related

Serializing stored json object gives null pointer hibernate vladmihalcea / hibernate-types

I'm saving a java object as json in my db. For this implementation when I save it, it works fine and I can find the new row in my db. However whenever I try to fetch the same object I stored, I get this error:
java.lang.NullPointerException: null
at java.lang.Class.isAssignableFrom(Native Method)
at com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor.fromString(JsonTypeDescriptor.java:104)
at com.vladmihalcea.hibernate.type.json.internal.JsonTypeDescriptor.wrap(JsonTypeDescriptor.java:165)
at com.vladmihalcea.hibernate.type.json.internal.AbstractJsonSqlTypeDescriptor$1.doExtract(AbstractJsonSqlTypeDescriptor.java:34)
at org.hibernate.type.descriptor.sql.BasicExtractor.extract(BasicExtractor.java:47)...
My hibernate versions are:
Hibernate Core {5.3.7.Final}
Hibernate Commons Annotations {5.0.4.Final}
And I'm using this library to serialize my java class to json.
com.vladmihalcea:hibernate-types-52:2.17.3
What is weird is that I can save entities to the database (can deserialize) but it cannot build the object back when I try to get it.
#Getter
#Setter
#TypeDefs({
#TypeDef(name = "json", typeClass = JsonType.class)
})
public class NotificationMessage implements Serializable {
private Long id;
private String name = "";
private String type;
}
#Entity
#Table(name = "notification")
#Getter
#Setter
#EntityListeners(AuditingEntityListener.class)
public class Notification {
#Id
#Column(name = "notification_id")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_notification_sequence")
#SequenceGenerator(name = "id_notification_sequence", sequenceName = "id_notification_sequence", allocationSize = 1)
private Long id;
#Column(name = "type", nullable = false)
#Enumerated(EnumType.STRING)
#NotNull
private NotificationType type;
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_uuid", referencedColumnName = "user_uuid")
private User user;
#Type(type = "json")
#Column(name = "message", columnDefinition = "json")
#NotNull
private NotificationMessage message;
#Column(name = "acknowledged")
private boolean acknowledged = false;
#Column(name = "created_date", nullable = false, updatable = false)
#CreatedDate
private LocalDateTime createdDate;
#Column(name = "modified_date")
#LastModifiedDate
private LocalDateTime modifiedDate;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Notification that = (Notification) o;
return id.equals(that.id);
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
This is the query I'm running to get the notification back:
#Repository
public interface NotificationRepository extends JpaRepository<Notification, Long>, JpaSpecificationExecutor<Notification> {
#Query("SELECT n FROM Notification n WHERE n.type = :type AND n.acknowledged = false")
List<Notification> findAllByTypeAndAcknowledgedIsFalse(#Param("type") NotificationType type);
}
You must also declare the Hibernate Type to the entity class using the #TypeDef annotation like this:
#Entity
#Table(name = "notification")
#Getter
#Setter
#EntityListeners(AuditingEntityListener.class)
#TypeDef(name = "json", typeClass = JsonType.class)
public class Notification {
I had the same issue as you, and was stuck on it full day, really frustrating. But now I have fixed it. My fixes are:
Match the versions. I was using Hibernate 5.1 and hypersistence-utils-hibernate-55. However, according to the README in the repo, I should use io.hypersistence: hypersistence-utils-hibernate-5: 3.0.1 with Hibernate 5.1. However, in your case, I would try to unify the Hibernate versions first, and then choose the matching version for hypersistence-utils.
Use the correct annotation. In my Entity, I was annotating the field #Type(type = "jsonb"). After changing it to #Type(type = "json"). It started to work.
Hopefully, this can help you.

JPA soft delete with #SqlDelete causes SQL error: Parameter index out of range (2 > number of parameters, which is 1)

I have Basket and BasketItem entities that both extend BaseEntity as below:
#MappedSuperclass
public class BaseEntity {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "deleted", nullable = false)
private Boolean deleted = false;
#Version #Column(nullable = false)
private Long version;
}
#Entity
#Where(clause = "deleted = false")
#SQLDelete(sql = "UPDATE basket SET deleted=true WHERE id=?")
public class Basket extends BaseEntity {
#OneToMany(mappedBy = "basket", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<BasketItem> items = new ArrayList<>();
}
#Entity
#Where(clause = "deleted = false")
#SQLDelete(sql = "UPDATE basketItem SET deleted=true WHERE id=?")
public class BasketItem extends BaseEntity {
#ManyToOne(optional = false)
#JoinColumn(name = "basket_id", nullable = false)
private Basket basket;
}
Now when I want to delete a BasketItem from a Basket, I just call remove(basketItem) on basket items list and save the Basket instance:
#Service
public class DeleteBasketItemUseCase {
#Transactional
public BasketOutput execute(final Long itemId, final Long basketId) {
// loading and checks omitted
basket.getItems().remove(basketItem);
basket = basketRepository.save(basket);
}
}
Deleting basketItem generates MySQL error:
java.sql.SQLException: Parameter index out of range (2 > number of parameters, which is 1)
Generated SQL in log is:
UPDATE
basketItem
SET
deleted=true
WHERE
id = ?
I am using
Spring Boot 2.2.6.RELEASE
MySQL 8.0
Hibernate 5.4.12.Final
mysql-connector-java-8.0.19
I update #SQLDelete and add and version=? AT the end of annotation:
#SQLDelete(sql = "UPDATE basket SET deleted=true WHERE id=? and version=?")
#SQLDelete(sql = "UPDATE basket_item SET deleted=true WHERE id=? and version=?")
Note: Also I've changed database tables names!

JPA mapping for one-to-many collection of shared data, with user specific values

I have a User model that contains a list of achievements
#Table(name = "user")
#Entity
#NamedEntityGraph(name = "User.achievements",
attributeNodes={
#NamedAttributeNode("achievements")
})
#Data
public class User {
#Id
#NotNull
#Column(name = "username")
private String username;
#Column(name = "password")
private String password;
#ElementCollection(fetch = FetchType.LAZY, targetClass = Achievement.class)
private List<Achievement> achievements = new ArrayList<>();
}
Here's the achievement model
#Entity
#Data
#Table(name = "achievement")
public class Achievement {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid")
private String achievementId;
#Column(name = "title")
private String title;
#Column(name = "description")
private String description;
#Column(name = "achieved", columnDefinition="BOOLEAN DEFAULT false", nullable = false)
private boolean achieved = false;
user_achievements table generated from #ElementCollection mapping, which atm only contains user and achievement foreign keys
I am looking to move the boolean achieved value to the user_achievements table, ideally without having to create a separate model User_Achievements
I am fairly new to using Jpa, but i feel like this scenario is too basic so there must be a straight forward way to do that i cant seem to locate it
#Entity
class UserAchievement {
#EmbeddableId
UserAchievementId id;
#ManyToOne(fetch=LAZY)
#JoinColumn(name="user_username", insertable=false, updatable=false)
User user;
#ManyToOne(fetch=LAZY)
#JoinColumn(name="achivement_achivement_id", insertable=false, updatable=false)
Achivement achivement;
// and other fields
}
class User {
// ...
#OneToMany(mappedBy="user")
List<UserAchievement> userAchievements;
}
and you need to define UserAchievementId

Can I use #Where annotation along with #ManytoOne association?

EER Diagram
I am not an expert in Spring, JPA, Hibernate or MySql.
However I am using all for a web service supporting RESTful calls.
I am building a store management app backend with Spring.
My entities at this point of time are StoreModel, StoreUserModel, StoreUserRoleModel and StoreUserAuthModel.
I have setup bidirectional relationships(OneToMany and ManyToOne) between
StoreModel - StoreUserAuthModel,
StoreUserMode - StoreUserAuthModel and
StoreUserRoleMode - StoreUserAuthModel.
I dont want the foreign key constraint though there are foreign key fields storeid, roleid and userid in StoreUserAuthModel.
Now All the four tables have isdeleted column to implement soft delete.
I am lazy fetching the associations. However I dont want the softdeleted values whenever i query the associations.
I would like to know if I can use #Where annotation along with the #ManyToOne annotation in the StoreUserAuthModel entity?
The issue is different from How to use #Where in Hibernate because my problem is with ManyToOne annotation whereas I have used the where annotation with OneToMany
#Entity
#Table(name = "store")
public class StoreModel {
#NotBlank
private String name;
#NotBlank
private String address;
#NotBlank
private String city;
#NotBlank
private String phone;
#JsonIgnore
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "storeid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ))
#Where(clause="isdeleted = 0")
private List<StoreUserAuthModel> authList = new ArrayList<StoreUserAuthModel>();
...
}
#Entity
#Table(name = "storerole")
public class StoreRoleModel {
#NotBlank
private String name;
#NotBlank
private Integer rolehierarchy;
#JsonIgnore
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "roleid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ))
#Where(clause="isdeleted = 0")
private List<StoreUserAuthModel> authList = new ArrayList<StoreUserAuthModel>();
...
}
#Entity
#Table(name = "storeuser")
public class StoreUserModel{
#NotBlank
#Column(unique = true)
private String username;
#Email
#Column(unique = true)
private String useremail;
#JsonIgnore
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "userid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ))
#Where(clause="isdeleted = 0")
List<StoreUserAuthModel> userAuthList = new ArrayList<StoreUserAuthModel>();
...
}
#Entity
#Table(name = "storeuserauth",
uniqueConstraints = #UniqueConstraint(columnNames = {"storeid", "roleid", "userid"}))
public class StoreUserAuthModel {
#NotNull
Long storeid;
#NotNull
Long roleid;
#NotNull
Long userid;
// Using #where to filter out the soft deleted storeuser
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="userid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ),insertable = false, updatable = false )
#Where(clause="isdeleted = 0")
private StoreUserModel storeuser;
// Using #where to filter out the soft deleted store
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="storeid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ),insertable = false, updatable = false )
#Where(clause="isdeleted = 0")
private StoreModel store;
// Using #where to filter out the soft deleted role
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="roleid", foreignKey = #ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT ),insertable = false, updatable = false )
#Where(clause="isdeleted = 0")
private StoreRoleModel role;
...
}
// In the controller, Following code shows how I plan to use
Optional<StoreUserModel> aUser = storeUserRepository.findByUseremailAndIsdeleted(zUserMail), 0);
if(aUser.isPresent()) {
// The user was found!!!
// Testing...
// Getting the User Auth List (that will filter out the soft deleted auths)
List<StoreUserAuthModel> authList = aUser.get().getUserAuthList();
for(StoreUserAuthModel auth :authList) {
StoreModel store = auth.getStore();
// here both soft deleted store as well as normal stores are shown.
// ie where clause on store relation is not working!!
logger.debug("Store is "+store.getName());
}
}
...
Now all the store rows matching the id are in the list.
The expected result should apply where clause too
I turned on logging for hibernate 5.3.9
There is no where clause when it fires the select query
The #Where annotation has no effect on ToOne relationships. But instead of adding #Where to the reference you can use #Where on the Entity:
#Where(clause="isdeleted = 0")
#Entity
#Table(name = "storerole")
public class StoreRoleModel {
That way no deleted entities of StoreRoleModel will be loaded by Hibernate.

JPA many to many relation not inserting into generated table

I have a many-to-many relation in my project and although I'm able to write in my two Entities table, the relational table does not get anything written.
Here's how I'm declaring this using JPA annotations:
Professor.java
#Entity
#Table(name = "Professor")
public class Professor implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "idProfessor", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "ALUNO_PROFESSOR",
joinColumns = #JoinColumn(name = "idProfessor", referencedColumnName = "idProfessor"),
inverseJoinColumns = #JoinColumn(name = "idAluno", referencedColumnName = "idAluno"))
private List<Aluno> alunoList;
// getters and setters
}
Aluno.java
#Entity
#Table(name = "Aluno")
public class Aluno implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "idAluno", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToMany(mappedBy = "alunoList", fetch = FetchType.EAGER)
private List<Professor> professorList;
// getters and setters
}
And here is the service layer to insert into database:
#Autowired
private AlunoDao alunoDao;
#Autowired
private ProfessorDao professorDao;
#RequestMapping(value = RestUriConstants.SUBMETER, method = RequestMethod.POST)
public #ResponseBody JsonResponse submeter(#RequestBody final Aluno aluno) {
Professor professor = professorDao.find(1);
aluno.setProfessorList(Arrays.asList(professor));
alunoDao.persist(aluno);
...
}
In this case, please consider that I already have an entry with id "1" for Professor.
As I said, it does write on Aluno and Professor table but does NOT write anything into ALUNO_PROFESSOR table.
I've already taken a look at these three kind of similiar questions but none of them could help me:
Hibernate and Spring: value of many-to-many not inserted into generated table
JPA many-to-many persist to join table
How to persist #ManyToMany relation - duplicate entry or detached entity
EDIT - Adding more code snippets
JpaAlunoDao.java
#Repository
public class JpaAlunoDao implements AlunoDao {
#PersistenceContext
private EntityManager em;
#Transactional
public void persist(Aluno aluno) {
em.persist(aluno);
}
}
JpaExercicioDao.java
#Repository
public class JpaExercicioDao implements ExercicioDao {
#PersistenceContext
private EntityManager em;
#Transactional
public void persist(Exercicio exercicio) {
em.persist(exercicio);
}
}
Try this:
public class Professor {
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "ALUNO_PROFESSOR",
joinColumns = #JoinColumn(name = "idProfessor", referencedColumnName = "idProfessor"),
inverseJoinColumns = #JoinColumn(name = "idAluno", referencedColumnName = "idAluno"))
private List<Aluno> alunoList;
}
public class Aluno {
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "ALUNO_PROFESSOR",
joinColumns = #JoinColumn(name = "idAluno", referencedColumnName = "idAluno"),
inverseJoinColumns = #JoinColumn(name = "idProfessor", referencedColumnName = "idProfessor"))
private List<Professor> professorList;
}
This will ensure that the metadata for the many-to-many relationship is available on both the entities and that operations on either side of the relationship are cascaded to the other side.
I also suggest replacing FetchType.EAGER with FetchType.LAZY for better performance because this has the potential of loading a very large dataset.
I have the same issue. I swapped where the full mapping declare to the class that we will use save() function on.
In your case:
public class Aluno {
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "ALUNO_PROFESSOR",
joinColumns = #JoinColumn(name = "idAluno"),
inverseJoinColumns = #JoinColumn(name = "idProfessor")
private List<Professor> professorList;
}
public class Professor {
#ManyToMany(fetch = FetchType.EAGER, mappedBy = "professorList",)
private List<Aluno> alunoList;
}
and It worked fine.
...
Normally, Hibernate holds the persistable state in memory. The process of synchronizing this state to the underlying DB is called flushing.
When we use the save() method, the data associated with the save operation will not be flushed to the DB unless and until an explicit call to flush() or commit() method is made.
If we use JPA implementations like Hibernate, then that specific implementation will be managing the flush and commit operations.
One thing we have to keep in mind here is that, if we decide to flush the data by ourselves without committing it, then the changes won't be visible to the outside transaction unless a commit call is made in this transaction or the isolation level of the outside transaction is READ_UNCOMMITTED.
...
From Difference Between save() and saveAndFlush() in Spring Data JPA by baeldung:
https://www.baeldung.com/spring-data-jpa-save-saveandflush
employeeRepository.saveAndFlush(new Employee(2L, "Alice"));
or
employeeRepository.save(new Employee(2L, "Alice"));
employeeRepository.flush();
Its not necessary to set many-to-many relationship on both entities.
Just remove session.setFlushMode(FlushMode.MANUAL);
By default HibernateTemplate inside of Spring set FlushMode.MANUAL
this is source code from HibernateTemplate.
if (session == null) {
session = this.sessionFactory.openSession();
session.setFlushMode(FlushMode.MANUAL);
isNew = true;
}
Some times the problem is in the way that you insert the values. I explain with an example.
User user = userFacade.find(1);
Post post = new Post("PRUEBA");
user.addPostCollection(post);
post.addUserCollection(user);
postFacade.create(post);
You have to add the post in postCollection and the user in userCollection. You have two add the correspond entity in the collections of the two entities.
Class USER
public void addPostCollection(Post post) {
if(postCollection == null){
postCollection = new ArrayList<Post>();
}
postCollection.add(post);
}
#ManyToMany(mappedBy = "userCollection")
private Collection<Post> postCollection;
Class Post
public void addUserCollection(User user){
if(userCollection == null){
userCollection = new ArrayList<User>();
}
userCollection.add(user);
}
#JoinTable(name = "USER_POST_R", joinColumns = {
#JoinColumn(name = "POSTID", referencedColumnName = "ID")}, inverseJoinColumns = {
#JoinColumn(name = "USERID", referencedColumnName = "ID")})
#ManyToMany
private Collection<User> userCollection;
Also, it is important to instance the list, for example userCollection = new ArrayList(). If you do not, the value won´t insert.