Wednesday, May 4, 2011

@OneToOne with shared primary key

A Friend of mine asked me lately how would I define @OneToOne mapping in JPA with shared primary key. Well, we will definitely need an example ;) - Suppose that we have two entities: Primus and Secundus. Both entities have primary key using Long Java type. They are related 1-1, and Secundus should use the same primary key as Primus. Let's start with following state of Primus:
@Entity
@Table(name = "PRIMUS")
public class Primus {

    private Long id;
    private String name;
    private Secundus secundus;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @OneToOne(cascade = CascadeType.PERSIST, mappedBy = "primus")
    public Secundus getSecundus() {
        return secundus;
    }
...
}
Primus is more important from our point of view, it should be persisted first, and then Secundus, therefore we define the relation between them using mappedBy attribute. We also use cascading here to be able to persist both entities at once. Note, that we will only try to persist the entities, that's why there is only one type of cascade.

Let's take a look at Secundus now:
@Entity
@Table(name = "SECUNDUS")
public class Secundus {

    private Long id;
    private String name;
    private Primus primus;

    @Id
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @OneToOne
    @PrimaryKeyJoinColumn
    public Primus getPrimus() {
        return primus;
    }
...
}
Note, that we don't define the method of generating Secundus identifier here. We also use Secundus identifier (primary key) as the join column for the primus property (see @PrimaryKeyJoinColumn).

Let's try to test the persisting of those two with following code:
 ... 
    @PersistenceContext
    private EntityManager entityManager;

    @Test
    @Transactional(readOnly = false)
    public void test01() {
        final Primus primus = new Primus();
        final Secundus secundus = new Secundus();
        primus.setSecundus(secundus);
        entityManager.persist(primus);
    }
...
The above code and our current JPA mappings will lead to the following SQL queries (used database is MySQL 5.5):
INSERT INTO PRIMUS (NAME) VALUES (null)
SELECT LAST_INSERT_ID()
INSERT INTO SECUNDUS (ID, NAME) VALUES (null, null)
and of course fail at the second insert.

As you probably suppose the Primus identifier is set right after the select last_insert_id() query is performed, let's try to use this knowledge to initialize the Secundus identifier - we will modify the Primus method setId as visible below:
    public void setId(Long id) {
        this.id = id;
        if (null != secundus) {
            secundus.setId(id);
        }
    }
This time the queries looks good:
INSERT INTO PRIMUS (NAME) VALUES (null)
SELECT LAST_INSERT_ID()
INSERT INTO SECUNDUS (ID, NAME) VALUES (12, null)
and both entities are written into database :).

At this point I would consider one more modification of Primus - which is not required for entities persisting, but will assure that both entities are bound in proper way:
    public void setSecundus(Secundus secundus) {
        this.secundus = secundus;
        if (null != secundus) {
            secundus.setPrimus(this);
        }
    }

I'm curious if you have any other solution for this situation, feel free to share it with me :)

PS: The above solution does work for following JPA providers: EclipseLink (2.1) or Hibernate (3.4), but doesn't work for OpenJPA (2.1).



For JPA 2.0+ based solution see my post: @OneToOne with shared primary key, revisited :)

3 comments:

  1. thank you, very good your solutions.
    i sorry for my english. :)

    ReplyDelete
  2. Thanks for this article. Is there any way around adding the setId method to Primus? Since it uses a generated Id, that normally wouldn't be required/desirable, right?

    ReplyDelete
  3. I'm hitting an issue here. Both classes refer to each other recursively and never stop.

    ReplyDelete