2

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?

Chuck Lowery
  • 902
  • 6
  • 17
  • 1
    you need to define the joincolumn `@ManyToOne @JoinColumn(name = "some_id")` in the inverse, but I am not sure if it will work because you are using an embedded class – fmodos Apr 07 '14 at 18:18
  • `private Item parent; @ManyToOne @MapsId("itemId") @JoinColumn(name = "itemId", referencedColumnName = "id") public Item getItem() { return parent; } public void setItem(Item parent) { this.parent = parent; }` If I add that code to the ItemCost with no other changes I get the following error: `attempted to assign id from null one-to-one property [testOneToMany.ItemCost.item]` – Chuck Lowery Apr 07 '14 at 18:28
  • 1
    do you set the Item before saving it or is this property null? I think you will need to change the test code... it wont work if you only set the property that has the mappedby – fmodos Apr 07 '14 at 18:44
  • Ok, that would violate the premise of the question. The mapping should take care of that relationship. If I am setting the properties by hand what good is the ORM tool? – Chuck Lowery Apr 07 '14 at 19:27
  • 1
    it is still better than write all the sql :) anyway check the item **2.2.5.3.1.1. Bidirectional** in [this link](http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/#d0e1168) I think this is what you are looking for – fmodos Apr 07 '14 at 20:06

1 Answers1

1

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):

  1. 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:

    1. Don't provide any methods that modify the object's state
    2. Ensure that the class can't be extended
    3. Make all fields final
    4. Make all fields private
    5. Ensure exclusive access to any mutable components.

    I believe a compound primary key class fits perfectly into this rule.

  2. 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).

wypieprz
  • 7,981
  • 4
  • 43
  • 46
  • This isn't exactly what I was looking for but it is the answer that is correct. The problem is that I do not want to have to specify an entity in the ID. Hibernate forces this behavior and as such your answer is spot on. I don't agree with your first side notes especially for compound keys with more than two fields. In general you are correct but in particular I disagree. What is the benefit of adding @Temporal? – Chuck Lowery Apr 18 '14 at 17:39
  • 1
    I have updated my answer. Just to clarify your statement: "_The problem is that I do not want to have to specify an entity in the ID_"? Do you want to preserve unidirectional association from `Item` entity (remove `Item` from `ItemCost` entity)? – wypieprz Apr 18 '14 at 20:27
  • Ok, because the parent portion of the ID is the parent itself it does make sense make the id's fields final. Thanks for your help! – Chuck Lowery Apr 18 '14 at 21:35
  • @wypieprz I have a similar question. Are you willing to help me with it? Here is the link: http://stackoverflow.com/questions/25316761/duplicate-entry-string1-string2-for-key-primary – CodeMed Aug 14 '14 at 20:37