JPA and Lombok causing StackOverflowError in circular dependency - java

I have two entity classes:
#Data
#Entity(name = "user")
#Table(name = "tbl_user")
#EqualsAndHashCode(callSuper = true, exclude = "products")
public class UserEntity extends BaseEntity {
#Column(unique = true)
private String username;
#ToString.Exclude
private String password;
#Transient
#JsonProperty("remember_me")
private Boolean rememberMe;
#Enumerated(EnumType.STRING)
#CollectionTable(name = "tbl_user_role")
#ElementCollection(fetch = FetchType.EAGER)
Set<Role> roles = new HashSet<>();
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<ProductEntity> products = new ArrayList<>();
}
#Data
#Entity
#Table(name = "tbl_product")
#EqualsAndHashCode(callSuper = true)
public class ProductEntity extends BaseEntity {
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private UserEntity user;
private String productName;
}
both extending a baseEntity:
#MappedSuperclass
public abstract class BaseEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Version
private long version;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseEntity that = (BaseEntity) o;
return Objects.equals(id, that.id) && Objects.equals(version, that.version);
}
#Override
public int hashCode() {
return Objects.hash(id, version);
}
}
Now when I try to retrieve all products (or all users) (e.g. findAll method) I get a StackOverflowError.
I know this error is caused by the circular dependency between user and products, so I added an exclude to the equals annotation in the userEntity to resolve it, like so: #EqualsAndHashCode(callSuper = true, exclude = "products")
Unfortunately the error keeps popping up. What am I missing here?

In case it might help, lombok also has anotation #EqualsAndHashCode.Exclude very useful for tests!

I managed to fix the issue by adding #ToString.Exclude to the roles and user class variables. Apparently I was printing them somewhere later in the code, which was causing the error.

Related

Deleting an entity with one to one relation

