jpa学习笔记

 理解JPA,第一部分:面向对象的数据持久化方案

 

    很多JAVA项目在处理数据持久化的时候,都努力想寻找一种很自然的面向对象的方式。JPA,作为JSR220的一个产物,提供了一种标准化的操作方式。这个介绍JPA的系列文章一共有2部分,在第一部分里,您将了解到JPA是如何使得数据持久化操作融入到你的面向对象架构当中的。


Why JPA?
     对于许多JAVA开发者来说,都会问到同一个问题:“为什么要推出JPA,既然Hibernate和Toplink等技术已经非常成熟,我为什么还要学习JPA?”答案很简单,JPA并不是一项新技术,更确切地说,它综合了Hibernate、Toplink和JDO等各种数据持久化技术的精髓,从而产生一个标准的规范来处理数据持久层,这样就不依赖任何一个特定的产品提供商。

    不管你是否喜欢,数据都是任何一个应用程序中不可缺少的一部分,尤其是哪些面向对象的应用程序。JAVA程序员在处理数据持久层的时候,比较传统的方式是写一些复杂的SQL查询语句,但是随着应用程序规模的不断增长,这些内容会使得程序变得难以管理。如果能够用面向对象的方式来处理这些查询,充分运用“封装”、“抽象”、“继承”和“多态”等特性,这将是多么美妙的一件事情啊。

    事实上,JAVA社区已经开发出很多种面向对象的方式来处理数据持久化:EJB,JDO,Hibernate还有Toplink都是非常不错的解决这一问题的方案。而JPA,则是java EE 5规定的标准的持久化应用程序接口。JPA规范一开始是作为JSR 220:EJB 3.0规范的一部分,目的是简化EJB中实体bean编程模型。尽管它和Java EE 5.0中的实体bean相关,但是在容器之外,在java SE环境中,JPA也是可以使用的。

    在这篇文章中,你将会看到,借助于JPA中的标注,使用面向对象的方式处理数据持久化,是多么的简洁和优雅。这篇文章面向的读者是那些JPA的初学者,同时需要掌握一些基本的关系型数据库概念以及熟悉JAVA 5中的标注。JPA需要JAVA 5或者更高版本,因为它大量使用了JAVA中的新特性,比如标注和泛型。

OpenJPA以及样例程序

     在这篇文章中,我们将使用OpenJPA来进行演示,它是由Apache组织提供的一个JPA规范的具体实现。我之所以选择OpenJPA而不是其他供应商的产品,主要是因为它被集成在Weblogic、WebSphere和Geronimo等应用服务器当中。在我撰写本文的时候,OpenJPA的最新版本是1.0.1,可以通过Resources section这个链接来下载。如果你想使用其他的JPA实现,那么很明显你首先要读一读相关文档。

     在本文余下的部分当中,我将通过一个例子向您介绍JPA中的各种概念。这个例子是基于一个名叫XYZ的超市,既有网上店铺也有实体零售店。一开始,你将了解到如何使用JPA对客户模型进行CRUD操作。在后面的部分,你将了解到如何通过对象继承的方式来扩展CRUD操作。

     本文代码包包含了实体监听器以及文章中讨论的三种继承类型(单表、连接、每个类一个表)的代码。

JPA: 如何使用?

为了实现一个JPA兼容的程序,你需要如下三样东西:

  • 一个实体类
  • 一个 persistence.xml 文件
  • 一个功能类,用于完成插入、更新或者查找一个实体

     JPA只能用于处理数据持久化,下面让我们来看看如何通过JPA来设计数据的存储方式。假设你已经有一个 CUSTOMER 表, 如表1所示:

表 1. CUSTOMER 表的模式
NAMEPK?TYPENULL?
CUST_IDYINTEGERNOT NULL
FIRST_NAME VARCHAR(50)NOT NULL
LAST_NAME VARCHAR(50) 
STREET VARCHAR(50) 
APPT VARCHAR(20)NOT NULL
CITY VARCHAR(25) 
ZIP_CODE VARCHAR(10)NOT NULL
CUST_TYPE VARCHAR(10)NOT NULL
LAST_UPDATED_TIME TIMESTAMPNOT NULL

