Pablo Navais
Pablo Navais

Reputation: 189

Mapping a @OneToMany relation with a primary key as foreign key

I'm trying to map the one-to-many table relation described in the following diagram in JPA :

Activity Diagram JPA

As it can be seen, the "activity_property" table uses a composite primary key with (id,name) and column "id" being a foreign key to the the column "id" in the table "activity".

My current entities are mapped this way (Some auxiliary methods have been ommitted for the sake of clarity) :

Activity.java

@Entity
@Getter
@Setter
public class Activity {
   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "act_id_generator")
   @SequenceGenerator(name = "act_id_generator", sequenceName = "activity_id_seq", allocationSize = 1, initialValue = 1)
   private Integer id;

   private String name;

   @OneToMany(mappedBy = "id.activityId", cascade = CascadeType.ALL, orphanRemoval = true)
   private List<ActivityProperty> properties;
}

ActivityProperty.java

@Entity
@Getter
@Setter
@Table(name = "activity_property")
public class ActivityProperty {
    @EmbeddedId
    private ActivityPropertyId id;

    private String value;

    @Enumerated(EnumType.STRING)
    private PropertyType type;
}

ActivityPropertyId.java

@Embeddable
@Getter
@Setter
@ToString
public class ActivityPropertyId implements Serializable {

    @Column(name = "id")
    private Integer activityId;

    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ActivityPropertyId that = (ActivityPropertyId) o;
        return activityId.equals(that.activityId) &&
                name.equals(that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(activityId, name);
    }
}

When I try to persist an activity this way :

Activity activity = createActivity("Activity_1");
activity.addProperty(ActivityProperty.from("Prop1", "value1", PropertyType.PARAMETER));
activityDAO.persist(activity);

I can see the following traces in Hibernate :

Hibernate: call next value for activity_id_seq
Hibernate: insert into activity (name, id) values (?, ?)
Hibernate: insert into activity_property (type, value, id, name) values (?, ?, ?, ?)
05-05-2019 12:26:29.623 [main] WARN  o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - SQL Error: 23502, SQLState: 23502
05-05-2019 12:26:29.624 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - NULL not allowed for column "ID"; SQL statement:
insert into activity_property (type, value, id, name) values (?, ?, ?, ?) [23502-199]

It seems the autogenerated Id for the Activity is not been used in the second insertion for the ActivityProperty.

I can't figure out how to map correctly this kind of relationship.Is there anything missing in my JPA annotations ?

Upvotes: 5

Views: 7790

Answers (2)

Simon Martinelli
Simon Martinelli

Reputation: 36223

The mappedBy attribute is used to indicate that this relationship is not used for persisting and the attribute in mappedBy is used for that purpose.

So either create a back reference from ActivityProperty to Activity and use that as mapped by or you have to use @JoinColumn to declare the foreign key.

That would look like. The attribute name points to the name of the foreign key in the target table.

@JoinColumn(name = "id", nullable = false)
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<ActivityProperty> properties;

Update 05/06/2019 As you have enabled cascading you should and writing the id from the relationship you must set the activityId field read only:

@Column(name = "id", insertable = false, updateable = false)
private Integer activityId;

Read more about relationship mappings here: https://thoughts-on-java.org/ultimate-guide-association-mappings-jpa-hibernate/

Upvotes: 1

K.Nicholas
K.Nicholas

Reputation: 11561

As far as I can tell you JPA doesn't support that through cascade operations. You're asking too much from JPA. I don't have a specific reference that says this but you can get as close as you can with the below code. It will not run because hibernate (at least) doesn't know how to automagically map the activity.id to the PK of the ActivityProperty class.

@Entity
@Getter
@Setter
@Table(name = "activity_property")
@IdClass(ActivityPropertyId.class)
public class ActivityProperty {
    @ManyToOne
    @Id
    @JoinColumn(name="id", referencedColumnName="id")
    private Activity activity;

    @Id
    private String name;
    private String value;

}

@Getter
@Setter
@ToString
public class ActivityPropertyId implements Serializable {
    public ActivityPropertyId() {}

    @Column(name = "id")
    private Integer activity;

    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ActivityPropertyId that = (ActivityPropertyId) o;
        return activity.equals(that.activity) &&
                name.equals(that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(activity, name);
    }
}

This will give the exception

Caused by: java.lang.IllegalArgumentException: Can not set java.lang.Integer field model.ActivityPropertyId.activity to model.Activity

And I haven't seen any good way around that. IMHO cascade is a fool's paradise and you should be saving the ActivityProperty instances yourself and using the bidirectional mapping for query only. This is really how it is meant to be used. If you do some simple saving operations on a OneToMany list through cascade and look at the SQL generated you will see horrible things.

Upvotes: 1

Related Questions