Spring-JPA中关于Entity继承的问题(上)

2021-02-20 16:29发布

参考资料: https://developer.aliyun.com/article/312873

org.hibernate.WrongClassException:Object with [id=11] was not of the specified subclass: xxxx.entity(loaded object was of wrong class ...

这个错误其实是说,在构建当前的Entity1列表(LIST)的时候,发现该Entity所共用对象的另一个Entity2中,已经存在了id=11的对象,并且Entity1和Entity2的id=11的对象在结构上是不一样的,无法覆盖。 看到这个错误是不是有点蒙,像是在听天书的感觉?其实,这个Exception的本质是和JPA中hibernate的继承映射有关。

当前笔者的项目中,定义了BaseEntity,其中包含了code(编码),name(名称),elementcode(要素的tag)这些通用属性。然后,分别定义了Entity1和Entity2,且都集成自BaseEntity。Entity1和Entity2的的elementcode返回的内容不一样。举例子来描述的话,就相当于两组不同的字典要素,一个是桌子(Entity1,elementcode=桌子),一个是椅子(Entity2,elementcode=椅子),他们都属于家具(BaseEntity)。这是一种比较常见的继承的实践。

在基于sringboot-jpa的架构上,用代码描述如下

`

// 家具
@Entity
@Data
public abstract class BaseEntity{
	@Id 
	@GeneratedValue 
	private int id; 

	private String code; 
	private String name; 
	private String elementcode; 
}

// 桌子 
@Entity 
@Data 
public class Entity1 extends BaseEntity implements java.io.Serializable{
	public String getElementcode(){ return "桌子"; }
}

// 椅子 
@Entity 
@Data 
public class Entity2 extends BaseEntity implements java.io.Serializable{
	int venderId;
	String venderCode;
	String venderName;
	
	public String getElementcode(){ return "椅子"; } 
}

`

此时,如果我在一个方法内调用了dao来获取Entity1和Entity2,就会出最开始的那个WrongClass的异常,而原因就是hibernate认为两个entity是同一个entity,先通过dao获得entity1后,再获取entity2的时候, 由于Entity2的内部属性和Entity1不一样,且两个entity都有一个id=11的对象,所以就报了WrongClassException。

一开始,再网上找了一下,结果发现很多内容都是就事论事,让修改id值,避免id重复就好了。这个明显不是我所期望的结果。于是我换了一个思路,开始寻找hibernate关于继承及映射的相关说明,还就真的找到了根本原因。

简而言之,EJB3支持三种类型的继承映射:

1、每个类一张表(TABLE_PER_CLASS) 这个策略的效果,类似于你查询多个表的结果然后通过union组合在一起而得到的查询结果。所以在此策略下,@Id 将无法使用自增这个策略的效果,类似于你查询多个表的结果然后通过union组合在一起而得到的查询结果。所以在此策略下,@Id 将无法使用自生成及自增模式(AUTO生成器和IDENTITY生成器)。道理也很简单,通过union方式获取的结果对象,其各自的Entity都是独立的,所以@Id肯定是不能在当前的这个组合而来的Entity内共用。

回到之前的案例,我的目标是大家都继承自一个父类Entity,而不是要通过并集的方式获得一个新的Entity,所以这个策略不符合期望。

2、每个类层次一张表(SINGLE_TABLE) 这种呢,相当于所有的子Entity都是来自于一个Entity中,然后每个子Entity是通过父Entity中的一个标识字段内不同value进行区分。就相当于有个很大的要素表<Table家具>,这个表里有一个字段elementcode,当elementcode=桌子,将得到对应的Entity——桌子,而当elementcode=椅子,则得到对应的Entity——椅子。

再继续看案例,貌似很相似啊,但其实不然,案例中的子类Entity是来自不同的表,这个策略则是要来自同一个表,所以是不行的。当然,我也试了这个策略,结果就是提示我elementcode有问题,所以就是说还是不行。

3、每个类一张表(JOINED) 这个策略,其实类似于基于父类的不同的属性的继承。网上的资料使用这个代码进行解释:

`

@Entity
@Table(name = "T_ANIMAL")
@Inheritance(strategy=InheritanceType.JOINED)
public class Animal {  
	@Id  
	@Column(name = "ID")  
	@GeneratedValue(strategy = GenerationType.AUTO)  
	private Integer id;  

	@Column(name = "NAME")  
	private String name;  

	@Column(name = "COLOR")  
	private String color;   
}  

@Entity  
@Table(name = "T_BIRD")  
@PrimaryKeyJoinColumn(name = "BIRD_ID")  
public class Bird extends Animal {  

	@Column(name = "SPEED")  
	private String speed;  
}  

@Entity  
@Table(name = "T_DOG")  
@PrimaryKeyJoinColumn(name = "DOG_ID")
public class Dog extends Animal {

	@Column(name = "LEGS")
	private Integer legs;
}  

` 实际表结构如下: T_ANIMAL ID,COLOR,NAME T_BIRD SPEED,BIRD(既是外键,也是主键) T_DOG LEGS,DOG_ID(既是外键,也是主键)

在这个策略中,所有Entity也都是需要有对应的数据库实体的,并且所有的子类相互可以认为没有啥强的共同属性,因此这个策略也不适用于先前的案例需求。

好吧,上述三种都不符合需求,那么这个时候该怎么办?很多时候,我们经常会通过一个父类来实现共享一些公共属性,并且这个父类并不一定有对应的映射实体,即该实体不一定有对应的表。 这个时候@MappedSuperclass登场了。 还是先前的案例,现在改写如下

`

// 家具 
@MappedSuperclass
@Data
public abstract class BaseEntity{
	@Id
	@GeneratedValue
	private int id;

	private String code;
	private String name;
	private String elementcode;
}

// 桌子
@Entity
@Data 
public class Entity1 extends BaseEntity implements java.io.Serializable{
	public String getElementcode(){ return "桌子"; }
}

// 椅子 
@Entity 
@Data 
public class Entity2 extends BaseEntity implements java.io.Serializable{
	int venderId;
	String venderCode;
	String venderName;
	
	public String getElementcode(){ return "椅子"; } 
}

`

此时,父类家具中的属性映射将复制到桌子和椅子两个子类实体中,父类是没有对应的数据库实体表的,而两个子类则是有对应的实体表,但他们都同时具有父类中的所有属性。所以案例中的需求在此得到完美实现。 P.S:如果不使用@MappedSuperclass注解父类的话,父类中的属性将被忽略。

标签: