2

Given the following example (departments - projects):

A department has the following properties (composite primary key):

@Entity
@IdClass(DeptId.class)
public class Department
{
    @Id
    @Column(name="number")
    private Integer number;

    @Id
    @Column(name="country")
    private String country;

    @Column(name="name")
    private String name;

    @OneToMany(mappedBy="dept")
    private Collection<Project> projects;

    ...
}

Here the PK class:

public class DeptId implements Serializable
{
    private Integer number;
    private String country;

    ...
}

The relationship between projects and departments is many-to-one, that is a deptartment can have many projects. The Project class is itself using a composite key referencing Department's composite key. Important note: it's only about the implementation with @IdClass not @EmbeddedId.

Then the (problematic) JPA 1.0 @IdClass implementation would have to look something like that (redundant deptNum and deptCtry properties): -> it's just a unique name within a department

@Entity 
@IdClass(ProjectId.class)
public class Project
{
    @Id
    @Column(name="dept_number")
    private Integer deptNumber;

    @Id
    @Column(name="dept_country")
    private String deptCountry;

    @Id
    @Column(name="name")
    private String name;

    @ManyToOne 
    @JoinColumns({
       @JoinColumn(name="dept_number", referencedColumnName="number"),
       @JoinColumn(name="dept_country", referencedColumnName="country")
    })    
    private Department dept;

    ...
}

The ProjectId is:

public class ProjectId implements Serializable
{
    private String name;
    private DeptId dept;

    ...
}

The problem with this is that neither Hibernate nor EclipseLink know how to map the two redundant properties deptNum and deptCtry in Project to the dept property in DeptId (or the properies within it). -> MappingException etc.

My question is:

Is this a limitation of JPA 1.0, that tables with composite keys referencing other composite keys with @IdClass implementations generally WON'T work, because the JPA implementation simply can't know how to map these fields?

As a workaround, you'd have to use @EmbeddedId for these classes or use JPA 2.0 syntax to annotate the @XToX associations with @Id. I just want to make sure my view on this is right.

Thanks

Kawu
  • 13,647
  • 34
  • 123
  • 195

3 Answers3

4

