OneToMany映射在findById / findAll

时间:2020-09-21 18:46:59

标签: java spring hibernate kotlin

我无法让下面的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错误。

1 个答案:

答案 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作为测试用例