理解JPA,第二部分:JPA中的关系

来源:互联网 发布:premium知乎 编辑:程序博客网 时间:2024/04/30 14:07

     你用JAVA写的web程序非常依赖于数据之间的关系,如果你处理不好的话,结果将会变得非常糟糕。在这篇文章中,作者将向你展示,如何利用JPA的标注,在面向对象代码与关系数据之间创建一个透明的接口。最终的数据关系将会更容易管理,并且更具备可移植性。


     数据对于任何一个应用程序来讲都是必不可少的,而数据之间存在的关系也具有同样的重要性。关系型数据库能够支持数据表之间的各种关系,并且还要满足完整性约束。

     在这个系列文章的下半部分中,你将了解到如何使用JPA以及Java5的标注来按照面向对象的方式处理数据间的关系。这篇文章面向的读者是那些掌握了基本的JPA概念,了解一般的关系型数据库编程,以及那些想要更深入地了解使用JPA来进行面向对象的关系设计的人们。对于JPA的简单介绍,请参考本系列文章的上半部分。

一个现实生活中的例子

    假设有一家名叫XYZ的公司,为它的顾客提供5中商品,分别是A、B、C、D、E。顾客可以自由的同时订购多种商品(可以享受折扣优惠),也可以订购单一的商品。在订购商品的时候顾客不用付钱。在月底的时候,如果顾客对商品很满意,将会收到公司开出发票从而进行付款。这家公司的数据模型如图1所示,一个消费者可以下0个或多个订单,每个订单包含1个或多个商品。对于每个订单,将会产生一张发片用于付账。

Diagram of a data model

图 1. 数据模型

     现在XYZ公司想要调查一下顾客对他们的商品满意度如何,所以他们必须首先调查一下每一个顾客拥有多少商品。为了改进商品的质量,公司还需要对那些撤销过订单的客户做一个特别的调查。

     为了实现这个目标,比较传统的方法是构建一个DAO层,然后在 CUSTOMER, ORDERS, ORDER_DETAIL,ORDER_INVOICE, 和PRODUCT这些表之间做复杂的连接查询。这种设计模式表面上看还不错,但是随着应用程序规模的增长,很难维护和调试。

     JPA提供了一种更为简洁优雅的方式来解决这一问题。我所提出的解决方案采用了面向对象的方法,同时感谢JPA,我不用写任何SQL查询,持久层供应商把这些复杂的工作都做了,这对于开发者是透明的。

     你最好先从资源区下载样例代码,其中包含了本文中介绍的“1对1”,“多对1”,“1对多”还有“多对对”关系映射代码。

 

One-to-one 关系

     首先,我们的程序要解决“订单-发票”之间的关系。对于每一个订单,都将有一个发票,同理,每一个发票也要关联到一张订单上。这两个表之间是一种 one-to-one 的映射关系,如图2所示,利用 ORDER_ID 这个外键进行关联。JPA使用 @OneToOne 这个标注来处理 one-to-one 映射关系。

 

Diagram of a one-to-one relationship

图 2. A one-to-one 关系

     应用程序会针对每一个发票ID来取出相应的订单数据。如清单1所示,发票实体的属性与INVOICE表的字段是完全对应的,同时发票实体还拥有一个订单对象来和 ORDER_ID 外键做关联。

清单 1. 一个样例实体,描述了 one-to-one 关系

@Entity(name = "ORDER_INVOICE") 
public class Invoice {

@Id
@Column(name = "INVOICE_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long invoiceId;

@Column(name = "ORDER_ID")
private long orderId;

@Column(name = "AMOUNT_DUE", precision = 2)
private double amountDue;

@Column(name = "DATE_RAISED")
private Date orderRaisedDt;

@Column(name = "DATE_SETTLED")
private Date orderSettledDt;

@Column(name = "DATE_CANCELLED")
private Date orderCancelledDt;

@Version
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
@OneToOne(optional=false)
@JoinColumn(name = "ORDER_ID")
private Order order;

...
//getters and setters goes here
}