用于持久化的对象: 实体

     既然JPA是用于处理“实体-关系”映射的,那么接下来你就应该设计一个Customer实体对象。实体对象没有什么特别的,就是一个POJO类在加上一个 @Entity 标注,如清单1所示:

清单 1. Customer实体
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity(name = "CUSTOMER") //Name of the entity
public class Customer implements Serializable{
    private long custId;
    private String firstName;
    private String lastName;
    private String street;
    private String appt;
    private String city;    
    private String zipCode;
    private String custType;
    private Date updatedTime;

// Getters and setters go here 
......................
}

     Customer实体需要知道如何把他的属性映射到CUSTOMER表中。有两种方式可以做到,要么写一个名叫orm.xml的配置文件,要么如清单2所示,利用JPA的标注。

清单 2. 带有标注的 Customer 实体
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

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

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

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

// By default column name is same as attribute name
    private String street;

    @Column(name = "APPT",nullable = false)  
    private String appt;

    // By default column name is same as attribute name
    private String city;

    @Column(name = "ZIP_CODE",nullable = false)  
    // Name of the corresponding database column
    private String zipCode;

    @Column(name = "CUST_TYPE", length = 10)
    private String custType;

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

// Getters and setters go here 
......................
}

让我们来仔细看看清单2中使用的标注。

  • 所有的标注都在 javax.persistence 中定义,所以你必须包含这个java包。
  • @Enitity 指明某一个类为实体类。如果实体的名字和表的名字不同,则要使用 @Table 标注;反之则不需要。
  • 如果属性的名字和表中相应列的列名不同,则需要使用 @Column 标注 (默认情况下,这两者的名字应该是相同的)
  • @Id 指明主键。
  • @Version 指明实体中的版本字段。JPA 使用版本字段来检测对于数据的并发修改。当JPA检测到有多个操作同时修改一个数据,它将向最后提交的事务抛出一个异常。这将保护你先前提交的事务能够稳定地提交数据。
  • 默认情况下,所有的字段都是 @Basic 类型的,将按原样保存到数据库中。
  • @GeneratedValue 指明一种策略来为ID字段分配一个唯一的值。可选的策略有 IDENTITY, SEQUENCE, TABLE, 和 AUTO。默认的策略是 auto,具体的实现有JPA提供商来完成。(OpenJPA 是用序列来实现的)

当你创建一个实体类的时候,如下几点需要牢记:

  • JPA 允许持久化类继承自非持久化类、持久化类继承自持久化类、非持久化类继承自持久化类。
  • 实体类必须有一个默认的无参数构造函数。
  • 实体类不能是 final 的。
  • 持久化类不能继承自某些特定的系统类,例如java.net.Socket 和 java.lang.Thread
  • 如果一个持久化类继承自一个非持久化类,那么父类中的属性是不能被持久化的。

持久化单元

     既然实体类已经完成,接下来就该处理 persistence.xml 配置文件了。 列表3所示的XML文件位于 META-INF 文件夹中; 它被用于指定持久化提供商的名字,实体类的名字,还有一些系统属性,例如数据库的URL、驱动、用户、密码等等。

列表 3. persistence.xml 文件样例
<?xml version="1.0"?>
<persistence>
    <persistence-unit name="testjpa" transaction-type="RESOURCE_LOCAL">
        <provider>
            org.apache.openjpa.persistence.PersistenceProviderImpl
        </provider>
        <class>entity.Customer</class>
        <properties>
            <property name="openjpa.ConnectionURL" 
                      value="jdbc:derby://localhost:1527/D:/OpenJPA/Derby/testdb;create=true"/>
            <property name="openjpa.ConnectionDriverName" 
                      value="org.apache.derby.jdbc.ClientDriver"/>
            <property name="openjpa.ConnectionUserName" value="admin"/>
            <property name="openjpa.ConnectionPassword" value="admin"/>
    <property name="openjpa.Log" value="SQL=TRACE"/>
        </properties>
    </persistence-unit>
</persistence>

