Java 在Hibernate中将多个关联持久化到同一实体

Java 在Hibernate中将多个关联持久化到同一实体,java,hibernate,jpa,orm,Java,Hibernate,Jpa,Orm,我正在做一个聊天系统,我有下面的数据库模式,所有与核心问题无关的东西都被删除了 线程表示两个参与者之间的对话。创建新线程时,应创建两个参与者;一个用于发送方,一个用于接收方,一条消息被添加到线程中,但这在本例中并不相关。因此,我将两个数据库表映射到两个实体 @Entity @Table(name = "participant") public class Participant { @Id @GeneratedValue(strategy = GenerationType.IDE

我正在做一个聊天系统,我有下面的数据库模式,所有与核心问题无关的东西都被删除了

线程表示两个参与者之间的对话。创建新线程时,应创建两个参与者;一个用于发送方,一个用于接收方,一条消息被添加到线程中,但这在本例中并不相关。因此,我将两个数据库表映射到两个实体

@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
}
这将抛出以下异常

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

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


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

好吧,正如@AlanHay所指出的,这个问题是由一些显而易见的原因造成的,比如我的数据库约束。我删除了sender\u id和receiver\u id列上的NOTNULL约束,并将代码更新为以下内容

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

嗯,那很尴尬,不是吗它发生了

根据我的评论,一个更详细的回答,作为一个回答

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


我建议您将其拆分,并独立于参与者创建/保存线程对象。可能还有几行代码,但从长远来看,它们更容易理解,也更易于维护。

如上所述,原始问题是由线程和参与者之间的循环依赖性造成的,因此插入线程需要发送者id和接收者id可用:但是插入参与者需要线程id可用

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

一个更好的方法可能是在参与者表中添加一个额外的列,即参与者类型sender、receiver和other,并从线程中删除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;
}

…必须首先持久化线程实体,以便参与者获得生成的ID。。。。然而,线程本身对发送方和接收方不是都有不可为空的FK吗?由于您有一个循环关系,因此肯定两者都不能持久化:insert to T需要发送者和接收者id可用。然而,insert to participant需要线程id可用。@AlanHay您完全正确!我不知道我怎么没有想到这一点。想到的第一个解决方案是允许接收方和发送方为空,将线程与参与者一起持久化,然后更新线程。这意味着我必须从数据库中删除NOTNULL约束,并在代码中强制执行该约束。考虑到逻辑被包装在服务中,这并不太糟糕,因为它降低了出错的风险。但也许有更好的办法?不管怎样,谢谢你的提醒。我能说的只有d'oh!:-退一步,从业务逻辑的角度看你在做什么。在现实中是否存在这样一个用例:参与者将与线程同时创建。我个人不这么认为。所以spl
它会将该线程初始化并创建线程。然后创建参与者并将其添加到线程中。不要试图通过层叠来简化。始终将cascade属性视为语义,并思考模型对象是如何交互的。而且不是为了方便保存操作。@mh-dev您也完全正确!我采用了你的方法,更新后的代码很有效。请随意添加答案。谢谢-另一种选择可能是从线程中删除FK列,使用参与者表中的鉴别器列创建Particpant的Sender和Receiver子类,并将它们映射到线程中。在模式级别上,这似乎是一个“更干净”的解决方案。哦,这是一个非常有趣的方法,我非常喜欢它。非常感谢你的帮助,我真的很感激!如果你想补充这一点作为回答,我很乐意投你一票-
@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;
}