【问题标题】:Why is Hibernate expecting a separate table for my composite primary key? (@EmbeddedId)为什么 Hibernate 期望我的复合主键有一个单独的表? (@EmbeddedId)
【发布时间】:2021-10-28 00:40:34
【问题描述】:

我正在开发一个排行榜系统作为一个爱好项目。我有三个实体:

  • Player 代表排行榜上的玩家
  • StatKey 代表排行榜上某个统计数据的键
  • PlayerStat 代表给定玩家的特定统计数据,应该有一个由 PlayerStatKey 组成的复合主键,以及一个将其映射到相关 StatKey 和父 Player 实体的外键。

我对整个 JPA 感到不知所措。在创建复合主键时,我仍然不确定@EmbeddedId@IdClass 是否是我最好的选择。根据我在网上看到的,似乎最好的方法是使用@EmbeddedId

但是,这会导致一些非常奇怪的查询和表结构。我本来期望三个表:

  • 玩家(pk(id)usernametotal_leveltotal_experiencedate_createdlast_updated
  • player_stat (pk(player_id, stat_id), level, experience, date_created, last_updated)
  • statkey (pk(id), key)

还有两个外键:

  • player_stat (player_id) 引用 player
  • player_stat (stat_id) 引用 statkey

所有这些都按预期生成。但是,除了这三个表之外,还会生成第四个表:

  • player_stats (pk(player_id, stats_player_id, stats_stat_id))

以及以下外键:

  • player_stats (stats_player_id, stats_stat_id) 引用 player_stat
  • player_stats (player_id) 引用 player

奇怪的第四张表不仅是 DDL 问题,而且在尝试查询数据库时也会用到。我不知所措。任何帮助将不胜感激!

代码:

@Entity
@Table(name = "player", uniqueConstraints = @UniqueConstraint(name = "player_uk_username", columnNames = "username"))
public class Player {

    @Id
    @Column(nullable = false)
    private UUID id;

    @Column(nullable = false)
    private String username;

    @Column(name = "total_level", nullable = false)
    private int totalLevel;

    @Column(name = "total_experience", nullable = false)
    private long totalExperience;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<PlayerStat> stats;

    @Column(name = "date_created", nullable = false, updatable = false)
    private OffsetDateTime dateCreated;

    @Column(name = "last_updated", nullable = false)
    private OffsetDateTime lastUpdated;

    protected Player() {

    }

    public Player(final UUID id, final String username, final Set<PlayerStat> stats) {
        this.id = id;
        this.username = username;
        this.stats = stats;
    }

    @PrePersist
    public void prePersist() {
        this.dateCreated = OffsetDateTime.now();
        this.lastUpdated = this.dateCreated;
    }

    @PreUpdate
    public void preUpdate() {
        this.lastUpdated = OffsetDateTime.now();
    }
}
@Entity
@Table(name = "statkey", uniqueConstraints = @UniqueConstraint(name = "statkey_uk_key", columnNames = "key"))
public class StatKey {

    @Id
    @Column(nullable = false)
    private int id;

    @Column(nullable = false)
    private String key;

    @Column(name = "date_created", nullable = false, updatable = false)
    private OffsetDateTime dateCreated;

    @Column(name = "last_updated", nullable = false)
    private OffsetDateTime lastUpdated;

    protected StatKey() {

    }

    public StatKey(final int id, final String key) {
        this.id = id;
        this.key = key;
    }

    @PrePersist
    public void prePersist() {
        this.dateCreated = OffsetDateTime.now();
        this.lastUpdated = this.dateCreated;
    }

    @PreUpdate
    public void preUpdate() {
        this.lastUpdated = OffsetDateTime.now();
    }
}
@Embeddable
public class PlayerStatKey implements Serializable {

    @JsonUnwrapped(prefix = "player")
    @ManyToOne
    @JoinColumn(name = "player_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "player_stat_fk_player_id"))
    private Player player;

    @JsonUnwrapped(prefix = "stat")
    @ManyToOne
    @JoinColumn(name = "stat_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "player_stat_fk_statkey_id"))
    private StatKey statKey;

    protected PlayerStatKey() {

    }

    public PlayerStatKey(final Player player, final StatKey statKey) {
        this.player = player;
        this.statKey = statKey;
    }
}
@Entity
@Table(name = "player_stat")
public class PlayerStat {

    @EmbeddedId
    @JsonUnwrapped
    private PlayerStatKey key;

    @Column(nullable = false)
    private int level;

    @Column(nullable = false)
    private int experience;

    @Column(name = "date_created", nullable = false, updatable = false)
    private OffsetDateTime dateCreated;

    @Column(name = "last_updated", nullable = false)
    private OffsetDateTime lastUpdated;

    protected PlayerStat() {

    }

    public PlayerStat(final PlayerStatKey key, final int level, final int experience) {
        this.key = key;
        this.level = level;
        this.experience = experience;
    }
    

    @PrePersist
    public void prePersist() {
        this.dateCreated = OffsetDateTime.now();
        this.lastUpdated = this.dateCreated;
    }

    @PreUpdate
    public void preUpdate() {
        this.lastUpdated = OffsetDateTime.now();
    }
}

预期:

create table player (id binary not null, date_created timestamp not null, last_updated timestamp not null, total_experience bigint not null, total_level integer not null, username varchar(255) not null, primary key (id))
create table player_stat (date_created timestamp not null, experience integer not null, last_updated timestamp not null, level integer not null, player_id binary not null, stat_id integer not null, primary key (player_id, stat_id))
create table statkey (id integer not null, date_created timestamp not null, key varchar(255) not null, last_updated timestamp not null, primary key (id))
alter table player add constraint player_uk_username unique (username)
alter table statkey add constraint statkey_uk_key unique (key)
alter table player_stat add constraint player_stat_fk_player_id foreign key (player_id) references player
alter table player_stat add constraint player_stat_fk_statkey_id foreign key (stat_id) references statkey

现实:

create table player (id binary not null, date_created timestamp not null, last_updated timestamp not null, total_experience bigint not null, total_level integer not null, username varchar(255) not null, primary key (id))
create table player_stat (date_created timestamp not null, experience integer not null, last_updated timestamp not null, level integer not null, player_id binary not null, stat_id integer not null, primary key (player_id, stat_id))
create table player_stats (player_id binary not null, stats_player_id binary not null, stats_stat_id integer not null, primary key (player_id, stats_player_id, stats_stat_id))
create table statkey (id integer not null, date_created timestamp not null, key varchar(255) not null, last_updated timestamp not null, primary key (id))
alter table player add constraint player_uk_username unique (username)
alter table player_stats add constraint UK_9xeo0h5xwn53uq68knp5llr04 unique (stats_player_id, stats_stat_id)
alter table statkey add constraint statkey_uk_key unique (key)
alter table player_stat add constraint player_stat_fk_player_id foreign key (player_id) references player
alter table player_stat add constraint player_stat_fk_statkey_id foreign key (stat_id) references statkey
alter table player_stats add constraint FKrc4tc1jspyawwida0o2dtcp4j foreign key (stats_player_id, stats_stat_id) references player_stat
alter table player_stats add constraint FKfekdv3tvbrd0b8u6c2fuxk5fw foreign key (player_id) references player

【问题讨论】:

  • 您对 PlayerStat 使用复合主键的计划。我不知道这是否是一个问题,但总的来说,我倾向于在表中使用一维主键。是的,这两个外键一起形成了一个复合键,但是想象一下需要添加第三个外键的情况。如果从那时起前两个可能会重复,那将包括对表的完全重组。此外,如果您的两个外键可能由于任何其他原因而重复,那么您再次需要完全重新配置您的表。
  • JPA 不知道您的内部项目相关逻辑,因此,从它的行为来看,它似乎在创建表(pk,fk1,fk2)时采取了安全第一的方法。为什么不尝试将 PlayerStat 定义为 PlayerStat(pk, fk1, fk2) 而不是以 (fk1, fk2) 为主键的 PlayerStat(fk1, fk2) ?如果您有一个超出内部数据库相关逻辑范围的数字自动递增主键,那么它将与 JPA 拼命强制执行的表定义相匹配。因此,也许修复就像以 JPA 生成附加表的方式生成表一样简单。

标签: java sql hibernate jpa


【解决方案1】:

除了oa54所说的:

  1. 这与 Spring Data JPA 无关。如果您有自动生成的 SQL,我假设您(意外地)使用了 JPA 提供程序的 SQL 生成功能,可能是 Hibernate。我会相应地更新问题标题。

  2. 我强烈建议您宁愿使用专用的 SQL 迁移工具(Flyway 或 Liquibase)来编写定义数据结构的 SQL,因为无论如何您将不得不求助于这样的东西。一旦您的类需要新字段,您就需要迁移数据、更改表而不是重新创建它们等。

【讨论】:

  • 欣赏!此外,对于它的价值,虽然我绝对同意#2,但在这种情况下,奇怪的一代也反映在它所做的查询中(即它期望并需要额外的表才能运行)。跨度>
【解决方案2】:

问题似乎是缺少从父实体到子实体 PlayerStat 的映射。

解决方案是将以下内容添加到StatKey

@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "primaryKey.statKey")
private Set<PlayerStat> stats;

以及将mappedBy = "primaryKey.player" 添加到Player 中的@OneToMany 注释。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-09
    • 1970-01-01
    • 2020-12-28
    • 2015-03-18
    • 1970-01-01
    相关资源
    最近更新 更多