0

I have issues with mapping OneToMany foreign key to a composite primary key. I have already tried many solutions, including this post @OneToMany and composite primary keys?.

So the situation is:

I have two entities, say, Box & Color and the composite primary key is on the child side (Color).

@Entity
@Table(name = "box")
data class Box(
        @Id
        var id: Int = 0,
        ...
       @OneToMany(cascade = [CascadeType.ALL])
        @JoinColumns(
                JoinColumn(name = "box_id", referencedColumnName = "id"),
                JoinColumn(name = "locale", referencedColumnName = "locale"))
        val colors: List<Color> = emptyList(),
)

@Entity
@Table(name = "color")
data class Color(
        
        @EmbeddedId
        var colorId: ColorId? = null,
) : Serializable

@Embeddable
data class ColorId(
        var id: Int = 0,
        @Column(name = "locale", insertable = false, updatable = false)
        var locale: Locale = Locale.Germany
) : Serializable

So, in the Box entity, I am trying to create an OneToMany mapping between Box & Color entities. For this should I use the composite primary key of the Color entity? If I try to join columns to the composite primary key(as I have done in the Box entity) I do get an error saying - unable to locate the logical column "locale".

How can I resolve this?

Or what is the neat solution for this problem?

SGuru
  • 645
  • 1
  • 11
  • 28

3 Answers3

2

OneToMany mapping will connect one Box with many Colors. Therefore, each Color needs to point to a Box, so each Color has a foreign key referencing the primary key of Box (of Int type).

enter image description here

@OneToMany as an owning side

In your mapping, you should use a single join column since this join column will be placed in the color table:

@Entity
@Table(name = "box")
data class Box(
        @Id
        var id: Int = 0,
        ...

        @OneToMany(cascade = [CascadeType.ALL])
        @JoinColumn(name = "box_id", referencedColumnName = "id")
        var colors: MutableList<Color> = mutableListOf()
)

You can check-out the working sample (Kotlin and Java) in my github repo.

@OneToMany as an inverse (mapped-by) side

Database perspective

@OneToMany as an owning side and @OneToMany as an inverse side (with a complimentary mandatory @ManyToOne) produce the same database schema - a foreign key on the color table. That's why @JoinColumn annotation looks the same in both cases, no matter which side you place it on - the goal is to produce a foreign key on the many side.

Application perspective

The difference in 'owning-side' is in an application perspective. JPA/hibernate saves the relationships only from the owning side.

So, if you change the owning side, you must set up this side (attribute) in your application code. In this case, you must set up Box in Color, otherwise, JPA/hibernate will not create a relationship (even though you added your Colors to Box). Moreover, it will not raise any exception, just the relationship will not be created. Next time, you will retrieve your Box from a database, the Color list will be empty.

You can check-out the working samples and see the difference in ownership in my github repo.

@Entity
@Table(name = "box")
data class Box(
        @Id
        var id: Int = 0,
        ...

        @OneToMany(cascade = [CascadeType.ALL], mappedBy="box")
        var colors: MutableList<Color> = mutableListOf(),
)

@Entity
@Table(name = "color")
data class Color(
        
        @EmbeddedId
        var colorId: ColorId? = null,
        
        // the owning side is changed, therefore you MUST set the box in Color
        // otherwise the relationship in a database will not be saved (!)
        @ManyToOne
        @JoinColumn(name = "box_id")
        var box: Box? = null
) : Serializable

Other possible problems

Since you struggle with a OneToMany relationship, it might be useful for you to consider some other possible problems in this model.

Locale non-insertable and non-updatable

Another problem, you may encounter, is saving locale in ColorId as you marked it as non-insertable and non-updatable. If this is on purpose that's fine (in that case all your colors must be pre-inserted into the database, or they will be inserted without Locale).

Please, keep in mind, that setting Locale.GERMAN has no effect on a database in this case. It will be silently ignored and if you don't have such a color in a database, it will be inserted with null.

Color assigned to only one box

If you model this relationship, you make a single Color (like Black in German) be assigned to only one Box. It sounds a bit unnatural. Usually, I would assume Black can be assigned to many Boxes. So, it would be a ManyToMany relationship. Again, if this is on purpose, that's fine!

ColorId as a primary key

It's also a bit unnatural to have Locale as a part of a primary key in Color -- Black in German and Black in English as different colors? The colors themselves are locale-independent. The name of a color is locale-dependent, but it's more a matter of UI. Again, if this is on purpose, that's fine! After all, it's your business model!

  • But Color has composite primary key (id & locale). Shouldn't locale be considered in this case? – SGuru Jan 07 '21 at 06:27
  • No, you have many on the Color side and one on the Box side. The foreign key is on the many side and references the one side. So, the foreign key will be placed in Color and will reference the Box id, which is a simple int. – Patrycja Wegrzynowicz Jan 07 '21 at 13:48
  • @ThirumalaiParthasarathi my code is fine and tested. :) Check out the github repo (in Java for now, but the mappings are exactly the same): https://github.com/pwegrzynowicz/stackoverflow-examples/tree/master/jpa-box-colors, the approach with `ManyToOne` and `mappedBy` in `OneToMany` will work too. But it changes the owning side of a relationship, so it depends on what the author prefers and what his use-case is. – Patrycja Wegrzynowicz Jan 08 '21 at 13:26
  • @ThirumalaiParthasarathi And added Kotlin samples to the repo. You can clearly see it's working. Plus, you can see how changing the ownership impacts the code. – Patrycja Wegrzynowicz Jan 08 '21 at 20:29
  • You are right. I must've misread your post. But one point though, the strategy you are using here is unidirectional which is NOT that performant. I would still prefer a `@ManyToOne` on the Color entity (may be a bidirectional mapping with `@OneToMany` with the `mappedBy` attribute to specify the other side owns the join). It is also more performant than the unidirectional join. – Thirumalai Parthasarathi Jan 13 '21 at 15:08
