我无法让下面的OneToMany映射正常工作,即使它们已经过验证(通过hibernate.ddl-auto = validate验证)。我可以毫无问题地插入应用程序中的所有实体,但是在执行findAll或findById时,Hibernate为我生成的查询是错误的,并导致异常。这很可能是由于我的OneToMany映射出现问题,或者缺少ManyToOne映射,但我看不出如何使其起作用。
当前,我的postgres12数据库中存在以下表:
CREATE TABLE battlegroups (
id uuid,
gameworld_id uuid,
name varchar(255),
PRIMARY KEY(id)
);
CREATE TABLE battlegroup_players (
id uuid,
battlegroup_id uuid,
player_id integer,
name varchar(255),
tribe varchar(255),
PRIMARY KEY (id)
);
CREATE TABLE battlegroup_player_villages(
battlegroup_id uuid,
player_id integer,
village_id integer,
x integer,
y integer,
village_name varchar(255),
tribe varchar(255),
PRIMARY KEY(battlegroup_id, player_id, village_id, x, y)
);
这些映射到Kotlin中的以下实体:
@Entity
@Table(name = "battlegroups")
class BattlegroupEntity(
@Id
val id: UUID,
@Column(name = "gameworld_id")
val gameworldId: UUID,
val name: String? = "",
@OneToMany(mappedBy = "battlegroupId", cascade = [CascadeType.ALL],fetch = FetchType.EAGER)
private val players: MutableList<BattlegroupPlayerEntity>)
@Entity
@Table(name = "battlegroup_players")
class BattlegroupPlayerEntity(@Id
val id: UUID,
@Column(name = "battlegroup_id")
val battlegroupId: UUID,
@Column(name = "player_id")
val playerId: Int,
val name: String,
@Enumerated(EnumType.STRING)
val tribe: Tribe,
@OneToMany(mappedBy= "id.playerId" , cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
val battlegroupPlayerVillages: MutableList<BattlegroupPlayerVillageEntity>)
@Entity
@Table(name = "battlegroup_player_villages")
class BattlegroupPlayerVillageEntity(
@EmbeddedId
val id: BattlegroupPlayerVillageId,
@Column(name ="village_name")
val villageName: String,
@Enumerated(EnumType.STRING)
val tribe: Tribe)
@Embeddable
data class BattlegroupPlayerVillageId(
@Column(name = "battlegroup_id")
val battlegroupId: UUID,
@Column(name = "player_id")
val playerId: Int,
@Column(name = "village_id")
val villageId: Int,
val x: Int,
val y: Int
): Serializable
这是我在战场上执行findAll / findById时生成的SQL休眠:
select
battlegrou0_.id as id1_2_0_,
battlegrou0_.gameworld_id as gameworl2_2_0_,
battlegrou0_.name as name3_2_0_,
players1_.battlegroup_id as battlegr2_1_1_,
players1_.id as id1_1_1_,
players1_.id as id1_1_2_,
players1_.battlegroup_id as battlegr2_1_2_,
players1_.name as name3_1_2_,
players1_.player_id as player_i4_1_2_,
players1_.tribe as tribe5_1_2_,
battlegrou2_.player_id as player_i2_0_3_,
battlegrou2_.battlegroup_id as battlegr1_0_3_,
battlegrou2_.village_id as village_3_0_3_,
battlegrou2_.x as x4_0_3_,
battlegrou2_.y as y5_0_3_,
battlegrou2_.battlegroup_id as battlegr1_0_4_,
battlegrou2_.player_id as player_i2_0_4_,
battlegrou2_.village_id as village_3_0_4_,
battlegrou2_.x as x4_0_4_,
battlegrou2_.y as y5_0_4_,
battlegrou2_.tribe as tribe6_0_4_,
battlegrou2_.village_name as village_7_0_4_
from
battlegroups battlegrou0_
left outer join
battlegroup_players players1_
on battlegrou0_.id=players1_.battlegroup_id
left outer join
battlegroup_player_villages battlegrou2_
on players1_.id=battlegrou2_.player_id -- ERROR: comparing integer to uuid
where
battlegrou0_.id=?
这会导致异常:
PSQLException:错误:运算符不存在:integer = uuid
这很有道理,因为它正在将为uuid的Battlegroup_players id与作为整数的Battlegroup_player_villages player_id进行比较。相反,它应该将Battlegroup_player_village的player_id进行比较/合并。
如果我更改sql以反映该问题并手动执行上述查询,并替换错误行:
on players1_.player_id=battlegrou2_.player_id
我得到的正是我想要的结果。我该如何更改OneToMany映射,以便准确地做到这一点? 是否可以在我的BattlegroupPlayerVillageEntity类中没有BattlegroupPlayerEntity对象的情况下执行此操作?
如果您可以使左外部联接成为常规内部联接,则奖励点。
编辑:
我尝试了当前答案,由于我的代码无法编译,不得不稍微调整我的嵌入式id,应该是同一件事:
@Embeddable
data class BattlegroupPlayerVillageId(
@Column(name = "battlegroup_id")
val battlegroupId: UUID,
@Column(name = "village_id")
val villageId: Int,
val x: Int,
val y: Int
): Serializable {
@ManyToOne
@JoinColumn(name = "player_id")
var player: BattlegroupPlayerEntity? = null
}
由于某种原因,使用此方法仍会比较int和uuid。
Schema-validation: wrong column type encountered in column [player_id] in table [battlegroup_player_villages]; found [int4 (Types#INTEGER)], but expecting [uuid (Types#OTHER)]
有趣的是,如果我尝试在其中放置referencedColumnName = "player_id"
,则会收到stackoverflow错误。
答案 0 :(得分:0)
我做了一些挖掘,发现映射和类存在一些问题,我将尽力解释。
警告!!! TL; DR
我将使用Java编写代码,希望转换为Kotlin不会有问题。
类也有一些问题(提示:Serializable),因此类必须实现Serializable。
使用龙目岛来减少样板
这是已更改的BattleGroupPlayer实体:
@Entity
@Getter
@NoArgsConstructor
@Table(name = "battle_group")
public class BattleGroup implements Serializable {
private static final long serialVersionUID = 6396336405158170608L;
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "battleGroupId", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<BattleGroupPlayer> players = new ArrayList();
public BattleGroup(UUID id, String name) {
this.id = id;
this.name = name;
}
public void addPlayer(BattleGroupPlayer player) {
players.add(player);
}
}
以及BattleGroupVillage和BattleGroupVillageId实体
@AllArgsConstructor
@Entity
@Getter
@NoArgsConstructor
@Table(name = "battle_group_village")
public class BattleGroupVillage implements Serializable {
private static final long serialVersionUID = -4928557296423893476L;
@EmbeddedId
private BattleGroupVillageId id;
private String name;
}
@Embeddable
@EqualsAndHashCode
@Getter
@NoArgsConstructor
public class BattleGroupVillageId implements Serializable {
private static final long serialVersionUID = -6375405007868923427L;
@Column(name = "battle_group_id")
private UUID battleGroupId;
@Column(name = "player_id")
private Integer playerId;
@Column(name = "village_id")
private Integer villageId;
public BattleGroupVillageId(UUID battleGroupId, Integer playerId, Integer villageId) {
this.battleGroupId = battleGroupId;
this.villageId = villageId;
this.playerId = playerId;
}
}
现在,需要在每个类中实现序列化,因为我们已经使用过@EmbeddedId
,这也要求容器类也要序列化,因此每个父类都必须实现序列化,否则会出错。
现在,我们可以使用@JoinColumn
注释来解决问题,如下所示:
@OneToMany(cascade = CasacadeType.ALL, fetch =EAGER)
@JoinColumn(name = "player_id", referencedColumnName = "player_id")
private List<BattleGroupVillage> villages = new ArrayList<>();
子表中的name->字段和父表中的referenceColumnName->字段。
这将在两个实体中加入列player_id
的列。
SELECT
battlegrou0_.id AS id1_0_0_,
battlegrou0_.name AS name2_0_0_,
players1_.battle_group_id AS battle_g2_1_1_,
players1_.id AS id1_1_1_,
players1_.id AS id1_1_2_,
players1_.battle_group_id AS battle_g2_1_2_,
players1_.player_id AS player_i3_1_2_,
villages2_.player_id AS player_i4_2_3_,
villages2_.battle_group_id AS battle_g1_2_3_,
villages2_.village_id AS village_2_2_3_,
villages2_.battle_group_id AS battle_g1_2_4_,
villages2_.player_id AS player_i4_2_4_,
villages2_.village_id AS village_2_2_4_,
villages2_.name AS name3_2_4_
FROM
battle_group battlegrou0_
LEFT OUTER JOIN
battle_group_player players1_ ON battlegrou0_.id = players1_.battle_group_id
LEFT OUTER JOIN
battle_group_village villages2_ ON players1_.player_id = villages2_.player_id
WHERE
battlegrou0_.id = 1;
但是,如果您检查BattleGroup#getPlayers()
方法,这将给2个玩家带来好处,下面是要验证的测试案例。
UUID battleGroupId = UUID.randomUUID();
doInTransaction( em -> {
BattleGroupPlayer player = new BattleGroupPlayer(UUID.randomUUID(), battleGroupId, 1);
BattleGroupVillageId villageId1 = new BattleGroupVillageId(
battleGroupId,
1,
1
);
BattleGroupVillageId villageId2 = new BattleGroupVillageId(
battleGroupId,
1,
2
);
BattleGroupVillage village1 = new BattleGroupVillage(villageId1, "Village 1");
BattleGroupVillage village2 = new BattleGroupVillage(villageId2, "Village 2");
player.addVillage(village1);
player.addVillage(village2);
BattleGroup battleGroup = new BattleGroup(battleGroupId, "Takeshi Castle");
battleGroup.addPlayer(player);
em.persist(battleGroup);
});
doInTransaction( em -> {
BattleGroup battleGroup = em.find(BattleGroup.class, battleGroupId);
assertNotNull(battleGroup);
assertEquals(2, battleGroup.getPlayers().size());
BattleGroupPlayer player = battleGroup.getPlayers().get(0);
assertEquals(2, player.getVillages().size());
});
如果您的用例是从BattleGroup
中选拔一名球员,那么您将不得不使用FETCH.LAZY
,这同样对性能也有好处。
为什么LAZY可以工作?
因为当您真正访问LAZY加载时,它们会发出单独的select语句。 EAGER
将在您拥有的任何位置加载整个图形。这意味着,它将尝试加载与此类型映射的所有关系,因此它将执行外部联接(由于乡村ID,您的条件是唯一的,因此可能会导致玩家有2行,因为在查询之前您无法知道。)
如果您有1个以上这样的字段,即也想加入BattleGroupId,那么您将需要此
@JoinColumns({
@JoinColumn(name = "player_id", referencedColumnName = "player_id"),
@JoinColumn(name = "battle_group_id", referencedColumnName = "battle_group_id")
}
)
注意:在内存db中使用h2作为测试用例