【问题标题】:Composite Primary Foreign Key on Spring Boot Data RepositorySpring Boot 数据存储库上的复合主外键
【发布时间】:2020-05-26 14:01:51
【问题描述】:

我正在尝试在两个实体之间建立关系,即SubscriptionDelivery。两个实体都应该使用用户的 id 作为他们的主键(即两者都与User 存在一对一的关系,但Delivery 只使用也存在于Subscription 中的 id)。 Delivery 还使用属性 email 作为附加键(与用户 ID 一起建立复合主键)。虽然我将 eclipselink 用作 spring boot 的 jpa 后端,但在包含 jpa 存储库并显示以下错误消息时,我的应用程序在此定义中似乎没有问题。

错误信息:

Caused by: java.lang.IllegalArgumentException: Expected id attribute type [class com.tnt.entity.Subscription$DeliveryPK] on the existing id attribute [SingularAttributeImpl[EntityTypeImpl@482885994:Subscription [ javaType: class com.tnt.entity.Subscription descriptor: RelationalDescriptor(com.tnt.entity.Subscription --> [DatabaseTable(tbl_subscription)]), mappings: 6],org.eclipse.persistence.mappings.ManyToOneMapping[subscription]]] on the identifiable type [EntityTypeImpl@1615668218:Delivery [ javaType: class com.tnt.entity.Subscription$Delivery descriptor: RelationalDescriptor(com.tnt.entity.Subscription$Delivery --> [DatabaseTable(tbl_subscription_delivery)]), mappings: 2]] but found attribute type [class com.tnt.entity.Subscription].
    at org.eclipse.persistence.internal.jpa.metamodel.IdentifiableTypeImpl.getId(IdentifiableTypeImpl.java:204) ~[org.eclipse.persistence.jpa-2.7.0.jar:na]
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation$IdMetadata.<init>(JpaMetamodelEntityInformation.java:262) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:88) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getEntityInformation(JpaEntityInformationSupport.java:66) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:211) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:161) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:144) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:69) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:312) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:297) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.util.Lazy.getNullable(Lazy.java:212) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.util.Lazy.get(Lazy.java:94) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:300) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:121) ~[spring-data-jpa-2.2.3.RELEASE.jar:2.2.3.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1792) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    ... 88 common frames omitted

实体:

package com.tnt.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;

@Entity
@Table(name = "tbl_subscription")
public class Subscription {
    @Column(name = "user_id")
    @Id
    private Long id;

    @Column(name = "daily_report")
    @Basic
    private boolean receiveDailyReport;

    @Column(name = "weekly_report")
    @Basic
    private boolean receiveWeeklyReport;

    @Column(name = "monthly_report")
    @Basic
    private boolean receiveMonthlyReport;

    @Column(name = "multi_report")
    @Basic
    private boolean multiReport;

    @PrimaryKeyJoinColumn(name = "user_id")
    @OneToOne(optional = false)
    @JsonIgnore
    private User user;

    public Subscription() {

    }

    public Subscription(User user){
        this.user = user;
        this.receiveDailyReport = true;
    }

    @Entity
    @Table(name = "tbl_subscription_delivery")
    @IdClass(DeliveryPK.class)
    public static class Delivery {
        @JoinColumn(name = "subscription_id")
        @ManyToOne
        @Id
        private Subscription subscription;

        @Column(name = "email")
        @Id
        private String email;

        public Delivery() {

        }

        public Delivery(Subscription subscription, String email){
            this.subscription = subscription;
            this.email = email;
        }

        public Subscription getSubscription() {
            return subscription;
        }

        public void setSubscription(Subscription subscription) {
            this.subscription = subscription;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }

        // ... equals, hashCode
    }

    public static class DeliveryPK implements Serializable {

        private Long subscription;

        private String email;

        public DeliveryPK() {
        }

        public Long getSubscription() {
            return subscription;
        }

        public void setSubscription(Long subscription) {
            this.subscription = subscription;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }

        // ... equals, hashCode
    }

    // ... getter, setter, equals, hashCode
}

存储库接口:

interface DeliveryRepository : JpaRepository<Delivery, DeliveryPK> {
    @Query("SELECT d FROM Delivery d WHERE d.subscription = :subscription AND d.email = :email")
    fun findByEmail(subscription: Subscription, email: String): Optional<Delivery>
}

有人知道我做错了什么吗?