My two entities have one to one relation
#Getter
#Setter
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
#Table(uniqueConstraints = #UniqueConstraint(columnNames = "email"), name = "library_user")
public class AppUser {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
#EqualsAndHashCode.Exclude
private Long id;
// other fields
#OneToOne(mappedBy="user", cascade={CascadeType.REMOVE,CascadeType.PERSIST}, orphanRemoval = true)
private PasswordResetToken token;
// getters/setters and equals/hashcode
}
#Getter
#Setter
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
#Table(name = "password_reset_token")
public class PasswordResetToken {
private static final int EXPIRATION = 60 * 24;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// other fields
#OneToOne(targetEntity = AppUser.class, fetch = FetchType.EAGER, cascade={CascadeType.REMOVE,CascadeType.PERSIST}, orphanRemoval = true)
#JoinColumn(nullable = false, name = "user_id")
private AppUser user;
// getters/setters and equals/hashcode
I tried to delete my user entity by this method
public void deleteUser(Long id) {
resetTokenRepository.deleteAllByUserId(id);
userRepository.deleteById(id);
}
PasswordResetTokenRepository class which method I called in my service method, for deleting user I used regular hibernate method deleteById(Long id)
#Repository
public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, Long> {
void deleteAllByUserId(Long id);
}
But when I try to delete by this method I got this error:
not-null property references a null or transient value : kpi.diploma.ovcharenko.entity.user.PasswordResetToken.user
I read several websites how to delete one to one relation, but their advices didn't help me. For example, I tried a lot of variants of annotation cascade={CascadeType.ALL}, tried all the variants(CascadeType.REMOVE,CascadeType.PERSIST and so on), all time I got the same error. Help me pls, to understand what I do wrong.
try this:
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
Here is complete explication .

Hibernate nested entity Column 'id' cannot be null

I'm trying to persist a workout object that has a list of exercises which has a list of exerciseSets. The error i get is when hibernate tries to persist the exerciseSets to the database when i use CascadeType.ALL. When i use MERGE i don't get a error, but the ExerciseSet is not saved to the database. But the Workout and Exercise objects are.
What am i missing here? The only thing i can come up with is that i have to place the exercise object inside the exerciseSet object which kinda ends up in a loop of adding them to each other.
The objects are as follows:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Workout {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
Long workoutId;
#Column
int day;
#Column
String workoutName;
#OneToMany(cascade = CascadeType.ALL,
orphanRemoval = true)
#JoinColumn
List<Exercise> exercises;
#Column
String createDate;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class Exercise {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private Long exerciseId;
#ManyToOne(fetch = FetchType.LAZY)
private Workout workout;
#OneToMany(mappedBy = "exercise",
cascade = CascadeType.ALL)
private List<ExerciseSet> exerciseSets;
#OneToOne(cascade = CascadeType.MERGE)
#JoinTable(name = "var_exercise",
joinColumns =
{ #JoinColumn(name = "exercise_id")},
inverseJoinColumns =
{ #JoinColumn(name = "variation_id" )})
private ExerciseVariations exerciseVariations;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class ExerciseSet {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
private Long setId;
#Column
private int setPosition;
#Column
private int reps;
#Column
private int weight;
#ManyToOne
#JoinColumn(name="exerciseId", nullable = false)
private Exercise exercise;
}
I get a WorkoutDto from the frontend and map it to a entity with a mapper as follows
#PostMapping("/save")
public void saveWorkout(#RequestBody WorkoutDto workoutDto) {
workoutService.saveWorkout(workoutMapper.toWorkout(workoutDto));
}
public Workout toWorkout(WorkoutDto workoutDto) {
Workout workout = modelMapper.map(workoutDto, Workout.class);
if (workoutDto.getWorkoutId() != null) {
workout.setWorkoutId(workout.getWorkoutId());
}
workout.setWorkoutName(workoutDto.getWorkoutName());
workout.setDay(workoutDto.getDay());
List<Exercise> exerciseList = new ArrayList<>();
for (ExerciseDto exerciseDto: workoutDto.getExercisesDto()) {
List<ExerciseSet> exerciseSets = new ArrayList<>();
for (ExerciseSetDto exerciseSetDto : exerciseDto.getExerciseSetDtoList()) {
ExerciseSet exerciseSet = new ExerciseSet();
exerciseSet.setSetPosition(exerciseSetDto.getSetPosition());
exerciseSet.setReps(exerciseSetDto.getReps());
exerciseSet.setWeight(exerciseSetDto.getWeight());
exerciseSets.add(exerciseSet);
}
Exercise exercise = exerciseMapper.toExercise(exerciseDto);
exercise.setExerciseSets(exerciseSets);
exerciseList.add(exercise);
}
workout.setExercises(exerciseList);
workout.setCreateDate(workoutDto.getCreateDate());
return workout;
}
You don't set the Exercise in the ExerciseSet and this is the side that manages the relationship.
You code should look like this:
public Workout toWorkout(WorkoutDto workoutDto) {
Workout workout = modelMapper.map(workoutDto, Workout.class);
if (workoutDto.getWorkoutId() != null) {
workout.setWorkoutId(workout.getWorkoutId());
}
workout.setWorkoutName(workoutDto.getWorkoutName());
workout.setDay(workoutDto.getDay());
List<Exercise> exerciseList = new ArrayList<>();
for (ExerciseDto exerciseDto: workoutDto.getExercisesDto()) {
Exercise exercise = exerciseMapper.toExercise(exerciseDto);
List<ExerciseSet> exerciseSets = new ArrayList<>();
for (ExerciseSetDto exerciseSetDto : exerciseDto.getExerciseSetDtoList()) {
ExerciseSet exerciseSet = new ExerciseSet();
exerciseSet.setSetPosition(exerciseSetDto.getSetPosition());
exerciseSet.setReps(exerciseSetDto.getReps());
exerciseSet.setWeight(exerciseSetDto.getWeight());
exerciseSet.setExercise(excercise);
exerciseSets.add(exerciseSet);
}
exercise.setExerciseSets(exerciseSets);
exerciseList.add(exercise);
}
workout.setExercises(exerciseList);
workout.setCreateDate(workoutDto.getCreateDate());
return workout;
}
And then you can set the CascadeType to ALL.

Hibernate persists only first nested entity

My problem is that Hibernate does not persist nested entities given in entity.
Consider following entities:
PollEntity
#Table(name = "\"poll\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#EqualsAndHashCode(of = "id")
#Builder
public class PollEntity {
#Transient
public OptionEntity addOption(OptionEntity pollOptionEntity) {
if(options == null)
options = new HashSet<>();
options.add(pollOptionEntity);
pollOptionEntity.setPoll(this);
return pollOptionEntity;
}
#Transient
public OptionEntity dropOption(OptionEntity pollOptionEntity) {
options.remove(pollOptionEntity);
pollOptionEntity.setPoll(null);
return pollOptionEntity;
}
#Id
#Column(name = "id")
private UUID id;
#Column(name = "author")
#UUIDv4
private UUID author;
#Column(name = "poll_question")
#Size(max = 1000)
private String pollQuestion;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "poll", cascade = CascadeType.MERGE)
#Builder.Default
#Valid
private Set<OptionEntity> options;
}
OptionEntity
#Table(name = "\"option\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#EqualsAndHashCode(of = "id")
#Builder
public class OptionEntity {
#Id
#GeneratedValue(generator = "UUID")
#Column(name = "id")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
#JoinColumn(name = "poll_id")
#ManyToOne
private PollEntity poll;
#Column(name = "option")
#Size(max = 1000)
#NotNull
private String option;
}
And here's service method:
#Transactional(rollbackFor = Throwable.class)
public void createPoll(#Valid PollEntity pollEntity) throws ValidationException {
validationService.validateOrThrow(pollEntity);
if (pollRepository.findById(pollEntity.getId()).isPresent())
throw new ValidationException("Invalid id", Map.of("id", "Poll with id (" + pollEntity.getId() + ") already exists"));
pollEntity = validationService.validateAndSave(pollRepository, pollEntity);
And corresponding test:
#Test
public void createPollTest() throws ValidationException {
var uuid = UUID.randomUUID();
var pollOption1 = OptionEntity.builder()
.option("Test option 1")
.build();
var pollOption2 = OptionEntity.builder()
.option("Test option 2")
.build();
var pollOption3 = OptionEntity.builder()
.option("Test option 3")
.build();
var poll = PollEntity.builder()
.id(uuid)
.pollQuestion("Test question")
.author(UUID.randomUUID())
.build();
poll.addOption(pollOption1);
poll.addOption(pollOption2);
poll.addOption(pollOption3);
pollService.createPoll(poll);
}
Which gives following output in database
poll
2e565f50-7cd4-4fc9-98cd-49e0f0964487 feae5781-ff07-4a21-9292-c11c4f1a047d Test question
option
c786fe5d-632d-4e94-95ef-26ab2af633e7 fc712242-8e87-41d8-93f2-ff0931020a4a Test option 1
and rest options ended up unpersisted.
I've also used to create options in separate method
#Transactional(propagation= Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
public Set<OptionEntity> createOptions(#Valid Set<OptionEntity> pollOptionsEntities) throws ValidationException {
for (var pollOption : pollOptionsEntities) {
validationService.validateAndSave(pollOptionsRepository, pollOption);
}
return pollOptionsEntities;
}
and option entities were getting produced but had to switch to persisting from built-in entity methods due to errors with persisting poll entity.
Database schema looks like this:
CREATE TABLE "poll"
(
"id" UUID PRIMARY KEY,
"author" UUID NOT NULL,
"poll_question" VARCHAR(1000) NOT NULL
)
CREATE TABLE "option"
(
"id" UUID PRIMARY KEY,
"poll_id" UUID NOT NULL REFERENCES "poll" ("id") ON DELETE CASCADE
"option" VARCHAR(1000) NOT NULL
)
What are the possible approaches to try?
UPD 1
Having considered various looking-alike questions (1,2,3,4,5)
I've came up with this addition to entity which suppose make entities persistence regardless of actual value and still having only one option in output. What was done wrong?
#Override
public boolean equals(Object object) {
if ( object == this ) {
return false;
}
if ( object == null || object.getClass() != getClass() ) {
return false;
}
final OptionEntity other = OptionEntity.class.cast( object );
if ( getId() == null && other.getId() == null ) {
return false;
}
else {
return false;
}
}
#Override
public int hashCode() {
final HashCodeBuilder hcb = new HashCodeBuilder( 17, 37 );
if ( id == null ) {
while (getOptions().iterator().hasNext())
hcb.append( getOptions().iterator().next() );
}
else {
hcb.append( id );
}
hcb.append( options );
return hcb.toHashCode();
}
So the answer were quite trivial all the way long.
In order to overcome this the only thing that should be done is to change container: Set<OptionEntity> to List<OptionEntity>.
Hope this will not produce some hard-to-tackle bugs but if it can - please add comment.
Because in my case uniqueness was not strict requirement, ended up with this:
PollEntity:
#Table(name = "\"poll\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Builder
public class PollEntity {
#Transient
public OptionEntity addOption(OptionEntity pollOptionEntity) {
options.add(pollOptionEntity);
pollOptionEntity.setPoll(this);
return pollOptionEntity;
}
#Transient
public OptionEntity dropOption(OptionEntity pollOptionEntity) {
options.remove(pollOptionEntity);
pollOptionEntity.setPoll(null);
return pollOptionEntity;
}
#Id
#Column(name = "id")
private UUID id;
#Column(name = "status")
#Enumerated(EnumType.STRING)
#NotNull
private Status status;
#Column(name = "author")
#UUIDv4
private UUID author;
#Column(name = "poll_question")
#Size(max = 1000)
private String pollQuestion;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "poll", cascade = CascadeType.MERGE)
#Builder.Default
#Valid
private List<OptionEntity> options = new ArrayList<>();
}

Not able to delete in #OneToMany relationship spring data jpa

In my spring boot project, I have one LineItem entity below is the code
#Entity
#Table(name = "scenario_lineitem")
#Data
#NoArgsConstructor
public class LineItem implements Cloneable {
private static Logger logger = LoggerFactory.getLogger(GoogleConfigConstant.class);
#Id
#GeneratedValue(strategy = IDENTITY)
private BigInteger lineItemId;
#Column
private String name;
#OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE })
#JoinColumn(name = "line_item_meta_id")
private List<QuickPopValue> quickPopValues;
}
Another entity is
#Entity
#Table(name = "quick_pop_value")
#Data
#NoArgsConstructor
public class QuickPopValue implements Cloneable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "quick_pop_value_id", columnDefinition = "bigint(20)", unique = true, nullable = false)
private BigInteger quickPopValueId;
#Column(name = "column_name")
private String columnName;
#Column(name = "value")
private String value;
#Column(name = "formula", columnDefinition = "longtext")
private String formula;
}
Now I am trying to delete QuickPopValue one by one but it's not getting deleted and not getting any exception as well.
Below is the delete code :
List<QuickPopValue> quickPopValues = sheetRepository.findByColumnName(columnName);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.delete(qpValue);
}
Such behavior occurs when deleted object persisted in the current session.
for (QuickPopValue qpValue : quickPopValues) {
// Here you delete qpValue but this object persisted in `quickPopValues` array which is
quickPopValueRepository.delete(qpValue);
}
To solve this you can try delete by id
#Modifying
#Query("delete from QuickPopValue t where t.quickPopValueId = ?1")
void deleteQuickPopValue(Long entityId);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.deleteQuickPopValue(qpValue.getQuickPopValueId());
}

