Set::removeIf with true predicate does not delete elements - java

I have a quite simple problem, yet a very weird behaviour on which a Set does not delete elements on true predicates for some reason.
The entities (for reference only, as this does not have anything to do with the set item - or shouldn't):
#Data
#MappedSuperclass
#EqualsAndHashCode(of = "uuid")
public abstract class Model<ID> {
#Id
#GeneratedValue
protected ID id;
protected UUID uuid; // Hybrid model
#PrePersist
private void onPersisting() {
uuid = UUID.randomUUID();
}
}
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, of = "name")
#Table(uniqueConstraints = #UniqueConstraint(name = "scope_name", columnNames = "name"))
public class Scope extends Model<Long> {
private String name;
public Scope(UUID uuid) {
this.uuid = uuid;
}
public Scope(String name) {
this.name = name;
}
}
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, of = "name")
#Table(uniqueConstraints = #UniqueConstraint(name = "role_name", columnNames = "name"))
public class Role extends Model<Long> {
private String name;
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "role_scopes", joinColumns = #JoinColumn(name = "role_id"), inverseJoinColumns = #JoinColumn(name = "scope_id"))
private Set<Scope> scopes = new HashSet<>();
public Role(UUID uuid, Scope... scopes) {
this.scopes = Stream.of(scopes).collect(Collectors.toSet());
this.uuid = uuid;
}
public Role(String name, Scope... scopes) {
this.scopes = Stream.of(scopes).collect(Collectors.toSet());
this.name = name;
}
}
The following snippet, called within a JUnit test case, does not delete the set elements (simplified for readability):
#Transactional
public Role create(Role role) {
role.getScopes().removeIf(unused -> true); // <----
return role;
}
For some reason, that snippet does work:
Set<String> strings = new HashSet<>();
strings.add("FOO");
strings.add("BAR");
strings.removeIf(unused -> true);
What's going on here?

It is because of overridden equals and hashCode that is done by #Data annotation on your Scope class.
If you remove it or replace with #Getter #Setter it will work

Related

Mention field = null in child comments but works fine for root level comments

I want to make childComments nested in root comments. I did it. But the problem is that mention field = null in childComments but other fields are displayed correctly. However, for root comments, mention field works well.
I also want to have only two levels of comments.
Root comments -> child comments. And child comments shoul'd have childComments.
Perhaps I did not register the mapping correctly. I will be glad to hear your advice.
#Override
public CommentListResponse list(Specification<CommentEntity> spec, Pageable pageable, DeletedRecords deleted) {
pageable = DefaultSorting.apply(pageable, Sort.by(Sort.Order.desc("commentedAt")));
spec = SpecificationHelper.addDeletedFilter(spec, deleted);
Page<CommentEntity> page = commentRepository.findAll(spec, pageable);
List<CommentListResponse.CommentItem> comments = page.stream()
.filter(comment -> comment.getParentCommentId() == null)
.map(comment -> commentMapper.toListResponseItem(comment, deleted != null))
.toList();
return CommentListResponse.builder()
.data(comments)
.fromPage(page)
.build();
}
#Data
#SuperBuilder
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode(callSuper = true)
public class CommentListResponse extends PagedResponse {
private List<CommentItem> data;
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public static class CommentItem {
private long id;
private String body;
private List<String> mentions;
private Long parentCommentId;
private List<CommentItem> childComments;
#JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean deleted;
#JsonIgnore
private boolean includeDeletedField;
public Boolean getDeleted() {
return includeDeletedField ? deleted : null;
}
}
}
#Mapper(componentModel = "spring")
public interface CommentMapper {
#Mapping(source = "comment.mentionUsers", target = "mentions")
CommentListResponse.CommentItem toListResponseItem(CommentEntity comment, boolean includeDeletedField);
}
#Entity
#Table(name = "comment")
#Getter
#Setter
#Builder
#AllArgsConstructor
#NoArgsConstructor
#ToString(onlyExplicitlyIncluded = true)
public class CommentEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
#ToString.Include
private Long id;
#Column(name = "body", nullable = false)
#ToString.Include
private String body;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "comment_user_mention",
joinColumns = #JoinColumn(name = "comment_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private List<UserEntity> mentionUsers;
#Column(name = "parent_comment_id")
#ToString.Include
private Long parentCommentId;
#OneToMany(mappedBy = "parentCommentId", fetch = FetchType.LAZY)
private List<CommentEntity> childComments;
}
JSON output: { "id": 1,
"body": "test root comment",
"mentions": ["user-1"],
"parentCommentId": null,
"childComments": [
{
"id": 3,
"body": "test child comment",
"mentions": null, - // in db i have mention for this child comment but it's null.
"parentCommentId": 1,
"childComments": [] // i don't want to see this field in child comments.
}
]
}
Thats most probably because you didn't provide a method that maps List<UserEntity> to List<String>.
Just add such a default method and test if it works.

