Search code examples
jpaeclipselink

Creating new JPA Entity with EmbeddedID


I have a very simple set of tables modelling a many to many relationship.

Foo -< FooBar >- Bar

Which is working fine when I select data but when I try to create a new instance of the relationship I can getting errors because JPA is trying to insert nulls.

I am having to set both the @ManyToOne values as well as the key (which represent the same things) and this makes me think this is not setup correctly.

Question is therefore how do I correctly setup the annotations to create a new FooBar relationship?

Entities

@Entity
@Table(name = "FOO")
public class Foo implements Serializable {

  @Id
  @Column(name = "FOO_ID")
  private int id;
  
  @column(name = "FOO_DESC")
  private String desc;
  
  @OneToMany(mappedBy = "foo")
  private List<FooBar> fooBars;

  //getters / setters / hashCode / equals
}
@Entity
@Table(name = "BAR")
public class Bar implements Serializable {

  @Id
  @Column(name = "BAR_ID")
  private int id;
  
  @column(name = "BAR_DESC")
  private String desc;
  
  @OneToMany(mappedBy = "bar")
  private List<FooBar> fooBars;

  //getters / setters / hashCode / equals
}

Intersection Table (and Key)

@Embeddable
public class FooBarKey implements Serializable {
  private int fooId;
  private int BarId;

  //getters / setters / hashCode / equals
}
@Entity
@Table(name ="FOO_BAR_XREF")
public class FooBar implements Serializable {

  @EmbeddedId
  private FooBarKey key;
  
  @MapsId("fooId")
  @ManyToOne
  @JoinColumn(name = "FOO_ID", referencedColumnName = "FOO_ID")
  private Foo foo;

  @MapsId("barId")
  @ManyToOne
  @JoinColumn(name = "BAR_ID", referencedColumnName = "BAR_ID")
  private Bar bar;  
  
  @Column(name = "DESC")
  private String desc; 
  
  //getters / setters / hashCode / equals
}

Creating Relationship

Finally I am creating a new intersection instance:

//foo and bar exist and are populated
FooBar fb = new FooBar();
FooBarKey fbk = new FooBarKey();
fbk.setFooId(foo.getId());
fbk.setBarId(bar.getId());
fb.setKey(fbk);
fb.setDesc("Some Random Text");

entityManager.persist(fb);

at which point JPA errors with the insert saying it cannot insert null into FOO_ID.

I have checked and at the point I persist the object, the key is populated with both Foo and Bar IDs.

If I add

fb.setFoo(foo);
fb.setBar(bar);

prior to the persist it works but should the @MapsId not effectively tell JPA to map using the key?

I presume that I should be setting both the @ManyToOne and key values which are logically the same thing so I must have something not configured correctly?

I am using Eclipselink if that makes a difference.


Solution

  • When you use mapsId, you are telling JPA that this relationship, and the value of the target entity's primary key, is used to set the mapping named within the mapsId value. JPA then uses this relationship mapping to set the foreign key AND the basic mapping value when it flushes or commits. In your case, you left the relationship NULL, which forces the FK to be null when it gets inserted.

    This allows sequencing to be delayed, as you may not have the primary key generated in the referenced entity when creating and traversing the graph - JPA will calculate and populate the values and propagate them through the graph when it needs them.

    If you aren't using the ID class in your model, the simplest solution is just to remove it and avoid the overhead:

    @Entity
    @IdClass(package.FooBarKey.class)
    @Table(name ="FOO_BAR_XREF")
    public class FooBar implements Serializable {
      
      @Id
      @ManyToOne
      @JoinColumn(name = "FOO_ID", referencedColumnName = "FOO_ID")
      private Foo foo;
    
      @Id
      @ManyToOne
      @JoinColumn(name = "BAR_ID", referencedColumnName = "BAR_ID")
      private Bar bar;  
      
      @Column(name = "DESC")
      private String desc; 
      
      //getters / setters / hashCode / equals
    }
    
    
    public class FooBarKey implements Serializable {
      private int foo;
      private int bar;
    }
    

    IdClass has similar restrictions to what is needed for @EmbeddedId but with one more - the names within it must match the property names designated with @Id, but the types must be the same as the ID class within the referenced entity. Pretty easy if you are using basic mappings within Foo and Bar, but can be more complex.

    Adding more to your composite key is easy:

    @Entity
    @IdClass(package.FooBarKey.class)
    @Table(name ="FOO_BAR_XREF")
    public class FooBar implements Serializable {
      
      @Id
      @ManyToOne
      @JoinColumn(name = "FOO_ID", referencedColumnName = "FOO_ID")
      private Foo foo;
    
      @Id
      @ManyToOne
      @JoinColumn(name = "BAR_ID", referencedColumnName = "BAR_ID")
      private Bar bar;
    
      @Id
      private int someValue
      
      @Column(name = "DESC")
      private String desc; 
      
      //getters / setters / hashCode / equals
    }
    
    
    public class FooBarKey implements Serializable {
      private int foo;
      private int bar;
      private int someValue
    }
    

    JPA will populate your foreign keys for you when the relationships are not-null, but any other fields require either sequencing or your own mechanisms to ensure they are populated prior to insert, and all Ids should be treated as immutable within JPA.