如何在Spring JPA中实现一个实体关联多个不同实体?
问题描述
需要实现投诉功能:User可以投诉多种不同实体(Question、Answer、Comment或其他User),数据库schema设计为单表complains,其中user_id关联发起投诉的用户(一对多),entity_id关联被投诉的实体(多对一)。
尝试用泛型类Complain实现,但抛出异常:
org.hibernate.AnnotationException: Property com.*.*.Entities.Complain.entity_id has an unbound type and no explicit target entity. Resolve this Generic usage issue or set an explicit target attribute (eg @OneToMany(target=) or use an explicit @Type...
原有代码如下:
Complain.java(泛型版本)
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "complains") public class Complain<T extends BaseComplain> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user_id; @ManyToOne @JoinColumn(name = "entity_id", nullable = false) private T entity_id; @Column @Temporal(TemporalType.TIMESTAMP) private Date created_on; }
User.java
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "users") public class User extends BaseComplain { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @OneToMany(mappedBy = "user_id", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude private Set<Complain<BaseComplain>> author_complains; @OneToMany(mappedBy = "entity_id", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude private Set<Complain<User>> complains; // 其他内容... }
Question.java(其他实体实现类似)
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "questions") public class Question extends BaseComplain { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "entity_id", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude @JsonManagedReference private Set<Complain<Question>> complains; // 其他内容... }
问题:能否仅使用JPA基础特性实现该逻辑?已考虑过@MappedSuperclass,需要具体实现方案。
解决方案
核心问题分析
JPA规范不支持泛型实体类,Hibernate无法在运行时解析未绑定的泛型参数T,因此抛出类型未绑定的异常。需要用多态关联+映射超类的方式替代泛型实现。
步骤1:重构BaseComplain为@MappedSuperclass
将所有可被投诉的实体的公共主键字段抽离到映射超类中:
@MappedSuperclass public abstract class BaseComplain { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 公共getter/setter public Long getId() { return id; } public void setId(Long id) { this.id = id; } }
步骤2:修改可被投诉的实体
让User、Question等实体继承BaseComplain,移除重复的id字段:
User.java
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "users") public class User extends BaseComplain { // 移除原有id字段,继承自BaseComplain @OneToMany(mappedBy = "user", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude private Set<Complain> authorComplains; @OneToMany(mappedBy = "targetEntity", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude private Set<Complain> complains; // 其他原有字段... }
Question.java
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "questions") public class Question extends BaseComplain { // 移除原有id字段,继承自BaseComplain @OneToMany(mappedBy = "targetEntity", orphanRemoval = true, fetch = FetchType.LAZY) @ToString.Exclude @JsonManagedReference private Set<Complain> complains; // 其他原有字段... }
步骤3:重构Complain实体(去掉泛型)
添加实体类型字段解决不同实体id冲突问题,直接关联BaseComplain:
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @ToString @Entity @Table(name = "complains") public class Complain { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; @ManyToOne @JoinColumn(name = "entity_id", nullable = false) private BaseComplain targetEntity; // 用枚举存储实体类型,比字符串更安全 @Enumerated(EnumType.STRING) @Column(name = "entity_type", nullable = false) private EntityType entityType; @Column @Temporal(TemporalType.TIMESTAMP) private Date createdOn; // 辅助方法:设置目标实体时自动填充类型 public void setTargetEntity(BaseComplain targetEntity) { this.targetEntity = targetEntity; if (targetEntity instanceof User) { this.entityType = EntityType.USER; } else if (targetEntity instanceof Question) { this.entityType = EntityType.QUESTION; } else if (targetEntity instanceof Answer) { this.entityType = EntityType.ANSWER; } else if (targetEntity instanceof Comment) { this.entityType = EntityType.COMMENT; } } }
步骤4:定义EntityType枚举
public enum EntityType { USER, QUESTION, ANSWER, COMMENT }
查询示例
查询某个用户收到的所有投诉:
List<Complain> userComplains = entityManager.createQuery( "SELECT c FROM Complain c WHERE c.targetEntity.id = :userId AND c.entityType = :userType", Complain.class ) .setParameter("userId", userId) .setParameter("userType", EntityType.USER) .getResultList();
内容的提问来源于stack exchange,提问作者p0var
相关产品推荐
相关产品推荐

