views:

39

answers:

1

I have the following domain objects:

public class Department  {
     private long departmentId;
}
public class Manager {
     private long managerId;
}    
public class Project  {
     private ProjectId compositeId;
     @ManyToOne
     private Department department;
     @ManyToOne
     private Manager manager;
}
public class ProjectId  {
     private long departmentId;
     private long managerId;
}

Project is identified by a composite key (departmentId,managerId). The question is how should Project.setManager(..) or Project.setDepartment(..) be implemented? Is the implemention listed below the best practice?

public void setManager( Manager manager ) {
     this.manager = manager;
     this.compositeId.setManagerId( manager.getId() );
}

My understanding is that compositeId needs to be updated whenever an property is set.

A harder and related question is how should Project.setCompositeId(..) be implemented? Project wouldn't be able to update property manager nor department based on a composite id (long). Overwriting the compositeId without updating the properties would leave Project at an incongruous state.

+2  A: 

I suggest the following:

@Entity
@IdClass(ProjectId.class)
public class Project  {
     @Id @Column(name="DEPARTMENT_ID")
     private long departmentId;
     @Id @Column(name="MANAGER_ID")
     private long managerId;

     @ManyToOne
     @PrimaryKeyJoinColumn(name="DEPARTMENT_ID", referencedColumnName="DPT_ID")
     private Department department;
     @ManyToOne
     @PrimaryKeyJoinColumn(name="MANAGER_ID", referencedColumnName="MGR_ID")
     private Manager manager;

     ...
}

This mapping is very well explained in the JPA Wikibook:

JPA 1.0 requires that all @Id mappings be Basic mappings, so if your Id comes from a foreign key column through a OneToOne or ManyToOne mapping, you must also define a Basic @Id mapping for the foreign key column. The reason for this is in part that the Id must be a simple object for identity and caching purposes, and for use in the IdClass or the EntityManager find() API.

Because you now have two mappings for the same foreign key column you must define which one will be written to the database (it must be the Basic one), so the OneToOne or ManyToOne foreign key must be defined to be read-only. This is done through setting the JoinColumn attributes insertable and updatable to false, or by using the @PrimaryKeyJoinColumn instead of the @JoinColumn.

A side effect of having two mappings for the same column is that you now have to keep the two in synch. This is typically done through having the set method for the OneToOne attribute also set the Basic attribute value to the target object's id. This can become very complicated if the target object's primary key is a GeneratedValue, in this case you must ensure that the target object's id has been assigned before relating the two objects.

(...)

Example ManyToOne id annotation

...
@Entity
@IdClass(PhonePK.class)
public class Phone {
    @Id
    @Column(name="OWNER_ID")
    private long ownerId;

    @Id
    private String type;

    @ManyToOne
    @PrimaryKeyJoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...

    public void setOwner(Employee owner) {
        this.owner = owner;
        this.ownerId = owner.getId();
    }
    ...
}

Reference

Pascal Thivent
sometimes the answer is long enough to make me not read it completely and upvote presuming the usual correctness :)
Bozho
@Bozho At least, I think this one is correct. It's also a good reading actually :)
Pascal Thivent
I mapped this object using mapping xml's instead of annotations. In Project's mapping xml, I defined an embedded-id pointing to the property compositeId, which requires a pair of get/set methods. My question is how should setCompositeId() be implemented. Your solution doesn't adjust this question. Thanks.
Candy Chiu