     清单1中的 @OneToOne 和  @JoinCloumn 标注将会被持久层提供商进行内部处理,如清单2所示:

清单 2. 处理 one-to-one 关系的SQL查询

SELECT t0.LAST_UPDATED_TIME, t0.AMOUNT_PAID, t0.ORDER_ID, 
t0.DATE_RAISED ,t1.ORDER_ID, t1.LAST_UPDATED_TIME, t1.CUST_ID,
t1.OREDER_DESC, t1.ORDER_DATE, t1.TOTAL_PRICE
FROM ORDER_INVOICE t0
INNER JOIN ORDERS t1 ON t0.ORDER_ID = t1.ORDER_ID
WHERE t0.INVOICE_ID = ?

     清单2中的查询语句显示了 ORDERS 和 INVOICE 表之间的内连接,但是如果你需要的是一个外连接该怎么办?你可以通过修改 @OneToOne 标注的 optional 属性来很容易的设置到底采用哪种连接方式,该参数的默认值是true,意味着相关联的对象可以存在也可以不存在,从而采用外连接的方式。但是在我们的例子中,每一个订单必然会有一张发票,反之也是一样,所以我们应该把该属性的值设为false。

     清单3中的代码演示了如何根据制定的发票找到相关的订单。

清单 3. 根据 one-to-one关系取出一个对象

....
EntityManager em = entityManagerFactory.createEntityManager();
Invoice invoice = em.find(Invoice.class, 1);
System.out.println("Order for invoice 1 : " + invoice.getOrder());
em.close();
entityManagerFactory.close();
....

     但是如果你指定一个订单,想要取出相关的发票,这时候会发生些什么呢?

反向 one-to-one 关系

每一个关系都有两端:

  • 其中一端叫 owning ,负责在数据库中更新关系。一般来讲这一端都包含外键。
  • 另一端叫做 inverse ,它将映射到 owning 端。

     在我们的例子中,发票对象是 owning 端,清单4展示了 inverse 端—— 也就是订单对象——的样子。

清单 4. 反向 one-to-one关系中的实体

@Entity(name = "ORDERS") 
public class Order {

@Id
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;

@Column(name = "CUST_ID")
private long custId;

@Column(name = "TOTAL_PRICE", precision = 2)
private double totPrice;

@Column(name = "OREDER_DESC")
private String orderDesc;

@Column(name = "ORDER_DATE")
private Date orderDt;

@OneToOne(optional=false,cascade=CascadeType.ALL,
mappedBy="order",targetEntity=Invoice.class)
private Invoice invoice;

@Version
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;

....
//setters and getters goes here
}

     清单4中的代码通过 mappedBy="order" 来使得关系映射到order字段。 targetEntity 属性指明了owning类的名字。还有一个叫cascade的属性,如果你在对订单实体进行插入、更新或删除操作的时候,希望能够同时对发票实体进行操作,则需要设置这个属性。

     清单5展示了如何根据制定的订单信息来找到发票对象。

Listing 5. Fetching objects involved in a bidirectional one-to-one relationship

....
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Invoice details for order 111 : " + order.getInvoice());
em.close();
entityManagerFactory.close();
....

Many-to-one 关系

    在刚才的内容中,你学到了如何通过给定的订单来找到相关的发票信息。现在我们来看看另一个问题,如何根据某一个客户来找到相关的订单信息,反之也是一样。一个客户可以拥有0个或多个订单,但一个订单只能关联到一个客户。因此,客户和订单之间是一种 one-to-man的关系,同理,订单和客户之间就是 many-to-one 的关系,如图3所示:

Diagram of a many-to-one/one-to-many relationship.

图 3.  many-to-one/one-to-many 关系