Yes, this is a limitation of JPA 1.0, corrected in JPA 2.0. In the new JPA 2.0, you can put the ID annotation on your dept relationship and completely avoid having the redundent deptCountry and deptNumber attributes, with the key class using nesting. In JPA 1.0, only basic mappings can be marked as apart of the ID, requiring redundent mappings and some code to ensure that the values/relationships get put into the cache correctly when persisting. Because of the redundancy, as mentioned in other answers, one of the mappings for a field needs to be marked read-only via the insertable/updatable=false. Doing so though means that value is not merged into the cache - so changes (such as on insert, since you can't change an objects ID once it exists) will not be reflected unless the object is refreshed from the database. If you mark the JoinColumns as read-only, you will need to get the values from the referenced dept and put them into the correspoinding basic id attributes manually when you want to persist a Project. But, you can also mark the basic attributes as read-only. Eclipselink anyway will not have any problems and will correctly set the field values using the associated dept entity (as long as it is set before persist is called on the Project). Notice though that the basic attributes may or may not be populated when you read back the project in a different context- this will depend on if the entity is refreshed from the database or not. If they are read-only, they do not get merged into the shared cache since they, being read only, should not have changed. So they can be just ignored, or if they must be populated, the entity refreshed or the values set from the dept in an event.

This same model can be reused by using the JPA2.0 @MapsId, which will also maintain the basic mappings using the values from the relationship for you. Only benifit I see is that you don't need to access the relationship (potentially causing unneccessary joins or database access on lazy relationships) to get the foreign key/id field values.

As for the ZipArea EclipseLink exceptions, they are due to ZipAreaId having a ZipId zip attribute instead it being flattened out. JPA 1.0 requires the key class to have an attribute of the same type and name for each @ID attribute in the Entity.

Chris
  • 20,138
  • 2
  • 29
  • 43
2

The problem with this is that neither Hibernate nor EclipseLink know how to map the two redundant properties deptNum and deptCtry in Project to the dept property in DeptId

This is why you need to define the ManyToOne foreign key(s) as read-only with this kind of mapping. This is done by setting the JoinColumn attributes insertable and updatable to false.

So try the following:

@Entity 
@IdClass(ProjectId.class)
public class Project
{
    @Id
    @Column(name="dept_number")
    private Integer deptNumber;

    @Id
    @Column(name="dept_country")
    private String deptCountry;

    @Id
    @Column(name="name")
    private String name;

    @ManyToOne 
    @JoinColumns({
       @JoinColumn(name="dept_number", referencedColumnName="number", insertable=false, updatable=false),
       @JoinColumn(name="dept_country", referencedColumnName="country", insertable=false, updatable=false)
    })    
    private Department dept;
    ...
}
Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • It's not about the insertable/updatable stuff. How do the ORMs know which properties of ProjectId's dept_x map to? I doesn't work with both... Hibernate AND EL. They already fail when the EntityManagerFactory is instantiated. It makes sense sort of... – Kawu Oct 31 '10 at 12:02
  • @Kawu: Maybe I'm not getting what you're saying but when you repeat mappings like above, you have to make the association read-only using insertable/updatable in the `JoinColumn`. If it doesn't work, show a mapping allowing to reproduce and some traces illustrating the problem. – Pascal Thivent Oct 31 '10 at 13:24
  • Let me clarify this: JPA 1.0 doesn't allow @Id on associations, this is why in Project you have two redundant members deptNumber and deptCountry. Since these two are a PK in the Departments table, the Project's composite key class ProjectId has a member of type DeptId. But Hibernate and EclipseLink both throw mapping exceptions, because they can't map the two redundant @Id members deptNumber and deptCountry to the DeptId's properties. I wanted to know if this is a general limitation or if there are way to tell JPA 1.0 ORMs how to map these. (JPA 1.0 requires the names of the fields to match) – Kawu Nov 01 '10 at 11:03
  • @Kawu `But Hibernate and EclipseLink both throw mapping exceptions, because they can't map the two redundant @Id members deptNumber and deptCountry to the DeptId's properties` which is why you have to define the ManyToOne foreign key(s) as read-only by setting insertable and updatable to false... there is no limitation. Did you try my suggestion? – Pascal Thivent Nov 01 '10 at 11:16
  • I left these details off the example. In my DB model, insertable = false and updatable = false *both* exist. It's about the ORMs not being able to *find* the respective properties in DeptId, because they only seem to be looking at ProjectId. The redundant fields and the real properties are actually *two* classes apart not one: Project (redundant deptNumber and deptCountry) -> ProjectId (DeptId ref) -> DeptId (declaring number and country). I have this constellation in my DB four times. None of them work, even with insertable = false and updatable = false... it doesn't seem to be relevant. – Kawu Nov 01 '10 at 11:18
  • Oh and, the mapping exceptions get thrown on EntityManagerFactory emf = Persistence.createEntityManagerFactory("bbstats"); – Kawu Nov 01 '10 at 11:19
  • @Kawu Please show mappings allowing to reproduce, without omitting anything such as insertable and updatable which are totally relevant. – Pascal Thivent Nov 01 '10 at 11:50
  • Okay, the example was off the top of my head. I'll post the one which is giving me headaches for weeks now. – Kawu Nov 01 '10 at 13:56
  • It's actually more complicated than I thought to describe. Would you mind checking the sources if I post a link to a ZIP file? Which ORM are you using? – Kawu Nov 01 '10 at 16:03
  • @kawu Post it and I'll see (but I'd prefer a simple runnable testcase if you can). I use any JPA provider but mostly Hibernate and EL. – Pascal Thivent Nov 01 '10 at 16:29
  • I'm using Tomcat 6 + MySQL 5. I have created an extra project for that matter (7 tables), including a persistence.xml for Hibernate and EL. It might be easy to port this to JBoss or whatever you're using. Do you need non-MySQL DDL scripts? – Kawu Nov 01 '10 at 16:38
  • The ZIP file can be found here: http://www.kawoolutions.com/media/geoinfo.zip . I'd really appreciate if you could have a look at it as it's starting to make me mad. – Kawu Nov 01 '10 at 16:57
  • @Kawu I didn't really look at your project yet (only opened it) but a standalone test case using an in memory database would indeed have my preference. – Pascal Thivent Nov 02 '10 at 14:30
2

The problem with the posted code is, that JPA 1.0 really doesn't allow nesting of composite primary key classes. This ProjectId is invalid:

public class ProjectId implements Serializable
{
    private String name;
    private DeptId dept;

    ...
}

DeptId has to be flattened, like:

public class ProjectId implements Serializable
{
    private Integer deptNumber;
    private String deptCountry;
    private String name;

    ...
}

I just got an EclipseLink version to go, but Hibernate has problems with that. I wonder how to tell Hibernate that JPA 1.0 is assumed.

Kawu
  • 13,647
  • 34
  • 123
  • 195