【问题标题】:Persisting multiple associations to same entity in Hibernate在 Hibernate 中保持与同一实体的多个关联
【发布时间】:2015-05-11 14:14:41
【问题描述】:

我正在制作一个聊天系统,并且我有以下数据库架构(与核心问题无关的所有内容都已删除)。

线程代表两个参与者之间的对话。当一个新线程被创建(持久化)时,应该创建两个参与者;一个用于发送者,一个用于接收者(一条消息被添加到线程中,但在这种情况下不相关)。所以我已经将两个数据库表映射到两个实体。

@Entity
@Table(name = "participant")
public class Participant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private int id;

    @ManyToOne(fetch = FetchType.LAZY, targetEntity = Thread.class, optional = false)
    @JoinColumn(name = "thread_id")
    private Thread thread;

    // Getters and setters
}

@Entity
@Table(name = "thread")
public class Thread {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private int id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "thread", targetEntity = Participant.class, cascade = CascadeType.ALL)
    private Set<Participant> participants = new HashSet<>();

    @ManyToOne(fetch = FetchType.LAZY, targetEntity = Participant.class, cascade = CascadeType.ALL, optional = false)
    @JoinColumn(name = "sender_id")
    private Participant sender;

    @ManyToOne(fetch = FetchType.LAZY, targetEntity = Participant.class, cascade = CascadeType.ALL, optional = false)
    @JoinColumn(name = "receiver_id")
    private Participant receiver;

    // Getters and setters
}

Thread 实体中,participants 关联应包含线程中的所有参与者,而senderreceiver 为方便起见分别包含对发送者和接收者的引用。因此,只有两个参与者应该被持久化在数据库中。这是我为持久化一个新线程而编写的代码:

Thread thread = new Thread();

Participant sender = new Participant();
sender.setThread(thread);

Participant receiver = new Participant();
receiver.setThread(thread);

thread.setSubject(subject);
thread.setSender(sender);
thread.setReceiver(receiver);

Set<Participant> participants = new HashSet<>(2);
participants.add(sender);
participants.add(receiver);
thread.setParticipants(participants);

Thread saved = this.threadRepository.save(thread);

这会引发以下异常。

org.hibernate.TransientPropertyValueException:非空属性 引用瞬态值 - 瞬态实例必须先保存 当前操作:com.example.thread.entity.Participant.thread -> com.example.thread.entity.Thread

我在两个实体上尝试了cascade 属性的许多变体,但在所有情况下都会抛出相同的异常(尽管具有不同的瞬态属性)。从逻辑上讲,该方法应该没有问题,因为必须首先持久化 Thread 实体,以便参与者获得生成的 ID,然后再将它们自己持久化。

我的映射是否有问题,或者是什么问题?谢谢!

【问题讨论】:

  • "...所要做的就是首先持久化 Thread 实体,以便参与者获得生成的 ID..."。但是,Thread 本身对发送方和接收方都没有不可为空的 FK 吗?由于您有循环关系,因此肯定两者都不能持久:插入 T 需要 sender_id 和 receiver_id 可用。:但是插入参与者需要 thread_id 可用。
  • @AlanHay 你完全正确!我知道我是怎么没想到的。想到的第一个解决方案是允许接收者和发送者为 NULL,将线程与参与者一起保留,然后更新线程。这意味着我必须从我的数据库中删除我的非空约束并在我的代码中强制执行这个约束。鉴于逻辑包含在服务中,这并不算太糟糕,因为它降低了出错的风险。但也许有更好的方法?不管怎样,谢谢你的提醒。我只能说“哦!” :-)
  • 退后一步,从业务逻辑的角度来看你在做什么。在现实中是否存在参与者将与线程同时创建的用例。我个人不这么认为。因此,将其拆分并创建线程。然后创建参与者并将他们添加到线程。不要尝试级联以使其更容易。始终将级联属性视为对模型对象如何交互的语义思考。而且不是为了方便的保存操作。
  • @mh-dev 你也完全正确!我采用了您的方法,并且更新的代码有效。随意添加答案。谢谢! :-)
  • 加了一点糖作为答案。

标签: java hibernate jpa orm


【解决方案1】:

根据我的评论提供更详细的答案,要求作为答案。

