[译文]JPA的实施模式:双向关联

原文:JPA implementation patterns: Bidirectional Associations

作者:Vincent Partington

出处:http://blog.xebia.com/2009/03/16/jpa-implementation-patterns-bidirectional-assocations/

 

 

上周我们通过数据访问对象模式开始了对JPA实施模式的探索,本周我们继续研究另一个麻烦的问题。

JPA提供了@OneToMany@ManyToOne@OneToOne@ManyToMany等注解来映射对象之间的关联,而EJB 2.x则是提供了容器管理的关联关系(container managed relationships)来管理这些关联,特别是要保持双向关联的同步,JPA留出了更多由开发者来决定的空间。

 

设置

 

我们先使用一个OrderLine对象来扩充前一篇博客中的Order例子,它有编号(id)、说明(description)、价格(price)和一个到包含了它的order的引用:

 

@Entity

public class OrderLine {

@Id

@GeneratedValue

private int id;

 

private String description;

 

private int price;

 

@ManyToOne

private Order order;

 

public int getId() { return id; }

public void setId(int id) { this.id = id; }

 

public String getDescription() { return description; }

public void setDescription(String description) { this.description = description; }

 

public int getPrice() { return price; }

public void setPrice(int price) { this.price = price; }

 

public Order getOrder() { return order; }

public void setOrder(Order order) { this.order = order; }

}

 

通过使用泛型DAO模式,我们很快就得到一个非常基本的OrderLineDao接口和一个实现:

 

public interface OrderLineDao extends Dao<Integer, OrderLine> {

public List<OrderLine> findOrderLinesByOrder(Order o);

}

 

public class JpaOrderLineDao extends JpaDao<Integer, OrderLine> implements

           OrderLineDao {

 

public List<OrderLine> findOrderLinesByOrder(Order o) {

           Query q = entityManager.createQuery("SELECT e FROM "

                             + entityClass.getName() + " e WHERE order = :o");

           q.setParameter("o", o);

           return (List<OrderLine>) q.getResultList();

}

}

 

我们可以使用这一DAO来把订单项(orderline)添加到订单(order)中,或者是从一个订单中找出所有的订单项。

 

OrderLine line = new OrderLine();

line.setDescription("Java Persistence with Hibernate");

line.setPrice(5999);

line.setOrder(o);

orderLineDao.persist(line);

 

Collection<OrderLine> lines = orderLineDao.findOrderLinesByOrder(o);

 

关联越多问题越多

 

所有这一切都非常简单,不过如果我们把这一关联变成双向的话,事情就开始有些意思了。让我们在Order对象中添加一个orderLines域,并加入单纯的getter/setter方法对的实现。

 

@OneToMany(mappedBy = "order")

private Set<OrderLine> orderLines = new HashSet<OrderLine>();

 

public Set<OrderLine> getOrderLines() { return orderLines; }

public void setOrderLines(Set<OrderLine> orderLines) { this.orderLines = orderLines; }

 

@OneToMany注解中的mappedBy域被用来告知JPA这是关联的另一端,因此它可以通过查看OrderLine对象的order域来获知其与哪一个Order对象相配,而不是把该域直接映射到数据库中的列上。

因此在不改变底层数据库的情况下,我们现在可以检索订单(order)的订单项(orderline),如下所示:

 

Collection<OrderLine> lines = o.getOrderLines();

 

不再需要访问OrderLineDaoJ

 

但是这里存在着一个圈套!虽然EJB 2.x定义的容器管理的关联关系(CMR)确保在添加OrderLine对象到OrderOrderLines属性中的时候,也设置了该OrderLineorder属性,但是JPA(作为一个POJO框架)并没有施展这样的魔法。

这实际上是一件好事,因为这使得我们的领域模型在JPA容器之外依然可用,这意味着你能够更容易地对它们进行测试,以及在它们还没有被持久时就可以使用他们。不过这对那些习惯了EJB 2.x CMR做法的人来说,可能也会感到有些混淆。

如果在单独的事务中运行前面的例子,你会发现他们运行正常,但是如果在类似以下代码实现的事务中运行它们的话,你会发现订单项的列表却是空的:

 

Order o = new Order();

o.setCustomerName("Mary Jackson");

o.setDate(new Date());

 

OrderLine line = new OrderLine();

line.setDescription("Java Persistence with Hibernate");

line.setPrice(5999);

line.setOrder(o);

 

System.out.println("Items ordered by " + o.getCustomerName() + ": ");

Collection<OrderLine> lines = o.getOrderLines();

for (OrderLine each : lines) {

           System.out.println(each.getId() + ": " + each.getDescription()

                             + " at $" + each.getPrice());

}

 

这可以通过在第一个System.out.println语句之前添加以下一行来修正:

 

o.getOrderLines().add(line);

 

改进再改进……

例子可用,但是不够完美,其破坏了抽象,而且脆弱,因为它依赖于领域对象的用户正确地调用这些setter方法和adder方法。我们可以通过把这些调用移到OrderLine.setOrder(Order)的定义中来解决这一问题:

 

public void setOrder(Order order) {

           this.order = order;

           order.getOrderLines().add(this);

}

 

