如何正确映射主键由两个外键组成的实体,其中一个外键本身是复合的?

How do I properly map entities where a primary key is composed of two foreign keys, one of which is composite itself?

我很难弄清楚如何在特定数据库设计上正确执行 ORM。

我的架构由三个 table 组成:用户 table、评论 table 和投票 table。用户可以发布专辑评论,他们还可以为任何评论指定正面或负面评级。相册由外部 API 提供,因此架构中缺少它们的 table,但引用了它们的 ID。

用户主键只包含他们的用户名。评论主键由评论者的用户名(外键)和评论的相册 ID 组成。最后,投票主键由投票者的用户名、外键和投票的评论主键组成,如前所述,由评论者的用户名和评论的相册 ID 组成。

用户可以为每个单独的专辑发表评论,也可以为每个单独的评论投票。

这是表示架构的 ER 模型:

为了映射实体 ID,我使用了 @IdClass 注释,但我不确定我的方向是否正确。我也尝试使用 @EmbeddedId 注释,但结果是一样的。

这是我的实体 class 目前的样子:

@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1;

    @Id @Column(name = "username")
    private String username;
    @Column(unique = true, nullable = false)
    private String email;
    @Column(name = "password", nullable = false)
    private String password;
    @Temporal(TemporalType.TIMESTAMP) @Column(name="signUpDate", nullable = false)
    private Date signUpDate;

    // empty constructor, getters, setters, equals and hashCode implementations

}
@Entity @IdClass(ReviewId.class)
public class Review implements Serializable {

    private static final long serialVersionUID = 1;

    @Id @ManyToOne @JoinColumn(name = "reviewerUsername", referencedColumnName = "username")
    private User reviewer;
    @Id @Column(name = "reviewedAlbumId")
    private Long reviewedAlbumId;
    @Column(name = "content", nullable = false, length = 2500)
    private String content;
    @Column(name = "rating", nullable = false)
    private Integer rating;
    @Temporal(TemporalType.TIMESTAMP) @Column(name = "publicationDate", nullable = false)
    private Date publicationDate;

    // empty constructor, getters, setters, equals and hashCode implementations

}
@Entity @IdClass(VoteId.class)
public class Vote implements Serializable {

    private static final long serialVersionUID = 1;

    @Id @ManyToOne @JoinColumn(name = "voterUsername", referencedColumnName = "username")
    private User voter;
    @Id @ManyToOne @JoinColumns({
            @JoinColumn(name = "reviewerUsername", referencedColumnName = "reviewerUsername"),
            @JoinColumn(name = "reviewedAlbumId", referencedColumnName = "reviewedAlbumId")
    })
    private Review review;
    @Column(name = "vote") // @todo add attribute nullable = false
    private Boolean vote;

    // empty constructor, getters, setters, equals and hashCode implementations

}

这些是我的 ID classes:

public class ReviewId implements Serializable {

    private static final long serialVersionUID = 1L;

    private User reviewer;
    private Long reviewedAlbumId;

    // empty constructor, getters, setters, equals and hashCode implementations

}
public static class VoteId implements Serializable {

    private static final long serialVersionUID = 1L;

    private User voter;
    private Review review;

    // empty constructor, getters, setters, equals and hashCode implementations

}

下面是用于生成模式的 MySQL 脚本的内容:

DROP SCHEMA IF EXISTS albumReviewsDatabase;
CREATE SCHEMA albumReviewsDatabase;
USE albumReviewsDatabase;

CREATE TABLE user (
    username VARCHAR(20) PRIMARY KEY,
    email VARCHAR(254) NOT NULL UNIQUE,
    password CHAR(60) NOT NULL,
    signUpDate TIMESTAMP NOT NULL DEFAULT now()
) ENGINE = INNODB;

CREATE TABLE review (
    reviewerUsername VARCHAR(20) NOT NULL,
    reviewedAlbumId BIGINT(20) NOT NULL,
    content TEXT NOT NULL,
    rating SMALLINT UNSIGNED NOT NULL,
    publicationDate TIMESTAMP NOT NULL DEFAULT now(),
    CHECK (rating >= 0 AND rating <= 10),
    PRIMARY KEY (reviewerUsername, reviewedAlbumId),
    FOREIGN KEY (reviewerUsername) REFERENCES user(username)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE = INNODB;

CREATE TABLE vote (
    voterUsername VARCHAR(20) NOT NULL,
    reviewerUsername VARCHAR(20) NOT NULL,
    reviewedAlbumId BIGINT(20) NOT NULL,
    vote BOOLEAN NOT NULL,
    PRIMARY KEY (voterUsername, reviewerUsername, reviewedAlbumId),
    FOREIGN KEY (voterUsername) REFERENCES user(username)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    FOREIGN KEY (reviewerUsername, reviewedAlbumId) REFERENCES review(reviewerUsername, reviewedAlbumId)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE = INNODB;

我目前在 TomEE webprofile 实例上使用 OpenJPA 作为持久性提供程序,使用的 JPA 版本是 2.0。

很明显,我误解了 JPA 的 ORM,因为当我部署包含这些实体的应用程序时,出现以下异常:

<openjpa-2.4.2-r422266:1777108 fatal user error> org.apache.openjpa.util.MetaDataException: The id class specified by type "class application.model.Review" does not match the primary key fields of the class.  Make sure your identity class has the same primary keys as your persistent type, including pk field types. Mismatched property: "reviewer"

抛出异常是因为 Review class 映射,而不是 Vote class;但是,我确信通过解决 Review class 上的问题,Vote.

也会出现同样的问题

我宁愿使用 @IdClass 注释而不是 @EmbeddedId,但我最终使用的两者都不是问题。

这些关系是"derived identities";所以你的 ID 类 应该是这样的(注意外键字段的类型与其对应的实体字段的类型不同):

public class ReviewId implements Serializable {

    private static final long serialVersionUID = 1L;

    private String reviewer; // matches name of @Id attribute and type of User PK
    private Long reviewedAlbumId;

    // ...

}
public static class VoteId implements Serializable {

    private static final long serialVersionUID = 1L;

    private String voter; // matches name of @Id attribute and type of User PK
    private ReviewId review; // matches name of @Id attribute and type of Review PK

    // ...

}

派生身份在第 2.4.1 节的 JPA 2.2 spec 中讨论(通过示例)。

此外,作为旁注,@IdClass 有点老套,而 @EmbeddedId 更简洁,消除了实体及其键中的重复代码。