JPA Entity Lifecycle Events #PostUpdate transaction commit occurs at different time

I have three entities in my application one parameter of which depends on the value of Building entity. Parameter type is dependent on the value of parameter endDate of Building entity. CustomType is an entity stored in another schema of the db.
I understand that this usage of JPA callback is probably breaking conventions and we already have the idea how to implement this other way, but my curiosity won't let me drop this case: what is the cause of different behaviour of Hibernate in those two situations?
I decided to update parameters of Stage and Project via JPA lifecycle event and implemented #PrePersist, #PostUpdate and #PostPersist listeners. This is when I encountered the problem: we are using JSON API to save our entities without explicitly writing code to do so. But for the operation of soft deletion we are using custom RestController, which is saving entity via JPA Repository save() method. When JPA Events are triggered by JSON API everything works correctly - CustomType is changed in all of the entities and persisted to the database. When we are deleting entity and causing changes in type - #PostUpdate callback is working in unexpected way. It seems to persist changes only in Building entity, completely ignoring Stage and Project. After checking via EntityManager.contains() - they are in managed state in the method that is inside of JPA #PostUpdate event, so I expected same behaviour - persist changes without explicitly calling to save() method.
Building.java
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "BUILDING", schema = "---")
#EntityListeners(BuildingListener.class)
#Where(clause = "DELETED = 0")
public class Building extends BaseEntity {
String name;
String description;
Date endDate;
Project project;
Stage stage;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#Column(name = "END_DATE")
public Date getName() {
return name;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "PROJECT_ID")
public Project getProject() {
return project;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "STAGE_ID")
public Stage getStage() {
return stage;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
Project.java
#Data
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "PROJECT", schema = "---")
#Where(clause = "DELETED = 0")
public class Project extends BaseEntity {
String name;
String description;
List<Stage> stages;
List<Building> buildings;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#OneToMany(fetch = LAZY)
public List<Stage> getStages() {
return stages;
}
#OneToMany(fetch = LAZY)
public List<Building> getBuildings() {
return buildings;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
Stage.java
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "PROJECT", schema = "---")
#Where(clause = "DELETED = 0")
public class Stage extends BaseEntity {
String name;
String description;
Project project;
List<Building> buildings;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "PROJECT_ID")
public Project getProject() {
return project;
}
#OneToMany(fetch = LAZY)
public List<Building> getBuildings() {
return buildings;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
BuildingListener.java
#PrePersist
public void prePersist(Building building){
listenerService.checkTypeForNewBuilding(building);
}
#PreUpdate
public void prePersist(Building building){
listenerService.checkTypeForExistingBuilding(building);
}
#PostUpdate
#PostPersist
public void post(Building building){
listenerService.resolveTypesForRelations(building);
}
}
ListenerService.java
public void checkTypeForNewBuilding(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
if (/*logic*/){
building.setType(type1);
} else{
building.setType(type2);
}
}
public void checkTypeForExistingBuilding(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
if (/*logic*/){
building.setType(type1);
} else{
building.setType(type2);
}
}
public void resolveTypesForRelations(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
Project project = building.getProject();
Stage stage = building.getStage();
if (/*logic*/){
project.setType(type1);
stage.setType(type1);
} else{
project.setType(type2);
stage.setType(type2);
}
}

JPA (Hibernate) - ManyToMany - setter

I am not sure if I am doing something wrong, but when I have these two entities:
#Setter #Getter #NoArgsConstructor #FieldDefaults(level = AccessLevel.PRIVATE)
#Entity
#Table(name = "GROUP_OF_USERS")
public class Group {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
Long id;
String name;
#ManyToOne
User administrator;
#ManyToMany(mappedBy = "groups", cascade = CascadeType.ALL)
Set<User> users = new HashSet<>();
public void setAdministrator(User user){
this.administrator = user;
addUser(user);
}
public void addUser(User user){
users.add(user);
}
}
#Entity
#Setter #Getter #NoArgsConstructor #FieldDefaults(level = AccessLevel.PRIVATE)
#Table(name = "APP_USER")
public class User {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
Long id;
String name;
#Column(unique = true)
String email;
String password;
String bankAccount;
boolean isActive = false;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(
name = "GROUP_USER_RELATIONSHIP",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "group_id"))
Set<Group> groups = new HashSet<>();
public void addGroup(Group group){
groups.add(group);
}
}
and service method like this:
#Override
public Group crateGroup(String userName, String groupName) {
var userInDb = userRepository.findUserByEmailIgnoreCase(userName).orElseThrow(() -> new UserNotFoundException(userName));
userInDb.getGroups().stream()
.filter(group -> group.getName().equals(groupName))
.findAny()
.map(Group::getName)
.ifPresent(name -> {throw new GroupUniqueConstrainException(name);});
var newGroup = new Group();
newGroup.setName(groupName);
newGroup.setAdministrator(userInDb);
return groupRepository.save(newGroup);
}
than I am surprised that administrator is set in DB, but join table doesn't contain any record. Could someone help me here to understand where could be a problem? I found out that I could update the addUser method so it would also add group to the user. But I would need to do this also for addGroup method in User class so when calling that it would create infinite loop.

ManyToMany relationship in "hibernate" does not insert values into the join table when initializing the application

I got a lot of answers and advice, tried changing my code according to these tips.
But, unfortunately, these tips helped only partially.
Now, when creating a new project and creating a new user, I can add the desired user to the set of projects, and the required project will be added to the set of users.
But the relationship between the desired project and the desired user will not appear in the project_user table.
Please help find the answer.
Entity Project
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "project")
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String name;
#Column
private String description;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_users",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "users_id"))
private Set<User> projectUserSet = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_task",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "task_id"))
private Set<Task> projectTaskSet = new HashSet<>();
public void addUserToProject(User user){
this.projectUserSet.add(user);
user.getUserProjectsSet().add(this);
}
public void addTasksToProject(Task task){
this.projectTaskSet.add(task);
task.getTasksProjectSet().add(this);
}
//constructors, hashCode, equals, toString
}
Entity User
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String firstName;
#Column
private String lastName;
#ManyToMany(mappedBy = "projectUserSet", cascade = CascadeType.ALL)
private Set<Project> userProjectsSet = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "users_task",
joinColumns = {#JoinColumn(name = "users_id")},
inverseJoinColumns = {#JoinColumn(name = "task_id")})
private Set<Task> userTasksSet = new HashSet<>();
public void addTaskToUser(Task task) {
this.userTasksSet.add(task);
task.getTasksUserSet().add(this);
}
//constructors, hashCode, equals, toString
}
project and user initialization
Project project1 = new Project("Project1", "Project1");
User user1 = new User("User1", "User1");
project1.addUserToProject(user1);
With code shown below, table project_user is populated, verified using H2 console. In order to avoid stack overflow, I had to modify method Project#addTaskToUser as shown below.
Please note that only code relevant to the question, is included.
Normally, issue should be described by some tests. In this case, I added a CommandLineRunner that runs at startup.
CascadeType.ALL is not recommended for many-to-many relations, hence I changed this in code shown below.
Tested using H2 in-memory db.
Project class
#Data
#Entity
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name = "project_users",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "users_id"))
private Set<MyUser> projectUserSet = new HashSet<>();
public void addUserToProject(User user) {
this.projectUserSet.add(user);
}
}
Project repo
public interface ProjectRepo extends JpaRepository<Project, Long> { }
User class
// cannot use #Data here because it will cause cyclic ref and stack overflow when accessing userProjectsSet
#Getter
#Setter
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#ManyToMany(mappedBy="projectUserSet")
private Set<Project> userProjectsSet = new HashSet<>();
}
User repo
public interface UserRepo extends JpaRepository<User, Long> {}
In app class
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
#Bean
CommandLineRunner run(ProjectRepo projectRepo, EntityManagerFactory entityManagerFactory) {
return args -> {
var testUser = new User();
testUser.setFirstName("first-name");
testUser.setLastName("last-name");
var project = new Project();
project.setName("project-name");
project.setDescription("project-description");
project.addUserToProject(testUser);
projectRepo.save(project);
// get saved user and print some properties
var userInDb = entityManagerFactory.createEntityManager().find(User.class, testUser.getId());
System.out.println(userInDb.getFirstName()); // prints "first-name"
System.out.println(userInDb.getUserProjectsSet().size()); // prints "1"
};
}
}

