繁体   English   中英

OneToMany 映射在 findById/findAll 期间生成错误的休眠 SQL

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM