OneToMany映射在findById/findAll期间生成错误的Hibernate SQL。

huangapple go评论54阅读模式
英文:

OneToMany mapping generates the wrong hibernate SQL during findById/findAll

问题

以下是您提供的内容的翻译:

@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 {
    @ManyToOne
    @JoinColumn(name = "player_id")
    var player: BattlegroupPlayerEntity? = null
}

请注意,为了使翻译更加精确,我已经移除了注释和引号中的内容。如果您需要进一步的帮助,请随时提问。

英文:

I cannot get the below OneToMany mappings to work properly, even though they are supposedly validated (by hibernate.ddl-auto=validate). I can insert all entities in the application with no problems, but while doing a findAll or findById, the queries Hibernate generates for me are wrong and result in exceptions. This is very likely due to a problem with my OneToMany mappings, or lack of a ManyToOne mapping but I don't see how to make it work.

Currently, the following tables exist in my postgres12 database:

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)
);

These are mapped to the following entities in Kotlin:

@Entity
@Table(name = &quot;battlegroups&quot;)
class BattlegroupEntity(
                        @Id
                        val id: UUID,
                        @Column(name = &quot;gameworld_id&quot;)
                        val gameworldId: UUID,
                        val name: String? = &quot;&quot;,
                        @OneToMany(mappedBy = &quot;battlegroupId&quot;, cascade = [CascadeType.ALL],fetch = FetchType.EAGER)
                        private val players: MutableList&lt;BattlegroupPlayerEntity&gt;) 

@Entity
@Table(name = &quot;battlegroup_players&quot;)
class BattlegroupPlayerEntity(@Id
                              val id: UUID,
                              @Column(name = &quot;battlegroup_id&quot;)
                              val battlegroupId: UUID,
                              @Column(name = &quot;player_id&quot;)
                              val playerId: Int,
                              val name: String,
                              @Enumerated(EnumType.STRING)
                              val tribe: Tribe,
                              @OneToMany(mappedBy= &quot;id.playerId&quot; , cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
                              val battlegroupPlayerVillages: MutableList&lt;BattlegroupPlayerVillageEntity&gt;) 

@Entity
@Table(name = &quot;battlegroup_player_villages&quot;)
class BattlegroupPlayerVillageEntity(
        @EmbeddedId
        val id: BattlegroupPlayerVillageId,
        @Column(name =&quot;village_name&quot;)
        val villageName: String,
        @Enumerated(EnumType.STRING)
        val tribe: Tribe) 

@Embeddable
data class BattlegroupPlayerVillageId(
        @Column(name = &quot;battlegroup_id&quot;)
        val battlegroupId: UUID,
        @Column(name = &quot;player_id&quot;)
        val playerId: Int,
        @Column(name = &quot;village_id&quot;)
        val villageId: Int,
        val x: Int,
        val y: Int
): Serializable

This is the SQL hibernate generates when I do a findAll/findById on a battlegroup:

 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=?

This results in an exception:

PSQLException: ERROR: operator does not exist: integer = uuid

Which makes perfect sense, since it is comparing the battlegroup_players id, which is a uuid, to the battlegroup_player_villages player_id, which is an integer. It should instead be comparing/joining on the battlegroup_player's player_id to the battlegroup_player_village's player_id.

If I change the sql to reflect that and manually execute the above query with the error line replaced:

   on players1_.player_id=battlegrou2_.player_id 

I get exactly the results I want. How can I change the OneToMany mappings so that it does exactly that?
Is it possible to do this without having a BattlegroupPlayerEntity object in my BattlegroupPlayerVillageEntity class?

Bonus points if you can get the left outer joins to become regular inner joins.

EDIT:

I tried the current answer, had to slightly adjust my embedded id because my code could not compile otherwise, should be the same thing:

@Embeddable
data class BattlegroupPlayerVillageId(
        @Column(name = &quot;battlegroup_id&quot;)
        val battlegroupId: UUID,
        @Column(name = &quot;village_id&quot;)
        val villageId: Int,
        val x: Int,
        val y: Int
): Serializable {
    @ManyToOne
    @JoinColumn(name = &quot;player_id&quot;)
    var player: BattlegroupPlayerEntity? = null
}

Using this still results in a comparison between int and uuid, for some reason.

Schema-validation: wrong column type encountered in column [player_id] in table [battlegroup_player_villages]; found [int4 (Types#INTEGER)], but expecting [uuid (Types#OTHER)]

Interestingly, if I try to put a referencedColumnName = &quot;player_id&quot; in there, I get a stackoverflow error instead.

答案1

得分: 0

我进行了一些调查,发现了一些与映射以及类相关的问题,我将尽量解释得清楚。

警告!太长不读

我将使用Java代码,我希望将其转换为Kotlin不应该是一个问题。

类也存在一些问题(提示:Serializable),因此类必须实现Serializable接口。

使用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;
    }
}

现在,由于我们使用了@EmbeddedId,每个类都需要实现Serializable接口,因为它要求容器类也是Serializable的,因此每个父类都必须实现Serializable,否则会出错。

现在,我们可以使用@JoinColumn注解来解决问题,如下所示:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.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()方法,这将返回两个玩家。下面是用于验证的测试案例。

如果您的用例是从BattleGroup中获取单个玩家,那么您将需要使用FETCH.LAZY,这对于性能也是有益的。

因为懒加载会在您实际访问它们时发出单独的查询语句。EAGER将加载整个关系图,无论您在哪里使用它。这意味着它将尝试加载与此类型映射的所有关系,因此它将执行外连接(这可能会导致玩家的两行,因为由于villageId,您的标准是唯一的,但在查询之前您无法知道)。

如果您有多个这样的字段,即也想要基于battleGroupId进行连接,您将需要这样:

@JoinColumns({
    @JoinColumn(name = "player_id", referencedColumnName = "player_id"),
    @JoinColumn(name = "battle_group_id", referencedColumnName = "battle_group_id")
})

注意:在测试案例中使用了内存数据库H2。

英文:

I did some digging and found some issues with the mapping as well as classes, I will try to explain as much as possible.

WARNING!!! TL;DR

> I will use Java for code, I hope that should not be a problem converting to kotlin.

There are some issues with classes also(hint: Serializable), so classes must implements Serializable.

> Used lombok to reduce the boilerplate

Here is the changed BattleGroupPlayer entity:

@Entity
@Getter
@NoArgsConstructor
@Table(name = &quot;battle_group&quot;)
public class BattleGroup implements Serializable {
    private static final long serialVersionUID = 6396336405158170608L;

    @Id
    private UUID id;

    private String name;

    @OneToMany(mappedBy = &quot;battleGroupId&quot;, cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List&lt;BattleGroupPlayer&gt; players = new ArrayList();

    public BattleGroup(UUID id, String name) {
        this.id = id;
        this.name = name;
    }

    public void addPlayer(BattleGroupPlayer player) {
        players.add(player);
    }
}

and BattleGroupVillage and BattleGroupVillageId entity

@AllArgsConstructor
@Entity
@Getter
@NoArgsConstructor
@Table(name = &quot;battle_group_village&quot;)
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 = &quot;battle_group_id&quot;)
    private UUID battleGroupId;

    @Column(name = &quot;player_id&quot;)
    private Integer playerId;

    @Column(name = &quot;village_id&quot;)
    private Integer villageId;

    public BattleGroupVillageId(UUID battleGroupId, Integer playerId, Integer villageId) {
        this.battleGroupId = battleGroupId;
        this.villageId = villageId;
        this.playerId = playerId;
    }
}

Now, Serializable needs to be implemented in every class as we have used @EmbeddedId which requires the container class to be Serializable as well, hence every parent class must implement serializable, otherwise it would give error.

Now, we can solve the problem using @JoinColumn annotation like below:

@OneToMany(cascade = CasacadeType.ALL, fetch =EAGER)
@JoinColumn(name = &quot;player_id&quot;, referencedColumnName = &quot;player_id&quot;)
private List&lt;BattleGroupVillage&gt; villages = new ArrayList&lt;&gt;();

> name -> field in child table and referenceColumnName -> field in parent table.

This will join the column player_id column in both entities.

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;

But this would give 2 players if you check the BattleGroup#getPlayers() method, below is the test case to verify.

UUID battleGroupId = UUID.randomUUID();

        doInTransaction( em -&gt; {
            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, &quot;Village 1&quot;);
            BattleGroupVillage village2 = new BattleGroupVillage(villageId2, &quot;Village 2&quot;);

            player.addVillage(village1);
            player.addVillage(village2);

            BattleGroup battleGroup = new BattleGroup(battleGroupId, &quot;Takeshi Castle&quot;);
            battleGroup.addPlayer(player);

            em.persist(battleGroup);

        });

        doInTransaction( em -&gt; {
            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());
        });

If your use case was to get the single player from BattleGroup then you would have to use FETCH.LAZY, which is btw good for performance as well.

> Why LAZY works?

Because LAZY loading will issue separate select statement when you really access them. EAGER will load whole graph, wherever you have it. It means, it will try to load all relationship mapped with this type, hence it will perform outer join (which may result in 2 rows for players as your criteria is unique because of villageId, which you cannot know before querying).

If you have more than 1 such fields i.e want join on battleGroupId as well, you would need this

@JoinColumns({
                    @JoinColumn(name = &quot;player_id&quot;, referencedColumnName = &quot;player_id&quot;),
                    @JoinColumn(name = &quot;battle_group_id&quot;, referencedColumnName = &quot;battle_group_id&quot;)
            }
    )

> NOTE: Used h2 in memory db for test case

huangapple
  • 本文由 发表于 2020年9月22日 02:46:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/63998269.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定