Hibernate Map ID automatically from field

I have something similar to this:
#Entity
#Table(name = "claim", schema = "test")
public class Claim implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Integer idClaim;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JsonManagedReference
private ClaimReturnInfo claimReturnInfo;
#Column(name = "notes")
private String notes;
// Getters and setters
}
#Entity
#Table(name = "claim_returninfo", schema = "test")
public class ClaimReturnInfo implements Serializable {
#Id
#Column(name = "Claim_idClaim")
private Integer id;
#MapsId("Claim_idClaim")
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
#JsonBackReference
private Claim claim;
#Column(name = "description")
private String description;
// Getters and setters
}
ClaimReturnInfo Id is not autogenerated because we want to propagate the Id from its parent (Claim). We are not able to do this automatically and we are getting this error: ids for this class must be manually assigned before calling save() when 'cascade' is executed in ClaimReturnInfo .
Is it possible to map Claim Id into ClaimReturnInfo Id or should we do this manually?
Even if we set this ID manually on claimReturnInfo and we can perform updates, we still get this error when trying to create a new Claim:
// POST -> claimRepository.save() -> Error
{
"notes": "Some test notes on a new claim",
"claimReturnInfo": {
"description": "Test description for a new claimReturnInfo"
}
}
In the ServiceImplemetation:
#Override
#Transactional
public Claim save(Claim claim) throws Exception {
if(null != claim.getClaimReturnInfo()) {
claim.getClaimReturnInfo().setId(claim.getIdClaim());
}
Claim claimSaved = claimRepository.save(claim);
return claimSaved;
}
I have tried using the following mappings and from your comments it was apparent that Json object is populated correctly.
I have noticed that the annotation #MapsId is the culprit.If you check the documentation of #MapsId annotation it says
Blockquote
The name of the attribute within the composite key
* to which the relationship attribute corresponds. If not
* supplied, the relationship maps the entity's primary
* key
Blockquote
If you change #MapsId("Claim_idClaim") to #MapsId it will start persisting your entities.
import javax.persistence.*;
#Entity
#Table(name = "CLAIM")
public class Claim {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Long idClaim;
#Column(name = "notes")
private String notes;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, optional = false)
private ClaimReturnInfo claimReturnInfo;
public Long getIdClaim() {
return idClaim;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public ClaimReturnInfo getClaimReturnInfo() {
return claimReturnInfo;
}
public void setClaimReturnInfo(ClaimReturnInfo claimReturnInfo) {
if (claimReturnInfo == null) {
if (this.claimReturnInfo != null) {
this.claimReturnInfo.setClaim(null);
}
} else {
claimReturnInfo.setClaim(this);
}
this.claimReturnInfo = claimReturnInfo;
}
}
package com.hiber.hiberelations;
import javax.persistence.*;
#Entity
#Table(name = "CLAIM_RETURN_INFO")
public class ClaimReturnInfo {
#Id
#Column(name = "Claim_idClaim")
private Long childId;
#Column(name = "DESCRIPTION")
private String description;
#MapsId
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
private Claim claim;
public Long getChildId() {
return childId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Claim getClaim() {
return this.claim;
}
public void setClaim(Claim claim) {
this.claim = claim;
}
}

Categories