Reputation: 189
I'm trying to map the one-to-many table relation described in the following diagram in 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) :
@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;
}
@Entity
@Getter
@Setter
@Table(name = "activity_property")
public class ActivityProperty {
@EmbeddedId
private ActivityPropertyId id;
private String value;
@Enumerated(EnumType.STRING)
private PropertyType type;
}
@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
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
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