#JsonIgnoreProperties doesn't work in #OneToMany relation

#JsonIgnoreProperties doesn't work properly
I'm writing a one to many relation, one property can have multiply propertySale, I try to use #JsonIgnoreProperties to avoid infinite recursion, but for some reason it doesn't work when I try to save a propertySale. Counld someone please tell me where I do wrong?
In Property.java
#Data
#Getter
#Entity
#Table(name = "Property")
public class Property {
#Id
#GeneratedValue
private Long id;
...
#OneToMany(mappedBy="property", cascade = CascadeType.ALL, targetEntity = PropertySale.class)
#JsonIgnoreProperties("property")
private Set<PropertySale> propertySales = new HashSet<>();
...
public void addPropertySale(PropertySale propertySale){
this.propertySales.add(propertySale);
}
}
In PropertySale.java
#Data
#Getter
#Entity
#Table(name = "PropertySale")
public class PropertySale {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "property_id", referencedColumnName = "id")
#JsonIgnoreProperties("propertySales")
private Property property;
...
In PropertySaleServiceImp.java
#Service
public class PropertySaleServiceImp implements PropertySaleService{
#Autowired
private PropertySaleRepository propertySaleRepository;
#Autowired
private PropertyRepository propertyRepository;
#Override
public ResponseEntity<PropertySale> savePropertySale(PropertySale propertySale) {
Optional<Property> existPropertyOpt = this.propertyRepository.findById(propertySale.getProperty().getId());
if(existPropertyOpt.isPresent()){
Example<PropertySale> propertySaleExample = Example.of(propertySale);
Optional<PropertySale> existPropSale = this.propertySaleRepository.findOne(propertySaleExample);
if(existPropSale.isPresent()){
throw new PropertySaleAlreadyExistException();
}else{
Property existProperty = existPropertyOpt.get();
propertySale.setProperty(existProperty);
existProperty.addPropertySale(propertySale);
this.propertyRepository.save(existProperty);
return new ResponseEntity<>(propertySale, HttpStatus.CREATED);
}
}else{
throw new PropertyNotFoundException(propertySale.getProperty().getId());
}
}
I get
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.StackOverflowError
when the line this.propertyRepository.save(existProperty);
is executed.

Categories