0

I had previously modelled a ManyToMany relation in JPA but now I had to make it OneToMany and ManyToOne. I had some input from a friend but now I am unable to save the join table correctly.

These are my entities:

Label:

public class Label implements Serializable {

    @Id
    @GeneratedValue()
    @Column(updatable = false, nullable = false, columnDefinition = "BINARY(16)")
    private UUID id;

    // BEFORE
    // @OneToMany
    @OneToMany(mappedBy = "technology")
    private Set<TechnologyLabel> technology;

    //getters setters equal hashcode
}

Technology:

public class Technology implements Serializable {

    @Id
    @GeneratedValue
    @Column(updatable = false, nullable = false, columnDefinition = "BINARY(16)")
    private UUID uuid;

    @Column(nullable = false, length = 30)
    private String technologyName;

    // BEFORE
    // @OneToMany
    @OneToMany(mappedBy = "label")
    private Set<TechnologyLabel> labels;

    //getters setters equal hashcode
}

TechnologyLabel:

public class TechnologyLabel {

    @Id
    @EqualsAndHashCode.Include
    UUID technologyId;

    @Id
    @EqualsAndHashCode.Include
    @Column(name = "label__id")
    UUID labelId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(insertable = false, updatable = false)
    Technology technology;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(insertable = false, updatable = false)
    Label label;

    //getters setters equal hashcode

    @Data
    public static class PK implements Serializable {

        UUID technologyId;

        UUID labelId;
    }
}

My issues is in how to save TechnologyLabel. What I do is:

  1. create label and save it in DB
  2. create technology and save it in DB
  3. create technologyLabel add label and technology and save it in DB

an example of step 3 is:

var technologyLabelList = new ArrayList<TechnologyLabel>();
    for (var t : technologies) {
        for (var l : labels) {
            var technologyLabel = new TechnologyLabel();
            technologyLabel.setTechnology(t);
            technologyLabel.setLabelId(l.getId());
            technologyLabel.setTechnologyId(t.getUuid());
            technologyLabel.setLabel(l);
            technologyLabelList.add(technologyLabel);
        }
    }
technologyLabelRepository.saveAll(technologyLabelList);

like this my program runs and the table is created. The issue I have is that I have to manually set the labelId and technologyId. The resulting TechnologyLabel table looks weird to me though as it has the IDs twice and once empty.

enter image description here

I had to set the name of the column for the labelId to label__id or I would get the following error:

Caused by: org.hibernate.DuplicateMappingException: Table [technology_label] contains physical column name [label_id] referred to by multiple logical column names: [label_id], [labelId]

