第一步:在 FavoriteWord 类上加约束。找到类上面的 @Entity,在它下面加一行 @Table:
@Entity
@Table(
name = "favorite_word",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "kind", "word"})
)
public class FavoriteWord {
@Table:指定表名并附加约束。这里表名保持默认的 favorite_word。
@UniqueConstraint(columnNames = {...}):这三列合起来不能重复。同一个人的 (w, mitigate) 只能有一行;不同的人可以各有一行。
columnNames 里写的是数据库的列名(下划线),不是 Java 字段名:userId → user_id。
- 文件顶部要能识别
@Table 和 @UniqueConstraint:如果你的 import 是 import jakarta.persistence.*; 就已经包含;如果是一个个 import 的,补上 import jakarta.persistence.Table; 和 import jakarta.persistence.UniqueConstraint;。
第二步:让 Controller 在撞到约束时优雅处理。现在 POST 里的"先查再存"保留,它是第一道关。只在极少数并发时才会撞到约束,那时 save 会抛出 DataIntegrityViolationException。把 save 那一句包进 try/catch,捕获后重新查一次并返回已有的那条:
try {
return repository.save(new FavoriteWord(userId, kind, word, status));
} catch (DataIntegrityViolationException e) {
// 另一个请求刚好抢先写入了同一条:直接返回那一条
return repository.findByUserIdAndKindAndWord(userId, kind, word).orElseThrow();
}
需要 import org.springframework.dao.DataIntegrityViolationException;。变量名请换成你 Controller 里实际用的那几个。orElseThrow() 表示"盒子是空的就抛异常",这里理论上不会空。
第三步:重启后检查。启动时看控制台,因为 ddl-auto=update,Hibernate 会尝试给已有的表加上这个约束。如果你的 favorite_word 里已经有重复的行,加约束会失败(会在日志里报错但不一定停止启动)。先在 H2 控制台查一下:
SELECT user_id, kind, word, COUNT(*) FROM favorite_word
GROUP BY user_id, kind, word HAVING COUNT(*) > 1;
没有结果就没有重复;有结果就把多余的行 DELETE 掉再重启。上线用的是全新的 PostgreSQL,不会有这个问题,建表时就直接带着约束。