Issue
I'm getting this error when I try to save my user entity to the database
org.hibernate.PersistentObjectException: detached entity passed to persist: kpi.diploma.ovcharenko.entity.user.AppUser
Some more information where where does this error appear
at kpi.diploma.ovcharenko.service.user.LibraryUserService.createPasswordResetTokenForUser(LibraryUserService.java:165) ~[classes/:na]
at kpi.diploma.ovcharenko.service.user.LibraryUserService$$FastClassBySpringCGLIB$$ca63bf4b.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.3.10.jar:5.3.10]
at kpi.diploma.ovcharenko.service.user.LibraryUserService$$EnhancerBySpringCGLIB$$6160822e.createVerificationTokenForUser(<generated>) ~[classes/:na]
at kpi.diploma.ovcharenko.service.activation.RegistrationListener.confirmRegistration(RegistrationListener.java:39) ~[classes/:na]
at kpi.diploma.ovcharenko.service.activation.RegistrationListener.onApplicationEvent(RegistrationListener.java:32) ~[classes/:na]
at kpi.diploma.ovcharenko.service.activation.RegistrationListener.onApplicationEvent(RegistrationListener.java:16) ~[classes/:na]
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) ~[spring-context-5.3.10.jar:5.3.10]
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) ~[spring-context-5.3.10.jar:5.3.10]
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) ~[spring-context-5.3.10.jar:5.3.10]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:421) ~[spring-context-5.3.10.jar:5.3.10]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:378) ~[spring-context-5.3.10.jar:5.3.10]
at kpi.diploma.ovcharenko.controller.UserController.registerUserAccount(UserController.java:96) ~[classes/:na]
As you can see error appeared in this method. In this method I create a verification token for user
@Override
@Transactional
public void createVerificationTokenForUser(final AppUser user, final String token) {
final VerificationToken myToken = new VerificationToken(token, user);
verificationTokenRepository.save(myToken);
}
I call this method in my RegistrationListener
private void confirmRegistration(OnRegistrationCompleteEvent event) {
AppUser user = event.getUser();
log.info(user.toString());
String token = UUID.randomUUID().toString();
userService.createVerificationTokenForUser(user, token);
final SimpleMailMessage email = constructEmailMessage(event, user, token);
mailSender.send(email);
}
And how I call my confirmRegistration method in the RegistrationListener
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent event) {
this.confirmRegistration(event);
}
And this RegistrationListener I used in my Controller like this
@PostMapping("/registration")
public String registerUserAccount(@ModelAttribute("user") @Valid UserModel userModel, BindingResult result, HttpServletRequest request) {
AppUser existing = userService.findByEmail(userModel.getEmail());
if (existing != null) {
result.rejectValue("email", null, "There is already an account registered with that email");
}
if (result.hasErrors()) {
return "registration";
}
AppUser registeredUser = userService.save(userModel);
log.info(registeredUser.toString());
String appUrl = request.getContextPath();
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registeredUser, request.getLocale(), appUrl));
return "redirect:/regSuccessfully";
}
As you can see I have log.info() in the controller and in the listener, its because I thought that the problem can be because something wrong with user ID, but, when my logs showed that with user and user id is everything is ok
2022-05-16 21:33:27.522 INFO 65143 --- [nio-8080-exec-7] k.d.o.controller.UserController : AppUser{id=42, firstName='[email protected]', lastName='[email protected]', email='[email protected]', telephoneNumber='', password='$2a$10$4ao20SYR2QJsQ.Fj50Jek.ZBRG0R0g9N8t3iaksUx2.byIb0fj6Y6', registrationDate=2022-05-16 21:33:27.492, enabled=false, roles=[kpi.diploma.ovcharenko.entity.user.UserRole@bbe3026f], bookCards=[]}
2022-05-16 21:33:27.523 INFO 65143 --- [nio-8080-exec-7] k.d.o.s.activation.RegistrationListener : AppUser{id=42, firstName='[email protected]', lastName='[email protected]', email='[email protected]', telephoneNumber='', password='$2a$10$4ao20SYR2QJsQ.Fj50Jek.ZBRG0R0g9N8t3iaksUx2.byIb0fj6Y6', registrationDate=2022-05-16 21:33:27.492, enabled=false, roles=[kpi.diploma.ovcharenko.entity.user.UserRole@bbe3026f], bookCards=[]}
And another point is that after I try to register user, i am getting the error as above, but my user is successfully saved into the database [
Here is my AppUser class and VerificationToken class
@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.AUTO)
private Long id;
@NotEmpty
@Column(name = "first_name")
private String firstName;
@NotEmpty
@Column(name = "last_name")
private String lastName;
@NotEmpty
@Email
@Column(name = "email")
private String email;
@Column(name = "number")
private String telephoneNumber;
@NotEmpty
@Column(name = "password")
private String password;
@CreationTimestamp
@Column(name = "create_time")
private Timestamp registrationDate;
@Column(name = "enabled")
private boolean enabled;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Collection<UserRole> roles = new HashSet<>();
@EqualsAndHashCode.Exclude
@OnDelete(action = OnDeleteAction.CASCADE)
@OneToMany(mappedBy = "book", fetch = FetchType.EAGER)
private Set<BookCard> bookCards = new HashSet<>();
public void addBookCard(BookCard bookCard) {
bookCards.add(bookCard);
bookCard.setUser(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AppUser user = (AppUser) o;
return Objects.equals(id, user.id) &&
Objects.equals(firstName, user.firstName) &&
Objects.equals(lastName, user.lastName) &&
Objects.equals(email, user.email) &&
Objects.equals(password, user.password);
}
@Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, email, password);
}
public void setRoles(Collection<UserRole> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "AppUser{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", telephoneNumber='" + telephoneNumber + '\'' +
", password='" + password + '\'' +
", registrationDate=" + registrationDate +
", enabled=" + enabled +
", roles=" + roles +
", bookCards=" + bookCards +
'}';
}
}
@Getter
@Setter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Table(name = "verification_token")
public class VerificationToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne
@JoinColumn(nullable = false, name = "user_id", foreignKey = @ForeignKey(name = "FK_VERIFY_USER"))
private AppUser user;
private Date expiryDate;
public VerificationToken(final String token, final AppUser user) {
super();
this.token = token;
this.user = user;
this.expiryDate = calculateExpiryDate();
}
public Long getId() {
return id;
}
public String getToken() {
return token;
}
public void setToken(final String token) {
this.token = token;
}
public AppUser getUser() {
return user;
}
public void setUser(final AppUser user) {
this.user = user;
}
public Date getExpiryDate() {
return expiryDate;
}
public void setExpiryDate(final Date expiryDate) {
this.expiryDate = expiryDate;
}
private Date calculateExpiryDate() {
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(new Date().getTime());
cal.add(Calendar.MINUTE, VerificationToken.EXPIRATION);
return new Date(cal.getTime().getTime());
}
public void updateToken(final String token) {
this.token = token;
this.expiryDate = calculateExpiryDate();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VerificationToken that = (VerificationToken) o;
return Objects.equals(token, that.token) && Objects.equals(user, that.user) && Objects.equals(expiryDate, that.expiryDate);
}
@Override
public int hashCode() {
return Objects.hash(token, user, expiryDate);
}
}
I tried different variants with of cascade type , for example (cascade = CascadeType.ALL, cascade = CascadeType.MERGE). Also as you can see in VerificationToken class, I tried to delete any type of cascade, but it doesn't help me. I don't have any idea how can I solve this problem
Solution
You probably load the user within a transaction and then create and publish an event on which you set the user. After the transactional method where you created and published the event has ended, the transaction ends as well and the AppUser
entity becomes detached from the persistence context. In order to use it further like an entity that you have just obtained from a repository or the EntityManager
, you need to "reattach" it to the context. You can read more about the lifecycle of a Hibernate entity in the Hibernate docs or in this Baldung article.
Alternatively, you could load the AppUser
again within the same transaction where you want to persist the VerificationToken
. You could for example change your method to:
private final AppUserRepository appUserRepository;
@Override
@Transactional
public void createVerificationTokenForUser(final String userId, final String token) {
final AppUser user = appUserRepository.findById(userId).orElseThrow();
final VerificationToken myToken = new VerificationToken(token, user);
verificationTokenRepository.save(myToken);
}
Answered By - Times
Answer Checked By - Cary Denson (JavaFixing Admin)