spozzi
  • 65
  • 5
  • Why did you set it to be a unidirectional OneToMany, and then a separate read-only ManyToOne? See tutorials on bidirectional OneToManys for how you might do it, as the current approach won't set your FKs. The error though is because you've unmanaged Technology instances you've associated to your TechLabel and passed to save - JPA is required to throw an error if they are unmanaged. Read in the labels/tech instances from the context before setting them in your new TechLable instances to have it use the managed instances, or set the mappings to cascade merge the data. – Chris Nov 07 '22 at 19:40
  • Moreover, in the `TechnologyLabel`, there is no need to add `technologyId` and `labelId` since you already chose to have the corresponding `manyToOne`relations. – Pierre Demeestere Nov 08 '22 at 18:56
  • @Chris thx for the help. I believed I managed to make it bidirectional but I still think I have some issues with the code (I edited the code and the end of the post to reflect that) – spozzi Nov 09 '22 at 00:41
  • @PierreDemeestere the problem is that if I do not set them myself I get an error: -->Caused by: java.sql.SQLIntegrityConstraintViolationException: (conn=170) Column 'label__id' cannot be null – spozzi Nov 09 '22 at 00:44
  • @spozzi, I think that you don't need them at all. Could you try to remove these attribute in the `TechnologyLabel` class ? – Pierre Demeestere Nov 09 '22 at 07:30
  • @PierreDemeestere if I remove them I get: --> Caused by: org.hibernate.AnnotationException: No identifier specified for entity: com.example.demo.entity.TechnologyLabel – spozzi Nov 09 '22 at 11:28
  • @spozzi, you need to specify how the `id` is generated with `@GeneratedValue` as you did for the other entities for example. – Pierre Demeestere Nov 09 '22 at 11:37
  • @PierreDemeestere but if I use generated values the `IDs` will be different in the table `TechnologyLabel` Vs `Label/Technology`. I would like to keep them the same. Or am I missing something? – spozzi Nov 09 '22 at 14:05
  • Since you chose to have an entity to modelize the many-to-many relation between labels and technologies, this entity must also have an id. Don't be confused with the ids of labels, technologies and the id of your extra entity `TechnologyLabel`. Please note that, since you have no specific data on the many-to-many relation (except of course the link to label and technology), you could have avoided completly this entity by using `@ManytoMany` relation. – Pierre Demeestere Nov 09 '22 at 15:15
  • 1
    @spozzi, be aware that : `@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(insertable = false, updatable = false) Label label;` in the `TechnologyLabel entity` is represented in your Db by a column containing the label id in the TechnologyLabel table. – Pierre Demeestere Nov 09 '22 at 15:23
  • The "label__id" workaround is because you had 'labelId' the property using column "labelid" (if it isn't specified, JPA requires the default to be the property name), while the label property would default to using "label_id". Hibernate apparently detects the similarity and warns you that you have multiple writeable mapping apparently trying to use a similarly named column. By changing one to "label__id", you've bypassed its checks, but now have one using "label__id" and the reference mapping using "label_id", which is why you see extra columns and some being set and others not. – Chris Nov 09 '22 at 16:49
  • @Chris, these extra column will disappear when the useless and misleading `technologyId` and `labelId` will be remove from the `TechnologyLabel` class. – Pierre Demeestere Nov 09 '22 at 17:19
  • I do not believe they are misleading at all. Their usefulness depends on the application and its use cases, which I'm guessing are not well known or described at this point. They can be extremely useful if needing to serialize the data and it doesn't want or need to serialize the full referenced data, or you want to query without risk of forcing table joins. This gives the power of relationships and the power of a flat object without forcing the drawbacks of either on every situation. They are not necessary maybe, as the relationships themselves can be marked as ID without the property – Chris Nov 09 '22 at 19:11
  • In the begining, when you build a data model, you refrain from having redundancy. It is only when you have problem with scalability that you think about having a more complex physical model. Serializing only the id of the link data is still possible without redundance. It's true that you will join with other tables. But, that what relational databases are for. They do it very efficiently. Just make sure that all foreign keys have indices and every thing should go well until you really increase the overall amout of data. – Pierre Demeestere Nov 09 '22 at 21:39
  • That can be expensive, and assuming extra, unnecessary data is trivial to bring in is a bad assumption. Separate ID fields are not likely needed, but they do not hurt. The point is the ManyToOne mappings. Just replace the mapsId annotation with ID annotations, remove the uuid properties and it works the same just with reduced options for the application. – Chris Nov 12 '22 at 01:13
  • @PierreDemeestere would you mind providing a code solution? I can't seem to make it work... I tried to use this: https://stackoverflow.com/questions/61610701/jpa-2-0-many-to-many-with-extra-column-update-collection and it still will not work. Can't figure out what I am doing wrong – spozzi Nov 14 '22 at 12:03
  • @Chris I can't tag more than one person per comment. If you could read the one above it would be great – spozzi Nov 14 '22 at 12:04
  • @spozzi I provided a code solution - did you try it? If you do, I suspect issues you'll then hit have to do with maintaining the lifecycle of the new entity - persist/merge/delete operation handling and cascading them appropriately to the entity. This will be different from a M:M where changes to the relationship are picked up by the reference. But it will work for the code you've shown where you explicitly call save on the new entity instances. – Chris Nov 14 '22 at 15:40
  • @Chris Apologies, I must have done something wrong last time I tried your solution as it now works (kinda, I get `detached entity passed to persist: com.example.demo.entity.Label`). Which I know you already replied to it but I did not really understand how to fix it :/ – spozzi Nov 14 '22 at 16:26
  • You would get that exception with the straight M:M mapping you had before, or any of the solutions suggested. It has to do with tech and label being an unmanaged instances; JPA requires the provider know what to do with it, and has strict rules on what happens when you call persist/merge on an entity instance that references other entities. It is safest if you make sure you read in the label from the current EntityManager context and use that instance as the reference to the TechnologyLabel instance you are creating. Post the full error and code (maybe as a new question) – Chris Nov 14 '22 at 21:12
  • 1
    I guess that you have marked the TechnologyLabel.label reference mapping with cascade all or cascade.persist - my answer doesn't have any cascade settings intentionally. Cascade settings (specifically persist) forces JPA to try to persist the existing label instance when you save a new TechnologyLabel instance. Read in the label in the same context (em.find(label.getId(), Label.class)) means it is managed and so ignored by persist calls, or you can remove the cascade settings - JPA is required to just update the relationship, not the entity for the mappings I've shown. See 3.2.4 of the spec – Chris Nov 14 '22 at 21:17

2 Answers2

0

Try something like this:

  @Entity
  @IdClass(TechnologyLabelPk.class)
  public class TechnologyLabel {

    @Id
    UUID technologyId;

    @Id
    @Column(name = "label_id")//pick the column name you really want to use!
    UUID labelId;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("technologyId")//tells JPA to set your technologyId property from this relationship
    Technology technology;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("labelId")//tells JPA to set your labelId property from this relationship
    Label label;

  }

  public class TechnologyLabelPk implements Serializable {
     public UUID technologyId;
     public UUID labelId;
  }

This allows you to use the ID properties directly without having to fetch the relationships if you want, or in queries without forcing joins, but still have JPA manage everything just with the object reference. So you don't need to ever have to set the TechnologyLabel.labelId or technologyId properties yourself. It is a versatile model where if you are creating Labels and Technology instances, you can do so without having to get their Pks assigned before creating TechnologyLabel relations between them.

Downside is that you cannot create the object by setting those fields - you must set the TechnologyLabel.label and technology references.

Chris
  • 20,138
  • 2
  • 29
  • 43
  • You might need to specify the columnDefinition = "BINARY(16)" on column annotations on the IDs - I generally avoid special types and use Strings for UUID storage so I don't know for sure. – Chris Nov 09 '22 at 16:45
0

This easiest way to do it is a manyToMany relation :

public class Technology {
    
    @Id
    @GeneratedValue
    @Column(updatable = false, nullable = false)
    private long uuid;
    
    @Column(nullable = false, length = 30)
    private String technologyName;
    
    @ManyToMany(cascade = CascadeType.ALL)
    private Set<Label> labels = new HashSet<>();
    
    public Set<Label> getLabels() {
         return labels;
    }

    public void setTechnologyName(String technologyName) {
        this.technologyName = technologyName;
    }

    public void add(Label label) {
         labels.add(label);
         label.getTechnologies().add(this);
     }
}

public class Label {
        @Id
        @GeneratedValue()
        @Column(updatable = false, nullable = false)
        private long id;
    
        @ManyToMany(cascade = CascadeType.ALL, mappedBy = "labels")
        private Set<Technology> technologies = new HashSet<>();
    
        public Set<Technology> getTechnologies() {
            return technologies;
        }
    
        public void add(Technology t) {
            technologies.add(t);
            t.getLabels().add(this);
        }
    }

The relation is define on both sides but the mappedBy on the technologies of the Label class is needed to make sure it is the opposite direction of the one in the Technology class.

This create automatically an extra table to manage the manytomany relation.

  • This is what I had before, but I need the relation to hold more columns so I had to make it its own entity – spozzi Nov 10 '22 at 12:21
  • Ok, then remove the extra fields for the id of technology and label on the linking entity. You don't need them. – Pierre Demeestere Nov 10 '22 at 12:24
  • so I should remove the two IDs and replace with a single one that is GeneratedValue? – spozzi Nov 10 '22 at 12:35
  • 2 solutions : you add a generated id on the linking entity and add in the database a unique constraint on the pair of columns containing the ids of label and technology. Or, as you have done, you build a composite primary key based on the pair technology-label and add `@Id` on the attributes `technology` and `label` instead of `@manyToOne`. (no more `@JoinColumn` either) – Pierre Demeestere Nov 10 '22 at 12:52
  • You mean like the solution provided which is showing using the ManyToMany relational tables foreign keys as composite IDs since they are natural ids anyway. Or a new ID property that gets generated for the new entity, making it a complete and independent entity even if it doesn’t have technology or label references. The keys should be driven by the data requirements or they can get in the way. If you need multiple tech label instances between the same tech and label pair, you’ll need assigned Ids. If you need to ensure a single tech label between them, you’ll want the composite key – Chris Nov 12 '22 at 01:23
  • By " If you need multiple tech label instances between the same tech and label pair, you’ll need assigned Ids" you mean that a pair label-technology coukd be linked several times because each links might contains various extra metadata (su as a linkage date for instance), in this case, yes, an ID property must be defined because the link is a genuine entity. But, even if it was not the case, the ID property could be a good modelization associated with a unique constraint on label-technology in the database. – Pierre Demeestere Nov 12 '22 at 10:07