如何描述一对多关系(JPA / HIbernate)

时间:2014-04-07 17:27:45

标签: java hibernate jpa one-to-many many-to-one

我在描述与JPA的一对多关系时遇到了一些麻烦。我面临的问题是,我认为我应该能够保存关系的one方并让many方接收one方的新创建ID。

以下是问题域的一般架构:

Item ::= {
   *id,
   name
}

ItemCost ::= {
    *itemId,
    *startDate,
    cost
}
* indicates PK

在上述模式中,项目将花费一定的时间。这段时间只有一个成本,因此这是一对多的关系。

这是我遇到麻烦的地方。以下是如何描述与POJO和JPA的关系,并让自己陷入困境。

@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));
    }
}

在Item类中,我描述了表示模式的字段以及关系的附加字段。由于这个关系是一对多的,我已经添加了这个注释,并指示它级联所有,因为我希望它在更改时保存,更新,删除等子项。另外,我已将mappedBy属性添加到注释中,以指示我想要通过ItemCost中的id-&gt; itemId字段绑定关系(我认为这是我的假设是错误的:我假设这指示了JPA提供者,在这种情况下,Hibernate,在保存时,我希望在传播保存之前将项目ID放入此位置的ItmmCost中)。

@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;
        }
    }
}

我还在其中添加了字段cost以及主键itemIdstartDate。这是一个嵌入式ID,因为它似乎最好地描述了具有复合id的场景。此外,我可能需要在将来传递此标识符。

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

测试相当严格,最后一行失败了。如果我查看db,我会在ItemCost表中获得一个条目,但它的itemId为零(java中默认的int值)。

因为,级联节省了我认为我对mappedBy的假设是inccorect。是否有一些缺少的元数据有助于告知JPA提供者它应该将Item.Id值应用于映射中定义的子项?

1 个答案:

答案 0 :(得分:1)

考虑以下列方式更新您的类(假设使用了PROPERTY访问):

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;
    }
}

此外,您需要设置双向关系,即

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();

以上将产生:

INSERT INTO Item (id, Name) VALUES (1, 'A Jar')
INSERT INTO ItemCost (cost, startDate, itemId) VALUES (7, 2014-04-08 22:50:00, 1)

附注(关于复合主键类):

  1. 根据Effective Java,不要在使用@EmbeddedId@ClassId注释指定的复合主键类上使用setter方法(一旦构造它就不应该更改) ,第15项:尽量减少可变性:

      

    不可变类只是一个无法修改其实例的类。   每个实例中包含的所有信息都是在何时提供的   创建并在对象的生命周期内固定。 (...)制作   class immutable,遵循以下五条规则:

         
        
    1. 不提供任何修改对象状态的方法
    2.   
    3. 确保不能扩展课程
    4.   
    5. 将所有字段设为最终
    6.   
    7. 将所有字段设为私有
    8.   
    9. 确保对任何可变组件的独占访问。
    10.   

    我认为复合主键类非常适合此规则。

  2. 根据JPA 2.0规范第11.1.47章时间注释,为@Temporaljava.util.Date个持久字段指定java.util.Calendar注释:

      

    必须为持久字段指定Temporal注释   java.util.Date和java.util.Calendar类型的属性。它可能只是   为这些类型的字段或属性指定。

    使用java.util.Datejava.util.Calendar@Temporal类型添加注释 指示与JDBC驱动程序通信时要使用的相应JDBC java.sql类型(即TemporalType.DATE映射到java.sql.Date等)。当然,如果您在实体类中使用java.sql类型,则不再需要注释(我不知道您的代码片段中的这些类型)。