关于 persistence.xml 文件有如下重要内容需要注意:

  • persistence.xml 可以包含多个持久化单元。每一个持久化单元都可以被不同的JPA提供商使用,或者能够被用来操作不同的数据库
  • JPA提供商的名字在 <provider>标签中指定。OpenJPA 的提供商的名字是org.apache.openjpa.persistence.PersistenceProviderImpl.
  • 实体类的名字在 <class> 标签中指定。
  • 数据库连接属性可以在 <properties>标签中指定。注意各个提供商之间property name是不同的。
  • OpenJPA 拥有默认的日志能力,其默认级别是 INFO。

真正的展示

     既然准备工作已经完成,接下来我们就要写一个类,来向CUSTOMER表中插入一条记录,如清单4所示:

清单 4. 对象持久化的样例代码
public static void main(String[] args) {
    EntityManagerFactory entityManagerFactory =  Persistence.createEntityManagerFactory("testjpa");
    EntityManager em = entityManagerFactory.createEntityManager();
    EntityTransaction userTransaction = em.getTransaction();

    userTransaction.begin();
    Customer customer = new Customer();
    customer.setFirstName("Charles");
    customer.setLastName("Dickens");
    customer.setCustType("RETAIL");
    customer.setStreet("10 Downing Street");
    customer.setAppt("1");
    customer.setCity("NewYork");
    customer.setZipCode("12345");
    em.persist(customer);
    userTransaction.commit();
    em.close();
    entityManagerFactory.close();
}

     让我们仔细研究一下清单4中的代码都是什么含义。首先出场的是 Persistence 类。javadoc 中说: "Persistence 类是一个自举类,用于获得 EntityManagerFactory类。" 就像这样:

EntityManagerFactory emf=Persistence.createEntityManagerFactory("testjpa");

Persistence 类的工作十分简单:

  • 在类路径中,它搜索META-INF/services/directory中存在的javax.persistence.spi.PersistenceProvider文件。它从每一个文件中读取PersistenceProvider实现类的名字。
  • 之后它利用persistenceUnitName为每一个 PersistenceProvider 调用 createEntityManagerFactory()函数,直到得到一个EntityManagerFactory。OpenJPA的提供商名字是org.apache.openjpa.persistence.PersistenceProviderImpl

     PersistenceProvider是如何得到正确的EntityManagerFactory呢?这个有提供商来实现。

    EntityManagerFactory 是一个工厂类用于产生 EntityManager在整个应用程序中,EntityManagerFactory 应该被缓存,并且针对每一个持久化单元,它只应调用一次。

    EntityManager用于管理实体,它负责进行添加、更新和删除。你无须一个事务就能找到一个实体,当然,如果你要进行添加、更新或删除操作的话,还是应该位于一个事务之中的。

     如果你在进行获取操作的时候没有位于一个事务当中,实体就不会处于受管状态。因此,每一次获取记录的操作,系统都会对数据库进行访问。在这种情况下,每一次操作都是独立的,系统访问一次数据库只会做一件事情,而不是积攒许多事情之后一起做。

     剩余的代码都很好懂,首先创建了一个 customer 对象,给相应的属性设置正确的值,然后将对象插入到数据库中,如清单5所示:

清单 5. 将对象持久化的代码片段
EntityTransaction userTransaction = em.getTransaction();
    userTransaction.begin();
    em.persist(customer);
    userTransaction.commit();

     你可能已经注意到了,在我们的代码中,并没有明确的设置custId属性和updatedTime属性。因为主键的产生策略是AUTO,JPA提供商会小心地计算出主键。同样地,版本字段(在我们的例子中是updatedTime属性)也是由JPA提供商自动来计算生成的。

     现在你需要找到你刚才插入的记录。利用主键找到一个记录是非常简单的,如清单6所示:

清单 6. 将数据取出并组装成一个对象
....
OpenJPAEntityManager oem = OpenJPAPersistence.cast(em);
Object objId = oem.getObjectId(customer);
Customer cust = em.find(Customer.class, objId);
....


     由于我们无法预知主键的值,程序必须将 EntityManager 强制转化为 OpenJPAEntityManager,然后将刚刚存进数据库的customer对象传递给它,从而获得主键的值。对于这种操作,不同的提供商要写的代码可能不一样!!

一个复合主键

     现在我们来研究一下,如果一个实体的主键是由多个字段复合而成的,我们该如何通过主键来取得整条记录呢?

清单 7. 一个ID类
public class CustomerId {
    public String firstName;
    public String lastName;
    // override equal() method
    //override hascode() method
..................
}

     CustomerId 类可以是一个独立的类,也可以是一个内部类。如果它是一个内部类,那么它必须是static的,同时必须被实体类引用,就像清单8所展示的那样。很明显,在取得整条记录的时候,无论是复合主键还是单一主键,其操作都是一样的。

清单 8. 使用ID类
@Entity
@IdClass(Customer.CustomerId.class)
public class Customer implements Serializable{
    @Id
    @Column(name = "FIRST_NAME", nullable = false, length = 50)
    private String firstName;

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

    private String street;

    @Column(name = "APPT",nullable = false)  
    private String appt;

................
}

回调函数

     为了便于在持久化操作的各个阶段处理一些事情,JPA 提供了回调函数。设想一下,你要更新一个客户的信息,而这个客户是本地人,所以你需要删除其zip码中的连字符,或者是在你取得一条记录之后,你要填写一些临时信息。在获取、插入和更新操作的前后,JPA 提供了监听器来帮你完成这些事情。对于回调函数,可以按照如下的方式进行标注:

  • @PostLoad
  • @PrePersist
  • @PostPersist
  • @PreUpdate
  • @PostUpdate
  • @PreRemove
  • @PostRemove

     你可以在实体类中直接写回调函数,也可以把回调函数写在一个单独的类中,然后让实体类通过@EntityListeners 标注引用这个类,如清单9所示:

清单 9. 实现回调函数
@EntityListeners({CustListner.class})
@Entity(name = "CUSTOMER") //Name of the entity
public class Customer implements Serializable{
...
...
}
public class CustListner {
    @PreUpdate
    public void preUpdate(Customer cust) {
         System.out.println("In pre update");
    }
    @PostUpdate
    public void postUpdate(Customer cust) {
         System.out.println("In post update");
    }
}

内嵌对象

     正如你目前所看到的,在Customer实体中,地址信息是分为street、attp、city等几个字段位于其中。如果你想把地址信息提炼为一个单独的类,然后在Customer实体中引用这个类,该怎么做呢?毕竟,一个地址对象可以被用在许多类中,比如Customer、Employee、Order或者User等等。

     只要使用内嵌对象就能实现这个要求。你把地址信息提炼到一个单独的类中,然后将哪个类标注为“可内嵌的”,正如清单10所示,然后在Customer实体中通过 @Embedded 标注来引用这个地址类。

清单 10. 一个可内嵌的类
@Embeddable
public class Address implements Serializable{

    private String street;

    @Column(name = "APPT",nullable = false)  
    private String appt;

    private String city;
..
..
}

     内嵌类和他的拥有者一同被映射为实体的某些状态。当然,它们不能被单独的查询。清单11就是一个使用了内嵌对象的实体。

Listing 11. A sample entity using an embedded object
@Entity
public class Customer {
...............
    @Column(name = "FIRST_NAME", nullable = false,length = 50)
    private String firstName;

    @Embedded
    private Address address;
..............
}

继承的力量

一个实体可以从如下几种方式中进行继承:

  • 另一个实体——无论它是具体的还是抽象的
  • 另一个非实体,它能够提供一些行为或非持久化状态。从这里继承而来的属性仍然是不可持久化的。
  • 映射过的超类,它能够提供一些公共的实体状态。数据库中不同的表可能会拥有相似的字段,但是这些表之间全没有任何关系,这时候就可以用这种继承方式。

     下面让我们来看看 JPA 中提供的不同的继承方式。就我们这个例子而言,假设有2中不同类型的客户:1、普通客户,他们只在实体零售店中购买物品;2、在线客户,她们通过Internet在网店中购买物品。

