很多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 表的模式
NAME | PK? | TYPE | NULL? |
---|---|---|---|
CUST_ID | Y | INTEGER | NOT 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 | TIMESTAMP | NOT 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. 将数据取出并组装成一个对象
....
OpenJPA EntityManager oem = OpenJPAPersistence.cast(em);
Object objId = oem.getObjectId(customer);
Customer cust = em.find(Customer.class, objId);
....
由于我们无法预知主键的值,程序必须将 EntityManager
强制转化为 OpenJPA EntityManager
,然后将刚刚存进数据库的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. 单表继承映射策略
ENTITY | TABLE NAME |
---|---|
Customer | CUSTOMER |
OnlineCustomer | CUSTOMER |
Customer
实体拥有 custId
, firstName
, lastName
, custType
, 和 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. 连接表继承映射策略
ENTITY | TABLE NAME |
---|---|
Customer | CUSTOMER |
OnlineCustomer | ONLINECUSTOMER (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. 一表一类继承映射策略
ENTITY | TABLE NAME |
---|---|
Customer | CUSTOMER |
OnlineCustomer | ONLINECUSTOMER |
由于不同的实体总是存放在不同的表中,因此你无须提供 @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 中的数据间的关系,和面向对象中的一样优雅。