当我尝试保存实体时,分离的实体传递给持久化
Detached entity passed to persist when I try to save an entity
当我尝试将我的用户实体保存到数据库时出现此错误
org.hibernate.PersistentObjectException: detached entity passed to persist: kpi.diploma.ovcharenko.entity.user.AppUser
一些更多信息,这个错误出现在哪里
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$60822e.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]
如您所见,此方法出现错误。在这种方法中,我为 user
创建了一个验证令牌
@Override
@Transactional
public void createVerificationTokenForUser(final AppUser user, final String token) {
final VerificationToken myToken = new VerificationToken(token, user);
verificationTokenRepository.save(myToken);
}
我在我的 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);
}
以及我如何在 RegistrationListener 中调用我的 confirmRegistration 方法
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent event) {
this.confirmRegistration(event);
}
我在控制器中使用的这个 RegistrationListener 是这样的
@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";
}
如您所见,我在控制器和侦听器中有 log.info(),这是因为我认为问题可能是因为用户 ID 有问题,但是,当我的日志显示user 和 user id 一切正常
2022-05-16 21:33:27.522 INFO 65143 --- [nio-8080-exec-7] k.d.o.controller.UserController : AppUser{id=42, firstName='ovcharenko.messor@gmail.com', lastName='ovcharenko.messor@gmail.com', email='ovcharenko.messor@gmail.com', telephoneNumber='', password='aao20SYR2QJsQ.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='ovcharenko.messor@gmail.com', lastName='ovcharenko.messor@gmail.com', email='ovcharenko.messor@gmail.com', telephoneNumber='', password='aao20SYR2QJsQ.Fj50Jek.ZBRG0R0g9N8t3iaksUx2.byIb0fj6Y6', registrationDate=2022-05-16 21:33:27.492, enabled=false, roles=[kpi.diploma.ovcharenko.entity.user.UserRole@bbe3026f], bookCards=[]}
还有一点是,在我尝试注册用户后,出现了上述错误,但我的用户已成功保存到数据库中
[
这是我的 AppUser class 和 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);
}
}
我尝试了 级联类型 的不同变体,例如 (cascade = CascadeType.ALL, cascade = CascadeType.MERGE)。另外,正如您在 VerificationToken class 中看到的那样,我尝试删除任何类型的级联,但它对我没有帮助。我不知道如何解决这个问题
您可能在交易中加载用户,然后创建并发布您设置用户的事件。在您创建和发布事件的事务方法结束后,事务也结束并且 AppUser
实体从持久性上下文中分离。为了进一步像您刚从存储库或 EntityManager
获得的实体一样使用它,您需要将它“重新附加”到上下文。您可以在 Hibernate docs or in this Baldung article.
中阅读有关 Hibernate 实体生命周期的更多信息
或者,您可以在要保留 VerificationToken
的同一事务中再次加载 AppUser
。例如,您可以将方法更改为:
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);
}
当我尝试将我的用户实体保存到数据库时出现此错误
org.hibernate.PersistentObjectException: detached entity passed to persist: kpi.diploma.ovcharenko.entity.user.AppUser
一些更多信息,这个错误出现在哪里
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$60822e.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]
如您所见,此方法出现错误。在这种方法中,我为 user
创建了一个验证令牌@Override
@Transactional
public void createVerificationTokenForUser(final AppUser user, final String token) {
final VerificationToken myToken = new VerificationToken(token, user);
verificationTokenRepository.save(myToken);
}
我在我的 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);
}
以及我如何在 RegistrationListener 中调用我的 confirmRegistration 方法
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent event) {
this.confirmRegistration(event);
}
我在控制器中使用的这个 RegistrationListener 是这样的
@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";
}
如您所见,我在控制器和侦听器中有 log.info(),这是因为我认为问题可能是因为用户 ID 有问题,但是,当我的日志显示user 和 user id 一切正常
2022-05-16 21:33:27.522 INFO 65143 --- [nio-8080-exec-7] k.d.o.controller.UserController : AppUser{id=42, firstName='ovcharenko.messor@gmail.com', lastName='ovcharenko.messor@gmail.com', email='ovcharenko.messor@gmail.com', telephoneNumber='', password='aao20SYR2QJsQ.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='ovcharenko.messor@gmail.com', lastName='ovcharenko.messor@gmail.com', email='ovcharenko.messor@gmail.com', telephoneNumber='', password='aao20SYR2QJsQ.Fj50Jek.ZBRG0R0g9N8t3iaksUx2.byIb0fj6Y6', registrationDate=2022-05-16 21:33:27.492, enabled=false, roles=[kpi.diploma.ovcharenko.entity.user.UserRole@bbe3026f], bookCards=[]}
还有一点是,在我尝试注册用户后,出现了上述错误,但我的用户已成功保存到数据库中
[
这是我的 AppUser class 和 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);
}
}
我尝试了 级联类型 的不同变体,例如 (cascade = CascadeType.ALL, cascade = CascadeType.MERGE)。另外,正如您在 VerificationToken class 中看到的那样,我尝试删除任何类型的级联,但它对我没有帮助。我不知道如何解决这个问题
您可能在交易中加载用户,然后创建并发布您设置用户的事件。在您创建和发布事件的事务方法结束后,事务也结束并且 AppUser
实体从持久性上下文中分离。为了进一步像您刚从存储库或 EntityManager
获得的实体一样使用它,您需要将它“重新附加”到上下文。您可以在 Hibernate docs or in this Baldung article.
或者,您可以在要保留 VerificationToken
的同一事务中再次加载 AppUser
。例如,您可以将方法更改为:
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);
}