简体   繁体   中英

Persisting multiple associations to same entity in Hibernate

I am making a chat system, and I have the below database schema (everything not related to the core problem has been removed).

数据库架构

A thread represents a conversation between two participants. When a new thread is created (persisted), two participants should be created; one for the sender and one for the receiver (a message is added to the thread, but this is not relevant in this case). So I have mapped the two database tables to two entities.

@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
}

From the Thread entity, the participants association should contain all participants in the thread, while the sender and receiver contain references to the sender and receiver, respectively, for convenience. Thus, only two participants should be persisted in the database. Here is the code that I wrote to persist a new thread:

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

This throws the below exception.

org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : com.example.thread.entity.Participant.thread -> com.example.thread.entity.Thread

I tried many variations of the cascade attribute on both entities, but the same exception is thrown in all cases (although with different transient properties). Logically speaking, the approach should not be problematic, as all that has to happen is that the Thread entity is persisted first in order for the participants to obtain the generated ID, before they should be persisted themselves.

Do I have a problem with my mapping, or what is the problem? Thank you!

Okay, it turns out that the problem was caused by something as obvious as my database constraints, as pointed out by @AlanHay. I removed the not-null constraints on the sender_id and receiver_id columns, and updated the code to the following.

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

Well, that was embarrassing, wasn't it? ;-) It happens!

A more detailed answer based on my comment, which was requested as an answer.

Trying to solve problem with different cascading types, joincolumns and other hibernate annotations. Is usually the wrong way to solve a problem like that. Especially cascading should be used in a different way than you did and you should remove it from this mapping for now. Always see the semantic/meaning of a model in terms of usecases you wana accomplish with it. Cascading a participant with a thread is probably not what you want, because a participant will in most cases created and maintained indepedent of a thread. So don't try to simplify the save process of a single entity with the usage of cascade without an actual business case behind it.

I advise you to split it up and create/save the Thread object independent of participants. It might be a few lines of code more, but they are easier to understand in the long run and better maintainable.

As advised above the original issue was caused by the circular dependency between Thread and Participant so that insert into Thread needs sender_id and receiver_id to be available: however the insert to participant needs thread_id to be available.

While making the FK columns in Thread nullable solves your issue the schema does not seem quite right. For example, in the absence of any database trigger, it would seem to be possible to break referential integrity by setting the FK sender_id or receiver_id for a Thread to point to a participant without a corresponding record in the Participant table for that particular Thread.

A better approach may then be to add an additional column to the Participant table participant_type (sender, receiver, other) and remove the FK columns from Thread. If the number of participants for a Thread will always be small then you could simply iterate the collection in memory to get the sender and receiver according to the type.

@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
    }
}

If the number of participants will be large then to avoid loading all participants to get the sender and receiver then I think an alternative (and I haven't tested this) would be sub-classing and using a discriminator column which would look something like:

@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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM