1

I am using PostgreSQL 12.11, JPA 3.1.0, and Hibernate 5.6.10. This might become important because I am doing things that apparently do not work with JPA 2.0.

My goal is to add an attribute to a many-to-many relationship. I found this posting. @Mikko Maunu states that "There is no concept of having additional persistent attribute in relation in JPA (2.0)." To me, this sounds like what I want to do is not possible. However, the answer is rather old and might not be complete anymore.

Beside the time gap and the version gap, this is, in my opinion, a new question because I am doing something that is probably questionable and not part of the original thread.

What I did is this:

  1. Create a @ManyToMany relationship in JPA and specify a @JoinTable.
  2. Manually define an entity with identical table name to the table specified in 1. For this table, I chose a composite primary key using @IdClass. I also added my attribute.
  3. Inside one of the n:m-connected entities, create a @OneToMany relationship to the connection-table-entity created in 2. However, I did not create a corresponding @ManyToOne relationship as that would have created an error.

As a result, I can access the original entities and their relation as many-to-many, but also the relation itself, which is not an entity in the original ERM, but it is for JPA. First tests show this seems to be working.

I am aware, however, that I basically access the same part of the persistence (the PostgreSQL database) through two different ways at the same time.

Now my questions are:

  1. Is this a valid way to do it? Or will I get in bad trouble at one point?
  2. Is there a situation where I will need to refresh to prevent trouble?
  3. Is this something new in JPA > 2.0, or just an extension to the original answer?
Tim Lammarsch
  • 161
  • 1
  • 1
  • 10
  • You should show your entities, but it sounds like you've mapped the same relationship in two ways - treating the relational table as a relationship AND as an entity. Why? Why not have your classes setup to use a 1:M to the new entity? You will need to be able to access and set attributes on the 'relationship' entries, but you can write accessors that only return a list of the referenced entity (like a M:M), so your model usage doesn't have to change. If you do both though, you will have inconsistencies and problems keeping them in synch, and not overwriting each other. – Chris Aug 10 '22 at 14:50
  • I already suspected as much. As to why, I probably "hurt" from having an n:m relationship and not modelling it in the entity at all. However, after some testing, I have to agree that modelling it twice does create more problems than solving anything. I think I got what you are proposing, and I will report if that works for me. – Tim Lammarsch Aug 10 '22 at 15:26

1 Answers1

0

This should help.

Here is how I do it:

@Entity
@Table(name = "person", schema = "crm")
public final class Person implements Serializable {
  @Id
  @Column(name = "id", unique = true, nullable = false, updatable = false, columnDefinition = "bigserial")
  private Long id;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person", orphanRemoval = true)
  private Set<PersonEmail> emails = new HashSet<>();
}
@Entity
@Table(name = "email", schema = "crm")
public final class Email implements Serializable {
  @Id
  @Column(name = "id", unique = true, nullable = false, updatable = false, columnDefinition = "bigserial")
  private Long id;

  @Column(name = "email", nullable = false, length = 64, columnDefinition = "varchar(64)")
  private String localPart;

  @Column(name = "domain", nullable = false, length = 255, columnDefinition = "varchar(255)")
  private String domain;
}
@Entity
@Table(name = "person_email", schema = "crm")
public final class PersonEmail implements Serializable {
  @EmbeddedId
  private PersonEmailId id;

  // The mapped objects are fetched lazily.
  // This is a choice.
  @ToString.Exclude
  @MapsId("personId")
  @ManyToOne(fetch = FetchType.LAZY, optional = false)
  private Person person;

  @ToString.Exclude
  @MapsId("emailId")
  @ManyToOne(fetch = FetchType.LAZY, optional = false)
  private Email email;

  // Here's an extra column.
  @Column(name = "type", nullable = false, columnDefinition = "email_type_t")
  @Convert(converter = EmailType.EmailTypeConverter.class)
  private EmailType type;

  public final void setPerson(final Person person) {
    this.person = person;

    id.setPersonId(this.person.getId());
  }

  public final void setEmail(final Email email) {
    this.email = email;

    id.setEmailId(this.email.getId());
  }

  @Embeddable
  public static final class PersonEmailId implements Serializable {
    @Column(name = "person_id", nullable = false, insertable = false, updatable = false, columnDefinition = "bigint")
    private Long personId;

    @Column(name = "email_id", nullable = false, insertable = false, updatable = false, columnDefinition = "bigint")
    private Long emailId;
}
Oliver
  • 1,465
  • 4
  • 17
  • Thank you to both helpers so far. I think I am currently on the right track, e.g., I learned about `@MapsId`. I am trying to adapt my own implementation based on the example. Currently, I am still working out how to add new data. Doing it with Cascade seems to create a wrong order, of course a n:m-relation can only be added once the related entities both have the new elements. Sadly, I will be occupied with other things for a few days, but I will definitely return to this next week. – Tim Lammarsch Aug 11 '22 at 09:06
  • What do you mean, "wrong order"? Hibernate does not guarantee table column ordering. – Oliver Aug 13 '22 at 00:24
  • I am back on it, what I could do so far was validate my results from earlier this week. I am trying to explain it in a more verbose way in the next post. – Tim Lammarsch Aug 14 '22 at 18:07
  • I can create two instances, one for each of the related entities, and persist them. Then I can create an instance of the n:m-Relation-Entity and also persist it. So far, this seems to work fine. I was under the impression, that the following should work: I create everything in memory, and persist one part of it. Cascade looks through the relations, and persists everything. However, it does seem to persist the relation-entity before both entities are persisted. Thus, one FK which is part of the PK is null, and that is what I meant with "wrong order". – Tim Lammarsch Aug 14 '22 at 18:11
  • 1
    I have marked the answer by @Oliver as solution as it conveys the basic idea of what I am doing now, and it answers the original question. The main difference: I have changed the CascadeType.ALL to CascadeType.PERSIST because it cascades correctly that way. I would not want CascadeType.ALL anyway to be on the safe side regarding accidential pruning. The alternative would be persisting everything manually and not cascading at all. Thank you again. – Tim Lammarsch Aug 15 '22 at 15:16