     现在,订单实体是owning端,将 CUST_ID 作为外键与客户实体做关联。清单6订单实体是如何处理 many-to-one 关系的。

清单 6. 实现了反向 many-to-one 关系的实体

@Entity(name = "ORDERS") 
public class Order {

@Id //signifies the primary key
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;

@Column(name = "CUST_ID")
private long custId;

@OneToOne(optional=false,cascade=CascadeType.ALL,
mappedBy="order",targetEntity=Invoice.class)
private Invoice invoice;

@ManyToOne(optional=false)
@JoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
private Customer customer;


...............
The other attributes and getters and setters goes here
}


     在清单6中,顾客实体通过 CUST_ID 作为外键连接到订单实体。在这里 optional 属性的值仍然为 false,因为每个订单必须有一个相关联的顾客。现在,订单实体已经和两个实体做了关联,分别是与发票实体的 one-to-one 关联,以及和顾客实体的  many-to-one 关联。

     清单7展示了如何根据一个特定的订单来找到相关联的顾客。

清单 7. 利用 many-to-one 关系来取得一个对象

........
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Customer details for order 111 : " + order.getCustomer());
em.close();
entityManagerFactory.close();
........

     但是如果给定一个顾客,想取出相关联的订单信息该怎么办?

One-to-many 关系

     只要owning端进行了恰当的设计,那么根据顾客来查找相关订单信息是非常容易的。在前面的内容中,你看到订单实体被设计为owning端,拥有一个 many-to-one 关系,而反过来的话则变成一个 one-to-many 的关系。清单8展示了这种 one-to-many 关系是如何来定义的。

清单 8. 包含了 one-to-many 关系的实体

@Entity(name = "CUSTOMER") 
public class Customer {
@Id //signifies the primary key
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;

@Column(name = "FIRST_NAME", length = 50)
private String firstName;

@Column(name = "LAST_NAME", nullable = false,length = 50)
private String lastName;

@Column(name = "STREET")
private String street;
@OneToMany(mappedBy="customer",targetEntity=Order.class,
fetch=FetchType.EAGER)
private Collection orders;

...........................
// The other attributes and getters and setters goes here
}

     在清单8中的 @OneToMany 标注中,出现了一个新的属性:fetch。对于 one-to-many 关系来讲,默认的fetch策略是LAZY。 FetchType.LAZY 是JPA的默认设置,表明程序将延迟加载相关信息,直到你真正访问这些信息的时候才进行加载,这就是所谓的 lazy loading。延迟加载是完全透明的,当你访问相关内容的时候,程序自动访问数据库来为你获取相关信息。另外一种fetch策略是 FetchType.EAGER,他的含义是无论何时只要你取得了一个实体,那么这个实体中所有的字段都将立刻从数据库中取出。如果想要使用EAGER策略,那么你必须明确指定 fetch=FetchType.EAGER 。清单9中的代码是根据特定的顾客来取出相关的订单信息。

清单 9.  根据 one-to-many 关系来取出对象

........
EntityManager em = entityManagerFactory.createEntityManager();
Customer customer = em.find(Customer.class, 100);
System.out.println("Order details for customer 100 : " + customer.getOrders());
em.close();
entityManagerFactory.close();
.........

Many-to-many 关系

     还有最后一种关系映射需要考虑,一个订单可以包含多种商品,同时一个商品也可以出现在一个或多个订单当中,这就是所谓的 many-to-many 关系,如图4所示:

Diagram of a many-to-many relationship.

图 4.  many-to-many 关系

     为了定义 many-to-many 关系,我们的程序需要引入一个名叫 ORDER_DETAI 的连接表来保存订单和商品之间的联系,如清单10所示:

清单 10. 包含 many-to-many关系的实体

@Entity(name = "ORDERS") 
public class Order {
@Id
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;

@Column(name = "CUST_ID")
private long custId;

@Column(name = "TOTAL_PRICE", precision = 2)
private double totPrice;

@OneToOne(optional=false,cascade=CascadeType.ALL, mappedBy="order",
targetEntity=Invoice.class)
private Invoice invoice;

@ManyToOne(optional=false)
@JoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
private Customer customer;

@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name="ORDER_DETAIL",
joinColumns=
@JoinColumn(name="ORDER_ID", referencedColumnName="ORDER_ID"),
inverseJoinColumns=
@JoinColumn(name="PROD_ID", referencedColumnName="PROD_ID")
)
private List<Product> productList;

...............
The other attributes and getters and setters goes here

}

     @JoinTable 标注用于指定数据库中的一个表来保存订单ID和商品ID之间的联系。指定了 @JoinTable 的实体是owning端。在我们这个例子中,订单实体是owning端。

     清单11展示了如何根据订单取得相关的商品信息。

清单 11. 利用 many-to-many 关系取得信息

..........
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("testjpa");
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Product : " + order.getProductList());
em.close();
entityManagerFactory.close();
..........

反向 many-to-many 关系

     一个商品可以被包含在多个订单当中。一旦你已经在owning端做好了映射,那么inverse端的映射将变得非常容易,如清单12所示:

清单 12. 拥有反向 many-to-many 关系的实体

@Entity(name = "PRODUCT") 
public class Product {
@Id
@Column(name = "PROD_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long prodId;

@Column(name = "PROD_NAME", nullable = false,length = 50)
private String prodName;

@Column(name = "PROD_DESC", length = 200)
private String prodDescription;

@Column(name = "REGULAR_PRICE", precision = 2)
private String price;

@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
@ManyToMany(mappedBy="productList",fetch=FetchType.EAGER)
private List<Order> orderList;

...............
The other attributes and getters and setters goes here
}

     清单13展示了如何根据商品来取得相关的订单信息。

清单 13. 利用反向 many-to-many 关系取得数据

..........
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("testjpa");
EntityManager em = entityManagerFactory.createEntityManager();
Product product = em.find(Product.class, 2000);
System.out.println("Order details for product : " + product.getOrderList());
em.close();
entityManagerFactory.close();
..........

最终展示

     现在,请整理一下你的思路,回顾一下本文开头讲过的内容,这个公司XYZ想要查找如下信息:

  • 每一个顾客所购买的商品数目
  • 每一个顾客都撤消了哪些商品

     既然关系已经定义好了,只需要几行代码就能够实现这些功能,请看清单14

清单 14. 把所有的关系综合到一起

..............
Query query = em.createQuery("SELECT customer FROM CUSTOMER customer");
List list= query.getResultList();

for(Customer customer:list){
List prodList = new ArrayList();
List prodListCancelled = new ArrayList();
if(customer.getOrders()!=null){
for(Order allOrders: customer.getOrders()){
if(allOrders.getInvoice().getOrderCancelledDt() == null){
//Find out how many products each customer has
prodList.addAll(allOrders.getProductList());
}else{
//Find out how many products cancelled by each customer
prodListCancelled.addAll(allOrders.getProductList());
}
}
}
}
..............

     清单14的代码中,我们从CUSTOMER表中取出了所有的顾客信息。对于每一个顾客,我们取出订单的数量,然后过滤掉那些没有被撤销的订单,然后把这些内容添加到 prodList 中。同时我们每一个被用户撤销的订单中的商品数目,然后把这些信息添加到 prodListCancelled 中。

     因此 prodList 包含了用户使用中的商品,而 prodListCancelled 包含了用户撤销的商品。有这些数据在手,XYZ公司可以很容易地知道顾客最喜欢的商品是什么。

解密数据关系

     本文的样例程序被设计成展示各种各样的数据关系。在本文中,你能够看到在多种关系表之间使用JPA的面向对象能力来处理复杂的CRUD操作。这样就能够彻底降低企业级应用程序在查询方面的复杂度,使得程序更容易维护。

     JPA是一种简单的标准的数据持久层API,在JAVA SE 和 JAVA EE中都能够使用。希望这个简单的例子能够帮助你更好的学习JPA。

 

原创粉丝点击