Home > Software design >  Detached entity passed to persist when I try to save an entity
Detached entity passed to persist when I try to save an entity

Time:05-19

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 [saved user in 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

CodePudding user response:

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);
}
  • Related