How to create a composite primary key which contains a @ManyToOne attribute as an @EmbeddedId in JPA?

Tom Anderson picture Tom Anderson · Aug 25, 2011 · Viewed 19.9k times · Source

I'm asking and answering my own question, but i'm not assuming i have the best answer. If you have a better one, please post it!

Related questions:

I have a pair of classes which are in a simple aggregation relationship: any instance of one owns some number of instances of the other. The owning class has some sort of primary key of its own, and the owned class has a many-to-one to this class via a corresponding foreign key. I would like the owned class to have a primary key comprising that foreign key plus some additional information.

For the sake of argument, let's use those perennial favourites, Order and OrderLine.

The SQL looks something like this:

-- 'order' may have been a poor choice of name, given that it's an SQL keyword!
create table Order_ (
    orderId integer primary key
);
create table OrderLine (
    orderId integer not null references Order_,
    lineNo integer not null,
    primary key (orderId, lineNo)
);

I would like to map this into Java using JPA. Order is trivial, and OrderLine can be handled with an @IdClass. Here's the code for that - the code is fairly conventional, and i hope you'll forgive my idiosyncrasies.

However, using @IdClass involves writing an ID class which duplicates the fields in the OrderLine. I would like to avoid that duplication, so i would like to use @EmbeddedId instead.

However, a naive attempt to do this fails:

@Embeddable
public class OrderLineKey {
    @ManyToOne
    private Order order;
    private int lineNo;
}

OpenJPA rejects the use of that as an @EmbeddedId. I haven't tried other providers, but i wouldn't expect them to succeed, because the JPA specification requires that the fields making up an ID class be basic, not relationships.

So, what can i do? How can i write a class whose key contains @ManyToOne relationship, but is handled as an @EmbeddedId?

Answer

Tom Anderson picture Tom Anderson · Aug 25, 2011

I don't know of a way to do this which doesn't involve duplicating any fields (sorry!). But it can be done in a straightforward and standard way that involves duplicating only the relationship fields. The key is the @MapsId annotation introduced in JPA 2.

The embeddable key class looks like this:

@Embeddable
public class OrderLineKey {
    private int orderId;
    private int lineNo;
}

And the embedding entity class looks like this:

@Entity
public class OrderLine{
    @EmbeddedId
    private OrderLineKey id;

    @ManyToOne
    @MapsId("orderId")
    private Order order;
}

The @MapsId annotation declares that the relationship field to which it is applied effectively re-maps a basic field from the embedded ID.

Here's the code for OrderId.