编辑:这个问题也可以用不同的方式提出——如果我在 SQL 中对此进行建模,它可能看起来像这样:

CREATE TABLE tbl_user (
    id BIGINT NOT NULL PRIMARY KEY,
    email VARCHAR(255) UNIQUE,
    password VARCHAR(255),
    salt VARCHAR(64)
);

CREATE TABLE tbl_subscription (
    user_id BIGINT NOT NULL REFERENCES tbl_user(id),
    daily_report BOOLEAN,
    weekly_report BOOLEAN,
    monthly_report BOOLEAN,
    multi_report BOOLEAN,
    PRIMARY KEY (user_id)
);

CREATE TABLE tbl_subscription_delivery (
    subscription_id BIGINT NOT NULL REFERENCES tbl_subscription(user_id),
    email VARCHAR(255),
    PRIMARY KEY(email, subscription_id)
);

如何在 JPA 2.0 中对这种行为进行建模?

【问题讨论】:

  • 您使用的是什么版本的 JPA?关系上的 PrimaryKeyJoinColumn 是 v1 要做的事情,它被 MapsId 注释 (eclipse.org/eclipselink/api/2.6/index.html?javax/persistence/…) 取代,这使得更清楚哪个映射控制该字段。您可以尝试将这些实体和 Pk 类放入它们自己的 java 类文件中,因为它会以某种方式混淆 PK 类和订阅类。
  • 我使用的是 Eclipselink 2.7 版。我已经对 MapsId 进行了同样的尝试,但不幸的是收到了另一条错误消息:java.lang.ClassCastException: org.eclipse.persistence.internal.jpa.metadata.accessors.mappings.IdAccessor cannot be cast to org.eclipse.persistence.internal.jpa.metadata.accessors.mappings.ObjectAccessor
  • 不管你信不信,但是将类分成单独的文件确实解决了我的问题
  • EclipseLink 中的注释处理出了点问题,以奇怪的不同方式出现,但通常围绕主键旋转。这是我的大部分评论,我只是将 mapsID 作为旁注加入 - 它确实会让你的代码更容易 IMO。使用 primarykeyjoin 列,我不知道有人真正意识到哪个映射实际设置了数据库中的值,并且在很多情况下它会导致混淆,因为 subscription.id != subscription.user.id 除非您自己修复问题.
  • Brian Vosburgh 的代码是我所建议的,应该对你有用(现在你已经将你的类分解成单独的文件)。如果不是,请发布另一个问题,其中包含正在使用的代码和异常。 MapsId 是 primarykeyjoincolumn hack 的直接替代品,允许您将 id 字段映射为基本字段并作为参考进行访问。我通常只是将关系标记为 ID 并取消基本映射,但在某些情况下我需要访问 ID 字符串并且不想强制获取整个引用对象。

标签: java jpa spring-data-jpa eclipselink


【解决方案1】:

您可以将Subscription 映射到单个主键关系属性:

@Entity
@Table(name = "tbl_subscription")
public class Subscription {
    @Id
    @OneToOne(optional = false)
    @JoinColumn(name = "user_id")
    private User user;

    @Column(name = "daily_report")
    @Basic
    private boolean receiveDailyReport;

    @Column(name = "weekly_report")
    @Basic
    private boolean receiveWeeklyReport;

    @Column(name = "monthly_report")
    @Basic
    private boolean receiveMonthlyReport;

    @Column(name = "multi_report")
    @Basic
    private boolean multiReport;
...

或者,您可以将Subscription@MapsId 映射(希望这与您在上面的评论中所说的不完全相同,您已经尝试过:)):

@Entity
@Table(name = "tbl_subscription")
public class Subscription {
    @Id
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id")
    @MapsId
    private User user;

    @Column(name = "daily_report")
    @Basic
    private boolean receiveDailyReport;

    @Column(name = "weekly_report")
    @Basic
    private boolean receiveWeeklyReport;

    @Column(name = "monthly_report")
    @Basic
    private boolean receiveMonthlyReport;

    @Column(name = "multi_report")
    @Basic
    private boolean multiReport;
...

【讨论】:

  • 我明白你的意思,但问题是,订阅实体似乎已经在工作了。导致问题的原因是 Delivery 类。但同样,如果我插入 2 个相同的条目(但它也没有插入它们)它不会崩溃(即抛出完整性约束违规异常),它现在正在工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-25
  • 2020-07-13
  • 2020-04-18
相关资源
最近更新 更多