Reputation: 933
I am having some trouble describing a one-to-many relationship with JPA. The problem that I am facing is that I think I should be able to save the one
side of the relationship and have the many
side receive the newly create id for the one
side.
Here is a general schema of the problem domain:
Item ::= {
*id,
name
}
ItemCost ::= {
*itemId,
*startDate,
cost
}
* indicates PK
In the above schema an item will have a cost for some given amount of time. It will only have one cost for that time period thus this is a one-to-many relationship.
This is where I get into trouble. Here is how go about describing the relationship with POJOs and JPA and am getting myself stuck.
@Entity
@Table(name = "Item", schema = "dbo")
public class Item implements Serializable {
private Integer id;
private String name;
private Set<ItemCost> costs = new HashSet<>(0);
public Item() {
}
public Item(String name) {
this.name = name;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(name = "Name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToMany(cascade = CascadeType.ALL, mappedBy = "id.itemId")
public Set<ItemCost> getCosts() {
return costs;
}
public void setCosts(Set<ItemCost> costs) {
this.costs = costs;
}
public void addCost(Date date, int amount) {
getCosts().add(new ItemCost(date, amount));
}
}
In the Item class I have described the fields that represent the schema plus an additional field for the relationship. Since the relationship is one-to-many I have added that annotation and instructed it to cascade all as I want it save, update, delete etc its children when it is changed. Additionally, I have added the mappedBy attribute to the annotation to indicate that I want to relationship bound by the id->itemId fields in the ItemCost (I think this is where my assumption is wrong: I am assuming this instructs the JPA provider, in this case Hibernate, that upon save I want the item id to be placed into the ItmmCost at this location before propagating the save).
@Entity
@Table(name = "ItemCost", schema = "dbo")
public class ItemCost implements Serializable {
private Id id;
private int cost;
public ItemCost() {
id = new Id();
}
public ItemCost(Date startDate, int cost) {
id = new Id();
id.setStartDate(startDate);
this.cost = cost;
}
@Column(name = "cost")
public int getCost() {
return cost;
}
public void setCost(int cost) {
this.cost = cost;
}
@EmbeddedId
public Id getId() {
return id;
}
public void setId(Id id) {
this.id = id;
}
@Embeddable
public static class Id implements Serializable {
private int itemId;
private Date startDate;
public Id() {
}
public Id(Date startDate) {
this.startDate = startDate;
}
@Column(name = "itemId")
public int getItemId() {
return itemId;
}
public void setItemId(int itemId) {
this.itemId = itemId;
}
@Column(name = "startDate")
public Date getStartDate() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
@Override
public int hashCode() {
int hash = 3;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Id other = (Id) obj;
if (this.itemId != other.itemId)
return false;
if (!Objects.equals(this.startDate, other.startDate))
return false;
return true;
}
}
}
I have additionally filled out the Item save with its field cost
and additionally with its primary key of itemId
and startDate
. This is an embedded id as that seems to best describe the scenario with a compound id. Additionally, I may need to pass around this identifier in the future.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Item item = new Item("A Jar");
item.addCost(new Date(), 10);
em.persist(item);
em.getTransaction().commit();
em = emf.createEntityManager();
item = em.find(Item.class, item.getId());
Assert.assertNotNull(item);
assertThat(item.getCosts().size(), is(1)); // fails here with a result of 0
The test is rather strait forward and fails on the very last line. If I look at the db I am getting an entry in the ItemCost table but it has an itemId of zero (the default value of int in java).
Since, the cascade is saving I figure that my assumption about the mappedBy
is inccorect. Is there some piece of meta data that is missing that will help inform the JPA provider that it should apply the Item.Id
value to the children defined in the mapping?
Upvotes: 2
Views: 840
Reputation: 8219
Consider updating your classes in the following way (the assumption is that PROPERTY access is used):
public class Item implements Serializable {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "item") //non-owning side
public Set<ItemCost> getCosts() {
return costs;
}
}
public class ItemCost implements Serializable {
private Item item;
@ManyToOne //owning side
@JoinColumn(name = "itemId") //the foreign key (the primary key of "Item")
@MapsId("itemId") //is shared with the compound primary key of "ItemCost"
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
}
Additionally you would need to set up bidirectional relationship, i.e.
em.getTransaction().begin();
Item item = new Item("A Jar");
// item.addCost(new Date(), 7);
ItemCost itemcost = new ItemCost(new Date(), 7);
itemcost.setItem(item); //owning side
item.getCosts().add(itemcost); //non-owning side
em.persist(item);
em.getTransaction().commit();
The above will produce:
INSERT INTO Item (id, Name) VALUES (1, 'A Jar')
INSERT INTO ItemCost (cost, startDate, itemId) VALUES (7, 2014-04-08 22:50:00, 1)
Side notes (regarding your compound primary key class):
Don't use setter methods on compound primary key class specified with @EmbeddedId
or @ClassId
annotation (once it has been constructed it should not be changed), according to Effective Java, Item 15: Minimize mutability:
An immutable class is simply a class whose instances cannot be modified. All the information contained in each instance is provided when is created and is fixed for the lifetime of the object. (...) To make class immutable, follow these five rules:
- Don't provide any methods that modify the object's state
- Ensure that the class can't be extended
- Make all fields final
- Make all fields private
- Ensure exclusive access to any mutable components.
I believe a compound primary key class fits perfectly into this rule.
Specify @Temporal
annotation for java.util.Date
and java.util.Calendar
persistent fields, according to JPA 2.0 Specification, chapter 11.1.47 Temporal Annotation:
The
Temporal
annotation must be specified for persistent fields or properties of type java.util.Date and java.util.Calendar. It may only be specified for fields or properties of these types.
Annotating java.util.Date
and java.util.Calendar
types with @Temporal
indicates which of the corresponding JDBC java.sql
types to use when communication with JDBC driver (i.e. TemporalType.DATE
maps to java.sql.Date
and so on). Of course, if you use java.sql
types in your entity classes then the annotation is no longer required (I am not aware of these types from your code snippets).
Upvotes: 1