单表继承

     所谓单表继承,就是说整个继承架构中的所有实体都被存放在同一个表中。单表继承是默认策略。因此,对于清单12中的代码,你可以省略掉 @Inheritance这个标注,结果是完全一样的。

     在我们的例子程序中,无论是普通用户还是在线用户,都被存放在CUSTOMER表中,如表2所示:

表 2. 单表继承映射策略
ENTITYTABLE NAME
CustomerCUSTOMER
OnlineCustomerCUSTOMER

     Customer 实体拥有 custIdfirstNamelastNamecustType, 和 address 等信息,对于 OnlineCustomer 实体,除了它所特有的 website 属性外,其余属性一律继承自 Customer 类。这个策略应该被反映在超类中,如清单12所示:

清单 12. 单表继承中的超类
@Entity(name = "CUSTOMER") 
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="CUST_TYPE", discriminatorType=DiscriminatorType.STRING,length=10)
@DiscriminatorValue("RETAIL")
public class Customer implements Serializable{
    @Id 
    @Column(name = "CUST_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long custId;

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

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

    @Embedded
    private Address address = new Address();

    @Column(name = "CUST_TYPE", length = 10)
    private String custType;
................
}

     就目前的代码而言,你可以暂时不理睬 DiscriminatorColumn 和 DiscriminatorValue 标注,它们的功能我们在后面介绍。OnlineCustome 实体将是一个普通的实体,他继承自 Customer 类,如清单13所示:

清单 13. 单表继承中的子类
@Entity(name = "ONLINECUSTOMER") //Name of the entity
@DiscriminatorValue("ONLINE")
public class OnlineCustomer extends Customer{
    @Column(name = "WEBSITE", length = 100)
    private String website;
    ............
}

     现在你必须要创建一个 Customer 对象和 OnlineCustomer 对象,然后将它们持久化,如清单14所示:

清单 14. 在单表继承中持久化对象
......................
userTransaction.begin();
//inserting Customer
Customer customer = new Customer();
customer.setFirstName("Charles");
customer.setLastName("Dickens");
customer.setCustType("RETAIL");
customer.getAddress().setStreet("10 Downing Street");
customer.getAddress().setAppt("1");
customer.getAddress().setCity("NewYork");
customer.getAddress().setZipCode("12345");
em.persist(customer);
//Inserting Online customer
OnlineCustomer onlineCust = new OnlineCustomer();
onlineCust.setFirstName("Henry");
onlineCust.setLastName("Ho");
onlineCust.setCustType("ONLINE");
onlineCust.getAddress().setStreet("1 Mission Street");
onlineCust.getAddress().setAppt("111");
onlineCust.getAddress().setCity("NewYork");
onlineCust.getAddress().setZipCode("23456");
onlineCust.setWebsite("www.amazon.com");
em.persist(onlineCust);
userTransaction.commit();
......................

     执行了上述代码后,如果你去数据库中看一看 CUSTOMER 表,你会发现增加了2条记录。清单15所示的查询语句将返回数据库中的在线客户信息。

清单 15. 在单表继承中查询子类信息
..............
Query query = em.createQuery("SELECT customer FROM ONLINECUSTOMER customer");
List<OnlineCustomer> list= query.getResultList();
.................

     如果 CUSTOMER 表中既存储了Customer类,又存储了OnlineCustomer类,那么JPA是如何分辨它们的呢?JPA是如何只取得在线客户的信息的呢?实际上,如果你不给出一点点提示的话,JPA确实无法进行区分,这就是@DiscriminatorColumn 标注的重要作用。它告诉 CUSTOMER 表那一个字段是用来区分 CUSTOMER 和 ONLINE CUSTOMER 的。@DiscriminatorValue 指出什么样的值来区分 CUSTOMER  和 ONLINE CUSTOMER。@DiscriminatorValue 标注需要同时在超类和子类中标明。

     当你需要查询在线客户的时候,JPA 默默的按照清单16所展示的语句进行查询。

清单 16. 区分单表中的不同对象
SELECT t0.CUST_ID, t0.CUST_TYPE, t0.LAST_UPDATED_TIME, t0.APPT, t0.city, t0.street, t0.ZIP_CODE, 
t0.FIRST_NAME, t0.LAST_NAME, t0.WEBSITE FROM CUSTOMER t0 WHERE t0.CUST_TYPE = 'ONLINE'

连接表继承