尝试使用不同的级联类型、连接列和其他休眠注释来解决问题。通常是解决此类问题的错误方法。特别是级联应该以不同的方式使用,你现在应该从这个映射中删除它。始终根据您想用它完成的用例来查看模型的语义/含义。将参与者与线程级联可能不是您想要的,因为在大多数情况下,参与者将独立于线程创建和维护。因此,不要试图在没有实际业务案例的情况下使用级联来简化单个实体的保存过程。

我建议您将其拆分并独立于参与者创建/保存 Thr​​ead 对象。可能会多出几行代码,但从长远来看它们更容易理解并且可维护性更好。

【讨论】:

    【解决方案2】:

    好的,正如@AlanHay 所指出的那样,问题是由我的数据库限制这样明显的东西引起的。我删除了sender_idreceiver_id 列上的非空约束,并将代码更新为以下内容。

    /** Create thread **/
    Thread thread = new Thread();
    thread.setSubject(subject);
    Thread savedThread = this.threadRepository.save(thread);
    
    
    /** Save participants **/
    Participant sender = new Participant();
    senderParticipant.setThread(savedThread);
    
    Participant receiver = new Participant();
    companyParticipant.setThread(savedThread);
    
    this.participantRepository.save(sender);
    this.participantRepository.save(receiver);
    
    
    /** Add participants to thread **/
    savedThread.setSender(sender);
    savedThread.setReceiver(receiver);
    
    Set<Participant> participants = new HashSet<>(2);
    participants.add(sender);
    participants.add(receiver);
    savedThread.setParticipants(participants);
    this.threadRepository.save(savedThread);
    

    嗯,这很尴尬,不是吗? ;-) 它发生了!

    【讨论】:

    • 另一种选择可能是从 Thread 中删除 FK 列,使用参与者表中的鉴别器列创建 Particpant 的 Sender 和 Receiver 子类,并将它们映射到 Thread 中。这似乎是架构级别的“更清洁”解决方案。
    • 哦,这是一个非常有趣的方法,我非常喜欢它。非常感谢您非常的帮助,非常感谢!如果您想将其添加为答案,我很乐意为您投票。 :-)
    【解决方案3】:

    如上所述,最初的问题是由 Thread 和 Participant 之间的循环依赖引起的,因此插入 Thread 需要 sender_id 和 receiver_id 可用:但是插入参与者需要 thread_id 可用。

    虽然使 Thread 中的 FK 列可以为空可以解决您的问题,但架构似乎并不完全正确。例如,在没有任何数据库触发器的情况下,似乎可以通过将线程的 FK sender_id 或 receiver_id 设置为指向参与者而在该特定线程的参与者表中没有相应记录来破坏参照完整性。

    然后,更好的方法可能是向参与者表参与者类型(发送者、接收者、其他)添加一个额外的列,并从线程中删除 FK 列。如果线程的参与者数量总是很少,那么您可以简单地在内存中迭代集合以根据类型获取发送者和接收者。

    @Entity
    @Table(name = "thread")
    public class Thread {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column
        private int id;
    
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "thread", cascade = CascadeType.ALL)
        private Set<Participant> participants = new HashSet<>();
    
    
        public Participant getSender(){
        //iterate and find
        }
    
        public Participant getReceiver(){
        //iterate and find
        }
    }
    

    如果参与者的数量很大,那么为了避免加载所有参与者来获取发送者和接收者,那么我认为另一种选择(我还没有测试过)将是子分类并使用一个看起来像的鉴别器列类似:

    @DiscriminatorColumn(name="participant_type")
    public class Participant {
    
    
    }
    
    @DiscriminatorValue("S")
    public class Sender extends Participant{
    
    
    }
    
    @DiscriminatorValue("R")
    public class Receiver extends Participant{
    
    
    }
    
    @Entity
    @Table(name = "thread")
    public class Thread {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column
        private int id;
    
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "thread", cascade = CascadeType.ALL)
        //@WhereTable("....") //exclude sender and receiver ????
        private Set<Participant> participants = new HashSet<>();
    
        @ManyToOne
        @JoinTable(name="participant", joinColumns=@JoinColumn(name="thread_id"), inverseJoinColumns=@JoinColumn(name="participant_id"))
        private Sender sender;
    
        @ManyToOne
        @JoinTable(name="participant", joinColumns=@JoinColumn(name="thread_id"), inverseJoinColumns=@JoinColumn(name="participant_id"))
        private Receiver receiver;
    }
    

    【讨论】:

      猜你喜欢
      • 2010-12-11
      • 2015-01-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-23
      • 1970-01-01
      相关资源
      最近更新 更多