0

Before providing you a solution, i would like to give you more info on the @OneToMany and the @JoinColumns annotation.

A @OneToManymapping symbolizes the fact that the Entity which has that mapping applied has many references to the other entity.

i.e. A Box has many references to Color(s) in your case.

Now to model this, I would suggest you reverse the mapping and use @ManyToOne on the Color entity.

@Entity
@Table(name = "color")
data class Color(
        
        @EmbeddedId
        var colorId: ColorId? = null,
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "box_id"), //referencedColumnName is not needed here as it is inferred as "id"
        var box: Box? = null
) : Serializable

Because the foreign key is supposed to be in Color. You cannot have foreign keys from Box to Color as there may be more than one Color.

Also, note that it is not @JoinColumns, it is the singular @JoinColumn. You need the plural version if the Box entity contains a Composite Primary Key.

With that being said, you can all together ignore the @OneToMany mapping, because if you need to get all Colors of a Box, you can and should in my opinion, simply use the Query API.

A @OneToMany mapping in this case would only be a convenience. But if you insist on having the @OneToMany mapping then you can use the following.

@Entity
@Table(name = "box")
data class Box(
        @Id
        var id: Int = 0,
        ...
       @OneToMany(cascade = [CascadeType.ALL], mappedBy="box") // mappedBy will use the "box" reference from the Color Class
        val colors: List<Color> = emptyList(),
)
Thirumalai Parthasarathi
  • 4,541
  • 1
  • 25
  • 43
  • In @ManyToOne(fetch = LAZY) only box_id joining is done.., @JoinColumn(name = "box_id") : how about the other property locale . Joining locale is required? – SGuru Jan 07 '21 at 13:29
  • @SGuru in your question, the `locale` field is in the `Color` entity. Why do you think `locale` is needed to refer to a `Box` entity? – Thirumalai Parthasarathi Jan 08 '21 at 06:38
-1

You have two options. Either define a join table @JoinTable(name = "color_box_assignment", joinColumns = ...) or use an inverse mapping:

@Entity
@Table(name = "color")
data class Color(
        
        @EmbeddedId
        var colorId: ColorId? = null,
        @ManyToOne(fetch = LAZY)
        @JoinColumns(
            JoinColumn(name = "box_id", referencedColumnName = "id"),
            JoinColumn(name = "locale", referencedColumnName = "locale"))
        var box: Box? = null
) : Serializable
Christian Beikov
  • 15,141
  • 2
  • 32
  • 58
  • by having an inverse mapping, will the composite key be taken care? And how about the solution presented by Patrycja in the below post? That works as well, but I am unsure if composite key is considered there? – SGuru Jan 07 '21 at 09:21
  • Yeah it will handle composite keys. The answer Partycja gave is IMO not correct but goes into the same direction as mine. – Christian Beikov Jan 07 '21 at 09:54
  • I do get the following error: org.hibernate.MappingException: Unable to find column with logical name: locale in box – SGuru Jan 07 '21 at 14:11
  • What mapping are you using now? Approach this step by step. First try adding just the `box` association to `Color`. This should just work. Then add the inverse `@OneToMany(mappedBy = "box") Set colors` on the other side. – Christian Beikov Jan 08 '21 at 12:49
  • @ChristianBeikov my solution is fine and working, check out the sample code (in Java for now, but mappings are exactly the same): https://github.com/pwegrzynowicz/stackoverflow-examples/tree/master/jpa-box-colors, the approach with `ManyToOne` will work too, but it changes the `owning` side of a relationship. So, it depends on a use-case of the author. – Patrycja Wegrzynowicz Jan 08 '21 at 13:23
  • The OP mentioned he has a composite primary key which you are not mapping – Christian Beikov Jan 11 '21 at 08:59
  • @ChristianBeikov looks like you didn't see my code and didn't read my post. Please, take a closer look and you'll see that `@JoinColum` represents the FK column. In this case, the composite key is not an FK and it shouldn't be used in the `@JoinColumn` annotation. That's why the original code doesn't work, and your code will not work either. It will raise **the same error** as in the question - `unable to locate the logical column "locale"`. – Patrycja Wegrzynowicz Jan 11 '21 at 23:57
  • I was of the impression that the OP want this, but if it is not, your solution is fine. – Christian Beikov Jan 12 '21 at 07:54