更好的做法是可以以一种更好的方式来封装Order对象的orderLines属性:

 

public Set<OrderLine> getOrderLines() { return orderLines; }

public void addOrderLine(OrderLine line) { orderLines.add(line); }

 

然后按照以下方式重新定义OrderLine.setOrder(Order)

 

public void setOrder(Order order) {

           this.order = order;

           order.addOrderLine(this);

}

 

还理解我的意思吗?希望如此,如果没有跟上我的想法的话,请你自己试试看。

现在另一个问题出现了,如果有人直接调用Order.addOrderLine(OrderLine)方法会怎样呢?OrderLine会被添加到orderLines集合中,但是它的order属性并没有指向它所属的订单(order)。像以下这样来修改Order.addOrderLine (OrderLine)是无效的,因为这将会导致addOrderLine调用setOrdersetOrder反过来调用addOrderLineaddOrderLine又调用setOrder这样一个一直下去的无限循环。

 

public void addOrderLine(OrderLine line) {

           orderLines.add(line);

           line.setOrder(this);

}

 

这个问题可以通过引入一个Order.internalAddOrderLine(OrderLine)方法来解决,该方法添加订单项(line)到集合中,但不调用line.setOrder(this)OrderLine.setOrder(Order)随后会调用该方法,这样就不会导致一个无限循环。Order类的用户应该调用Order.addOrderLine(OrderLine)

 

模式

 

可以预料采取这一想法得到的结果是,OrderLine类最终的方法是这样的:

 

public Order getOrder() { return order; }

 

public void setOrder(Order order) {

           if (this.order != null) { this.order.internalRemoveOrderLine(this); }

           this.order = order;

           if (order != null) { order.internalAddOrderLine(this); }

}

 

以及Order类最终的方法是这样的:

 

public Set<OrderLine> getOrderLines() { return Collections.unmodifiableSet(orderLines); }

 

public void addOrderLine(OrderLine line) { line.setOrder(this); }

public void removeOrderLine(OrderLine line) { line.setOrder(null); }

 

public void internalAddOrderLine(OrderLine line) { orderLines.add(line); }

public void internalRemoveOrderLine(OrderLine line) { orderLines.remove(line); }

 

这些方法提供了对创建在EJB 2.x之内的CMR逻辑的一种基于POJO的实现,有着带有典型POJO特征的易于理解、测试和维护的优点。

当然关于这一主题会存在一些变数:

Ÿ           如果OrderOrderLine位于同样的包中的话,那么可以赋予内部的(internal…)方法封装范围来防止它们无意中被调用。(这就是C++的友元类(friend class)概念可派上用场的地方,再说一次,别用它。)

Ÿ           如果永远都不需要从订单中删除订单单项的话,那么可以去掉removeOrderLineinternalRemoveOrderLine方法。

Ÿ           基本倒转一下想法,可以从OrderLine.setOrder(Order)方法中去掉负责管理到Order类的双向关联的部分,不过那就意味着要通过addOrderLineremoveOrderLine方法来传播逻辑了。

Ÿ           作为替代,或者是另外再使用Collections.singletonSet来把orderLine设置成在运行时是只读的,还可以使用泛型类型来使得它在编译时是只读的。

 

public Set<? extends OrderLine> getOrderLines() { return Collections.unmodifiableSet(orderLines); }

 

不过,这会使得更难以通过EasyMock一类的模拟框架来模拟这些对象。

 

在使用这一模式时还有一些事情要考虑:

Ÿ           添加一个OrderLineOrder中并不会自动持久它,你还需要调用其DAO(或者EntityManager)的持久方法来完成这一工作,或者可以通过设置Order.orderLines属性上的@OneToMany注解的cascade属性的值(至少)为CascadeType.PERSIST来达到这一目的。我们在讨论EntityManager.persist方法时会谈到更多关于这方面的情况。

Ÿ           双向关联与EntityManager.merge方法之间的配合不是很好,我们会在进入游离对象这一主题时讨论该问题。

Ÿ           当一个作为双向关联的组成部分的实体(即将)被删除时,也应该从该关联的另一端中去掉它,在我们讨论EntityManager.remove方法时,这种情况也会提到。

Ÿ           以上模式只有在使用域访问(field access)(而不是属性/方法访问)来让你的JPA提供程序填充实体时才有效,当实体的@Id注解被放置在相应域而不是放在相应getter方法之上时,就是使用了域访问。

Ÿ           最后但并非是最不重要的是,尽管该模式可能是一个技术可靠的基于POJO的管理关联的实现,不过你可能会争辩说为什么会需要所有的这些gettersetter方法?为什么既要能使用Order.addOrderLine(OrderLine)又要能使用OrderLine.setOrder来获得相同的结果呢?去掉其中的一个可使得我们的代码变得更简单些,比如可看一下James Holub的关于gettersetter方法的文章。还有就是,我们发现这一模式赋予了使用这些领域对象的开发者按照他们的意愿关联对象的灵活性。

 

那么,今天到这就结束了,我非常有兴趣听取你们关于这一模式的意见以及你们是如何管理JPA领域对象中的双向关联的。当然,我们会在探讨下一个模式时再见!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值