     在连接表继承策略中,公共状态被存放在一个表中,子类特有的状态被存放在另一个表中,这两个表按照某种关系进行连接,如表3所示:

表 3. 连接表继承映射策略
ENTITYTABLE NAME
CustomerCUSTOMER
OnlineCustomerONLINECUSTOMER (only Website information is stored here; the rest of the information is stored in the CUSTOMER table)

     OnlineCustomer 和 Customer 的公共信息被存放在 CUSTOMER 表中, OnlineCustome 所特有的信息被存放在ONLINECUSTOMER 表中,这两个表利用外键约束进行连接。从JPA实现的立场来看,在 OnlineCustomer 实体中唯一需要做出的调整是应该提供一个 JOINED策略,如清单17所示:

清单 17. 连接表继承中的超类
@Entity(name = "CUSTOMER") //Name of the entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="CUST_TYPE", discriminatorType=DiscriminatorType.STRING,length=10)
@DiscriminatorValue("RETAIL")

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

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

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

    @Embedded
    private Address address = new Address();

    @Column(name = "CUST_TYPE", length = 10)
    private String custType;
    .................
}

     在清单18所示的 OnlineCustomer 实体中,你必须指出子类所特有的属性,还要用 @PrimaryKeyJoinColumn 标注出作为外键的字段。

清单 18. 连接表继承中的子类
@Table(name="ONLINECUSTOMER")
@Entity(name = "ONLINECUSTOMER") //Name of the entity
@DiscriminatorValue("ONLINE")
@PrimaryKeyJoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
public class OnlineCustomer extends Customer{

    @Column(name = "WEBSITE", length = 100)
    private String website;
    ................
}

     在清单18中,@PrimaryKeyJoinColumn 的name属性指明了超类中的主键,referencedColumnName 指明了子类中用哪个属性去和超类做连接。对于 Customer 或 OnlineCustomer对象的存储或读取操作,则没有任何变化。

一表一类继承

     在一表一类继承策略中,每一个类的信息都存放在一个单独的表中,如表4所示:

表 4. 一表一类继承映射策略
ENTITYTABLE NAME
CustomerCUSTOMER
OnlineCustomerONLINECUSTOMER

     由于不同的实体总是存放在不同的表中,因此你无须提供 @DiscriminatorColumn 标注。同样地,由于子类和超类之间不存在任何表关联,所以 @PrimaryKeyJoinColumn 标注也是不需要的。Customer 超类的内容如清单19所示:

清单 19. 一表一类继承中的超类
@Entity(name = "CUSTOMER")
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Customer implements Serializable{
    @Id //signifies the primary key
    @Column(name = "CUST_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long custId;

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

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

    @Embedded
    private Address address = new Address();
    ...........
}

    如清单20所示,OnlineCustomer 和一个普通的子类没什么两样。对于 Customer 或 OnlineCustomer对象的存储或读取操作,同样没有任何变化。

清单 20.  一表一类继承中的子类
@Entity(name = "ONLINECUSTOMER") //Name of the entity
public class OnlineCustomer extends Customer{

    @Column(name = "WEBSITE", length = 100)
    private String website;
    .................
}

通向更广阔的面向对象世界

     在本文中,你了解到了如何在JPA中应用面向对象中的“继承”以及回调函数。还有更多的面向对象能力可以在JPA中使用,JPA还允许你写JPQL查询语句或者原生的SQL查询,还有事务管理能力等等。

     在下一篇文章中,你将会看到JPA中的数据间的关系,和面向对象中的一样优雅。

 

 

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

553个读者  华丽的痘痘 @ yeeyan.com 2009年07月04日  双语对照  原文  字体大小   

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


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

     在这个系列文章的下半部分中,你将了解到如何使用JPA以及Java 5的标注来按照面向对象的方式处理数据间的关系。这篇文章面向的读者是那些掌握了基本的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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值