我的数据库中有 2 个实体,具有一对一的一对一方向映射:用户和密码重置令牌。这背后的想法是每次用户请求重置密码时创建新令牌并仅存储最新的令牌。
以下是我的实体:
@Entity
@Table(name = "USERS")
@Getter @Setter
public class User implements Serializable {
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "usersSeq")
@SequenceGenerator(name = "usersSeq", sequenceName = "SEQ_USERS", allocationSize = 1)
private long id;
@Column(name = "NAME")
private String name;
@Column(name = "PASSWORD")
private String password;
@Column(name = "EMAIL")
private String email;
@Column(name = "ROLE")
private Integer role;
}
///...
@Entity
@Table(name = "PASSWORD_RESET_TOKENS")
@Getter
@Setter
public class PasswordResetToken implements Serializable {
private static final int EXPIRATION = 24;
@Column(name = "TOKEN")
private String token;
@Id
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(nullable = false, name = "user_id")
private User user;
@Column(name = "EXPIRY_DATE")
private Instant expiryDate;
public PasswordResetToken() {
}
public void setExpiryDate(ZonedDateTime expiryDate) {
this.expiryDate = expiryDate.plus(EXPIRATION, ChronoUnit.HOURS).toInstant();
}
}
此外,我还为他们俩创建了 DTO,以便在我的应用程序中传递它们。代码片段:
@Getter @Setter
public class PasswordResetTokenModel {
private String token;
private ZonedDateTime expiryDate;
private UserModel user;
}
UserModel也用于Spring Security。
@Getter
@Setter
public class UserModel extends User {
public UserModel(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
private long id;
private String name;
public String getEmail() {
return this.getUsername();
}
}
对于人口,我创建了 2 个填充器:
@Component
public class UserPopulatorImpl implements UserPopulator {
@Autowired
UserDetailsService userDetailsService;
@Override
public UserModel populateToDTO(User user) {
UserModel userModel = new UserModel(user.getEmail(), user.getPassword(), userDetailsService.getAuthorities(user.getRole()));
userModel.setId(user.getId());
return userModel;
}
@Override
public User populateToDAO(UserModel userModel) {
User user = new User();
user.setEmail(userModel.getEmail());
user.setName(userModel.getName());
user.setPassword(userModel.getPassword());
//TODO: change it!
user.setRole(1);
return user;
}
}
//...
@Component
public class PasswordResetTokenPopulatorImpl implements PasswordResetTokenPopulator {
@Autowired
UserPopulator userPopulator;
@Override
public PasswordResetTokenModel populateToDTO(PasswordResetToken passwordResetToken) {
PasswordResetTokenModel passwordResetTokenModel = new PasswordResetTokenModel();
passwordResetTokenModel.setUser(userPopulator.populateToDTO(passwordResetToken.getUser()));
passwordResetTokenModel.setToken(passwordResetToken.getToken());
passwordResetTokenModel.setExpiryDate(ZonedDateTime.ofInstant(passwordResetToken.getExpiryDate(), ZoneId.systemDefault()));
return passwordResetTokenModel;
}
@Override
public PasswordResetToken populateToDAO(PasswordResetTokenModel passwordResetTokenModel) {
PasswordResetToken passwordResetToken = new PasswordResetToken();
passwordResetToken.setExpiryDate(passwordResetTokenModel.getExpiryDate());
passwordResetToken.setUser(userPopulator.populateToDAO(passwordResetTokenModel.getUser()));
passwordResetToken.setToken(passwordResetTokenModel.getToken());
return passwordResetToken;
}
}
我正在使用保存对象
sessionFactory.getCurrentSession().saveOrUpdate(token);
当我使用此代码时,我收到以下异常
object references an unsaved transient instance - save the transient instance before flushing: com.demo.megaevents.entities.User
此代码中目前有 2 个问题:
- 似乎我的一对一映射中的Cascade.ALL不起作用。如果我在令牌类中创建单独的主键,一切几乎都可以正常工作正如预期的那样,但将每个创建的令牌存储在数据库中(更像一对多关系(,但是我想避免它,因为我需要存储我的数据库中每个用户只有一个令牌
- 我不喜欢在填充器中使用 new,因为它强制休眠在刷新会话时创建新对象。但是,我也不想再做一次选择来从数据库中获取此数据,因为就在提到的填充器之前,我已经执行了此查询来获取它,我认为这是一个开销。
另外,我真的很想拥有DTO,但我不想删除DTO层。
所以,我的问题:
- 处理DTO和实体之间的填充的正确方法是什么?
- 我的解决方案是否有任何其他改进(可能是体系结构上的改进(?
多谢。
我不确定你为什么要让UserModel
扩展User
,但我想你这样做是因为你不想将所有属性从User
复制到UserModel
。太糟糕了,因为这是在实体模型和数据传输模型之间实现清晰分离所需要的。
获得该异常是因为您尝试保留具有对具有 id 的User
对象的引用的PasswordResetToken
,但User
未与当前会话关联。您不必查询用户,但至少将其与会话相关联,如下所示:
PasswordResetToken token = // wherever you get that from
Session s = sessionFactory.getCurrentSession();
token.setUser(s.load(User.class, token.getUser().getId());
s.persist(token);
级联会导致通过 SQL INSERT 或 UPDATE 语句创建/插入或更新User
,这显然不是您想要的。
如果你愿意,你可以在填充器中做Session.load()
调用,但我不会那样做。实际上,我建议根本不使用填充器,而是在您的服务中创建实体对象。
通常,您只有几种(主要是 1 种(实际创建新实体对象的方法,因此从 DTO 到实体的转换的完整范围仅在极少数情况下相关。
大多数情况下,您要进行更新,为此,您应该首先选择现有实体并应用允许从实体对象上的 DTO 更改的字段。
为了向表示层提供 DTO,我建议使用 Blaze-Persistence 实体视图来避免手动映射样板,并提高选择查询的性能。