![](/img/trans.png)
[英]Why hibernate generates insert and update for OneToMany mapping
[英]OneToMany mapping generates the wrong hibernate SQL during findById/findAll
我无法让下面的 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:错误:运算符不存在:整数 = uuid
这是完全有道理的,因为它比较了 uuid 的 Battlegroup_players id 和整数的 Battlegroup_player_villages player_id。 相反,它应该比较/加入 Battlegroup_player 的 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 错误。
我做了一些挖掘,发现了映射和类的一些问题,我会尽量解释。
警告!!! TL; 博士
我将使用 Java 编写代码,希望转换为 kotlin 不会有问题。
类也存在一些问题(提示:可序列化),因此类必须实现可序列化。
使用 lombok 来减少样板文件
这是更改后的 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;
}
}
现在,Serializable 需要在每个类中实现,因为我们使用了@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
将加载整个图形,无论您在哪里。 这意味着,它将尝试加载与此类型映射的所有关系,因此它将执行外连接(这可能会导致玩家有 2 行,因为您的条件是唯一的,因为 villageId,您在查询之前无法知道)。
如果你有超过 1 个这样的字段,即也想加入 BattleGroupId,你需要这个
@JoinColumns({
@JoinColumn(name = "player_id", referencedColumnName = "player_id"),
@JoinColumn(name = "battle_group_id", referencedColumnName = "battle_group_id")
}
)
注意:在内存数据库中使用 h2 作为测试用例
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.