文章来源:http://www.builder.com.cn/2006/0315/231398.shtml
Tags: EJBEJB | JPAJPA | AnnotationsAnnotations | HibernateHibernate
2007-1-31 18:21:48 | 编辑
Java标准版的EJB Persistence
自从起草EJB 3.0的规范开始,无论是在客户端还是在服务器端的应用程序里,Java的类就一直有一种单一的、标准的persistence机制。Java 5的Annotations(批注)功能很容易使用。本文将介绍如何使用它。
保持Java类;现在已经有很多方式做到这一点,而适用于所有应用程序的一个标准的出现很可能让所有的开发人员从中受益。但其中的挑战就是弥合Java标准版和企业版之间的差别,形成一个标准的API,从而能够用于运行在托管容器里的企业应用程序,也能用于希望管理自己的、无容器的标准应用程序。现在我们有了JSR-220——Enterprise Java Beans 3.0规范。随着JSR-220的发展,它分成两个部分:EJB3.0 persistence和EJB 3.0 core(以及其他)。
EJB3.0 persistence与先前的EJB persistence不同,它在Java 5.0里加入了Annotations(批注)以及各种POJO persistence开发人员的经验。
在编写EJB3.0 Persistence的时候,它的规范还在“最后的草案”阶段,有些内容还有可能发生改变。尽管如此,现在就是你研究一下规范里很多实现的好机会;参考实现可以在Glassfish里找到,而另外一个是实现Hibernate的Annotation和EntityManager项目。
现在让我们从基本的东西开始:如何让一个类在Java SE里面保持连续性。下面就是一个简单的例子:
public class Address {private String street;
private String postcode;
public Address() {}
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
public String getPostcode() { return postcode; }
public void setPostcode(String postcode) { this.postcode = postcode; }
}
为了把类放进一个能够被持久保持的实体,我们在一开始就加入了@Entity批注,从而让其能够保持持久,就像下面这样:
import javax.persistence.Entity;@Entity
public class Address {
…
现在,如果你得第一反应是“嗨,什么是@Something”,那么就让我简单介绍一下批注。从历史上讲,Java一直缺少在代码里嵌入元数据的方式。Javadoc是一种源数据,用于生成文档,使用方法是在注释块里使用前缀为@的标记。这一概念建立在Xdoclet基础之上,人们用它来承载自己的元数据。另外一种替代方式是用于类的匹配XML文件,例如Hibernate除了使用类以外,还利用一个.hbm.xml文件为同名的类承载自己的映射信息。这两种技术都很好用,但是对于Java 5来说,人们更希望在运行时实现看到一种更能够验证和发现的内联和编译器;这就是Annotations出现的原因。在本文里,你需要知道批注是什么,如何添加元数据,如何持久地映射一个类。
@Entity批注的作用是标记用来保持的类。为了便于更容易检索,POJO会给每一个需求配备一个主键。这样做的一种常见方法是引入一个长字段来作为这个键。所以我们会把这个加到类上面:
private Long id;@Id
@GeneratedValue
public Long getId() { return id; }
public void setId(Long id) { this.id=id; }
@Id批注表示主键字段的获得者是谁,而@GeneratedValue批注会要求persistence层为这个字段生成一个值。这就是我们需要做的所有事情;我们现在就可以开始使用Address(地址)了。
Address address=new Address();address.setPostcode("ZZ9 99Z");
address.setName("John Doe");
如果要保持Address,我们就需要为处理保持数据所需的每个工作单元取得一个EntityManager。Java EE和Java SE下的persistence在这一点上有很大的不同。在Java EE里,会有很多批注让周围的框架来管理实体。在Java SE里,这一任务就交给了开发人员,由他们来获得EntityManager。我们从EntityManagerFactory取得了一个EntityManager。
EntityManagerFactory emf=null;…
EntityManager em=emf.getEntityManager();
我们以后再回头讲是从哪里获得EntityManagerFactory的;现在,我们就假设它已经被初始化,我们能够从其中获得EntityManager。当你保持Java SE的时候,你还有责任管理数据库的事务;你必须开始和结束数据库的事务,所以就让我们从获得一个事务开始吧:
EntityTransaction tx=em.getTransaction();tx.begin();
现在我们可以要EntityManager保持我们的Address。
em.persist(address);然后执行事务,关闭EntityManager;
tx.commit();em.close();
完成之后,会有一个id被分配给我们的Person,它将被写入到数据库里。获得和执行事务的这种方式对于任何用来修改对象保持状态的代码来说都是相当常见的。为了保证可靠性,如果出现像下面这样的问题,它应该提示出现异常,并进行回滚。
EntityManager em=emf.getEntityManager();EntityTransaction tx=em.getTransaction();
try {
tx.begin();
// Do saves or modifications here
tx.commit();
} catch (Exception e) {
if(tx.isActive()) tx.rollback();
System.err.println("Error:"+e);
} finally {
em.close();
}
假设这段代码是放在下面用来修改数据库的例子的周围。如果要检索数据,我们只需要获得EntityManager就行了。
要检索先前保存的Address,我们可以把id用作参考;我们不需要来自EntityManager的事务,所以这个过程就变得很简单了:
Address address2=em.find(Address.class,address.getId());如果我们想要通过邮政编码找到一个Address,我们可以使用EJB查询语言——EJBQL来定义查询,用EntityManager来创建这一查询。
Query q=em.createQuery("select address from Address as address where postcode=:param");这个查询会着手查找邮政编码符合要求的地址(Address被保持,返回的结果被作为Address对象),也就是符合“param”参数为“to be set”的地址。现在我们可以设置这个参数:
q.setParameter("param",name);要获得结果,就要对查询调用getResultList,如果我们希望得到0个或者更多的结果,或者我们只希望获得1个结果的话。
List<Address> l=(List<Address>)q.getResultList();or
Address addresstochange=(Address)q.getSingleResult();我们最后要做的事情是更改Address(地址),并把它的改变合并到数据库里;假设我们已经通过先前的查询检索到了一个Address。
addresstochange.setStreet("A Different Street");
…
em.merge(addresstochange);
…
下面是另外一个POJO——Person,我们已经对它进行了批注,以便保持;
@Entitypublic class Person {private Long id;private String name;private Address address; public Person() {} @Id@GeneratedValuepublic Long getId() { return id; }public void setId(Long id) { this.id=id; } public String getName() { return name; }public void setName(String name) { this.name=name; } @ManyToOnepublic Address getAddress() { return address; }public void setAddress(Address address) { this.address = address;}}
上面这段代码中重要的一部分是用于地址字段的@ManyToOne批注。@ManyToOne批注是用于描述实体之间关系的一系列批注之一;在本文里,它表示很多个人可能参考的是一个地址。这就要求参考与一个已经保持了的地址相对应。
Person person=new Person();person.setName("John Doe");person.setAddress(address);…em.merge(person);…
Merge方法能够智能地处理交给它的实体。如果实体是非托管的,就保持它;否则它就会处理存在的差异,并根据需要对数据库进行更新。这用来处理正在被保持的Person。
这就是保持的基础概念。当然这要排除我们假设已经存在的EntityManagerFactory。这就是轮子碰到马路的情形。下面就是创建它的一个典型调用。
EntityManagerFactory emf=Persistence.createEntityManagerFactory("example");
我们要Persistence层根据“example”这个名字创建一个工厂。用来创建这个工厂的配置信息来自一个叫做persistence.xml的文件,这个文件应该在你源码树的META-INF目录下。它定义了一个带有名字的“persistence-unit”,它有应该如何管理persisitence的细节,底层的哪个persistence提供程序将完成这项任务,以及要用到哪个数据库。下面就是这个代码段:
<?xml version="1.0" encoding="UTF-8"?><persistence
xmlns="http://java.sun.com/xml/ns/persistence"><persistence-unit
name="example"><provider>org.hibernate.ejb.HibernatePersistence<
/provider><class>quick.Person</class><class>quick.Address</class><
properties>…</properties></persistence-unit></persistence>
这里,我们有一个名为“example”的persistence-unit,我们把这个名字交给createEntityManagerFactory()。我们联合使用Hibernate的Annotations和EntityManager,因此<provider>元素指向HibernatePersistence提供程序。你可以有一个或者多个<class>元素,每一个都指向一个你想要保持的类。在这个例子里,你可以看到用于Person和Address的类元素。这里还有一个<properties>元素,它包含有用于设置实现具体值的<property>元素,这些值会被传递用来创建EntityManagerFactory。在我们的例子里,我们使用Hibernate和嵌入的HSQLDB。我们需要设置JDBC驱动器来使用URL,以连接到数据库并告诉Hibernate要使用什么样的SQL语句:
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/> <property name="hibernate.connection.url" value="jdbc:hsqldb:data/example"/> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
嵌入的HSQLDB要用“sa”用户名和空白密码才能启动。我们还想要限制hibernate取得相关对象的程度。
<property name="hibernate.connection.username" value="sa"/> <property name="hibernate.connection.password" value=""/> <property name="hibernate.max_fetch_depth" value="3"/>
最后是hbm2ddl.auto属性。这用来控制Hibernate在SQL数据库里动态地创建所需表格的能力。如果被设置为“创建(create)”,它就会在每次运行的时候清除数据库并建立新的表格。如果设置为“更新(update)”,它就会尝试修改原有表格以适合当前版本的实体;
<property name="hibernate.hbm2ddl.auto" value="create"/>
生成DDL来创建数据库是大多数实现里应该都有的东西,但是是否开启了这一功能就要看各个实现的具体情况了。
有了这个XML文件,你现在就有了足够的信息供createEntityManagerFactory创建简单的数据库。你会在本文附带的源代码里发现这个persistence.xml和示例代码。要让它运行起来,你就需要正确的库。首先,你需要从Hibernate的web网站获得HSQLDB数据库、Hibernate、Hibernate-Annotations以及Hibernate-EntityManager。查看一下自述文件,看看你所需要的库可以从哪里获得。一旦让示例代码运行起来了,你就可以开始试验创建和保持一个简单的实体。如果想要看给persistence.xml属性加入SQL之后到底什么被执行,就要运行下面的代码;
<property name="hibernate.show_sql" value="true"/>
那么Hibernate将在执行每个SQL语句的时候进行转储。
现在,我们已经知道了保持的基本概念和一些简单的映射。以后,我们将再看一些可用的批注,以及如何保持集合然后研究一下Glassfish里面的参考EJB persistence实现。
你可以下载本教程的源代码。
资源
在Java persistence系列文章的第二篇里,我们将探讨一下如何提供对象之间的双向关系。
在本教程的上篇里,我们讲到了使用EJB3 persistence——现在也叫做Java Persistence API(JPA)——保持对象的基础知识。我们利用Hibernate的EntityManager/Annotations实现让简单的Person和Address类保持到嵌入的HSQLDB里。但是Person和Address这两个类之间是单向关系:一个Person指向一个Address,所以让我们来看看如何实现双向映射。在Address里,我们准备加入一系列住在该地址的Person——居民(residents):
public class Address {...private Set<Person> residents=new HashSet<Person>();
现在一个Address可能指向多个Person,所以我们加入一个@OneToMany批注来访问这些居民所对应的方法:
@OneToManypublic Set<Person> getResidents() {return residents;} public void setResidents(Set<Person> residents) {this.residents = residents;}
当我们给某个个人设置地址的时候,为了维护它们之间的关系,我们要获得这些居民,并把每个个人添加到set里。除非你想要通过上篇里的示例代码访问一个地址上的多个居民,就像下面这样……
Person p=new Person();p.setName("John Doe");p.setAddress(address);savePerson(p);address.getResidents().add(p);…
——否则甚至在你想要保持更改之前,你会碰到一个暂缓初始化(lazy initialisation)错误。当你检索一个Address对象时集合没有被取回;当你真正访问该字段的时候,它们才会被取回。这就是暂缓初始化。但是只有当对象还没有被从EntityManager里分离开的时候才会出现暂缓初始化。利用上篇里的代码,我们忽略掉被分离的对象,因为我们创建和关闭了每个数据访问方法里的EntityManager。现在,你可以通过更改对residents的批注来实现这一目的……
@OneToMany(fetch=FetchType.EAGER)public Set<Person> getResidents() {…
……但是这会迫使persistence层在对象被检索的时候总是取得所有的相关数据,一般来说我们不推荐这么做:过度使用它会导致大量的对象树被放到内存里。如果想要强制加载,你可以使用EJBQL的fetch关键字来实现这一目的。我们可以更改对findByPostcode的查询,并添加“left join fetch address.residents”来强制加载residents属性,就像下面这样:
Query q=em.createQuery("select address from Address as addressleft join fetch address.residents where postcode=:param ");
集合一般都会被暂缓取回,而大多数的其它字段都会被立即取回,这就是为什么当我们检索一个Person的时候,它的address属性会包含一个address对象。
即使我们做了这些改变,但还是存在另外一个问题;我们会碰到来自Person的瞬态对象异常。这是因为我们的savePerson方法没有控制好赋予它的Person,即使它通过EntityManagermerge()来保持它。在被给予一个新建立的实例时,merge()创建了一个新的受控对象,并把数据复制到这个新的受控对象里。
这就是对象的生命周期:当你创建一个全新的对象时,它就处于新的/瞬时状态;当你保持它的时候,它就会在你保持EntityManager时进入受控状态;当EntityManager被关闭的时候,实例就被分离。你可以利用合并把实例重新附加到另一个EntityManager上,或者通过分离对象的id使用EntityManager的find方法获得一个全新的版本。还有一个状态——删除,即当对象被从EntityManager的挂起删除的时候。现在,你可能会疑惑,为什么会有这些不同的状态?嗯,有了它们就不需要再有数据传输对象(Data Transfer Object,DTO)和那些专门用来把返回的数据移动到应用程序更高层次的类了。这样能够被分离的只有那些保持类了,而不会有数据传输对象和保持类。
回到示例代码,我们可以通过更改savePerson()来使用persist(),并添加一个非相异方法(not dissimilar method)updatePerson(),它使用合并和updateAddress()为Address进行合并。现在我们可以完成这个地址分配代码:
Person p=new Person();p.setName("John Doe");p.setAddress(address);savePerson(p);address.getResidents().add(p);updateAddress(address);
在从每个address的居民里删除或者添加Person之后,利用updateAddress()就可以把一个Person从一个Address移动到另外一个Address。但是移动应该是一个原子操作,所以我们要实现一个moveTo方法。我们就从“取得EntityManager和事务”这个前提开始吧:
private void moveTo(Person p,Address a){EntityManager em=emf.createEntityManager();EntityTransaction tx=em.getTransaction();tx.begin();
我们要做的是使用EntityManager的find方法来获得实体的可控版本;
Person managedperson=em.find(Person.class,p.getId());Address managedaddress=em.find(Address.class,a.getId());
在事务里检索可控实例的时候,我们从中检索到的内容也是可控的,所以可以获得当前地址的一个可控版本:
Address managedoldaddress=managedperson.getAddress();
现在,由于已经有了可控版本,所以我们可以像下面这样操控它们:
managedoldaddress.getResidents().remove(managedperson);managedaddress.getResidents().add(managedperson);managedperson.setAddress(a);
现在我们必须进行这些改变。由于我们只操控了可控的版本,所以我们需要做的就是进行事务以保持更改,地方就是从当前的EntityManager和事务里:
tx.commit();em.close();}
现在让我们看看在数据库里创建了哪些表格。正如你所预料的,有Person和Address表格。你可能没有料到的是还有一个Address_Person表格,生成它是为了映射居民set和resident_id以及address_id字段。所有这些表格和字段名都源于类的命名。在声明实体的时候,你可以用@Table批注来替代它,例如;
@Entity@Table(name="Location")public class Address{
这段代码会把Address类保持到一个名为Location(地点)的表格里。如果看一下数据库里生成的字段,你会发现数据列的名字都来源于类的属性名。类似的,你可以用@Column批注来替代保持字段的名字。如果是在Address类里,你可以插入
…@Column(name="postalcode")public String getPostcode() { return postcode; }
那么postcode(邮政编码)属性将被保持在一个叫做postalcode的数据列里。我们还可以用@Columnal设置保持字段的其它数据库属性,包括唯一性、长度、和真正数据库数据列的精度。它有用的时候(即使是要依靠默认的名字生成)是当所生成的数据列名字与SQL的关键字不相符的时候:例如,我们有一个叫做“select”的字段,那么你会碰出错,因为数据库会拒绝格式不正确的SQL,所以把数据列的名字替换为“my_select”就能够解决这个问题。
现在让我们回到先前生成的Address_Person表格。我们能够通过向Address里的@OneToMany批注加入一个mappedBy参数来删除这个表格,就像下面这样:
@OneToMany(mappedBy="address")public Set<Person> getResidents() {
这是在告诉persistence层要使用Person的address字段来映射居民set,而且不要生成Address_Person表格。Address和Person之间的关系已经得到了加强。有了这种映射,我们可以用address属性集来保持Person。在检索Address的时候,我们通过选择相关Person记录的映射来填充它的居民set。当然,你可能不想看到两个实体之间保持如此亲密的关系。
现在QuickExample里有一个功能更多的映射,所以就让我们转到另外一个不同的话题上吧。EJB3/JPA的优势应该是我们能够很容易就移动到一个不同的persistence库。要做到这一点,就让我们修改一下前面的示例代码,以便使用EJB3/JPA的Glassfish参考实现。
从理论上讲,我们应该只需要修改persistence.xml文件就可以使用Glassfish了,就像下面这样:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"><persistence-unit name="example"><provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
</provider><class>quick.Person</class><class>quick.Address</class>
<properties><property name="jdbc.driver" value="org.hsqldb.jdbcDriver"/><property name="jdbc.connection.string"value="jdbc:hsqldb:data/example"/><property name="toplink.platform.class.name"value="oracle.toplink.essentials.platform.database.HSQLPlatform"/>
<property name="jdbc.user" value="sa"/><property name="jdbc.password" value=""/>
<property name="ddl-generation" value="dropandcreate"/><property name="toplink.logging.level" value="FINE"/>
</properties></persistence-unit></persistence>
提供程序被改为toplink版本,而属性被改为用于驱动程序、连接、用户名和密码的toplink变量。替代Hibernate的dialect属性的是“toplink.plafrom.class.name”属性,而“ddl-generation”属性则替换掉Hibernate的“hbm2ddl.auto”属性,“toplink.logging.level”属性被设置为“fine”,并替换了Hibernate的“showsql“属性,而SQL语句被作为普通记录的一部分记录下来。
对于构建库,我们删掉了所有的Hibernate库,用来自Glassfish的库替换它们;antlr.jar、asm.jar、asm-attrs.jar、javaee.jar和toplink-essentials.jar等可以在 Glassfish的lib目录下找到。你可以从Glassfish的网站上下载和安装它。由于我们只能够使用来自Glassfish的库,所以就不需要运行Glassfish应用服务器了。但是你在运行代码的时候还需要进行一个调整,这是Toplink独特的要求——你需要注入代理,你需要toplink-essentials-agent.jar,而且在运行的时候,你需要在Java运行命令行里将“-javaagent:{path to the jar}/toplink-essentials-agent.jar”加为Java虚拟机的参数。
现在就应该准备好运行了。就同以往一样,由于我们还处在确定标准的阶段,所以TopLink和Hibernate的EntityManager/Annotations的行为之间可能还存在一些差别。在Glassfish的示例代码里,你会发现我们进行了下面的改变。
我们给Person.address的批注加入了@JoinColumn("address_id"),以解决Toplink无法正确处理默认这个很显著的问题。我们不得不用@Column(name="ADDRESS_ID")和@Column(name="PERSON_ID")明确地指定Address和Person的Id属性,否则Toplink就会同时调用这两个“ID”,并为Address_Person表格建立一个奇怪的“ID”数据列表格,而且连错误或者警告的提示都没有。
我们还需要修改“findBy”方法,这样它们在where语句的参考里就更加具体了:例如,“where person.name=:param”,而不是“where name=:param”,因为Toplink对命名方式更加挑剔。
最后,我们必须用“for(Person pi:ax.getResidents()) System.out.println(pi);”替换掉exercise方法里的“System.out.println(ax.getResidents())”。这是因为Toplink的暂缓加载实现有一个toString方法,它不会打印出set的内容,所以我们必须在set里迭代。通过这个变化,我们就能够运行示例代码。
要真正运行代码,我们就要把底层的数据库从嵌入的HSQL改为MySQL服务器。我们会假设你已经设置好了MySQL服务器,而且已经下载了MySQL JDBC驱动器,并且把它添加到你的库里了。在这种情况下,我们要做的就是更改persistence.xml里的属性:
…<property name="jdbc.driver" value="org.gjt.mm.mysql.Driver"/>
<property name="jdbc.connection.string"value="jdbc:mysql://localhost:3306/example"/>
<property name="toplink.platform.class.name"value="oracle.toplink.essentials.platform.database.MySQL4Platform"/>
<property name="jdbc.user" value="root"/><property name="jdbc.password" value=""/>…
我们告诉persistence层要使用MySQL JDBC驱动程序,并使用来自Toplink的MySQL4Platform的类。这里的设定是本地MySQL服务器,并带有一个没有密码的根帐号;你应该根据实际需要替换掉用户名和密码。有了我们的简单示例代码,值得注意的是,运行数据之后的延迟是可以解决的;没有必要担心嵌入的数据库会在外部SQL服务器接手的情况下丢失数据。正如你看到的,选择使用什么数据库是可以在部署阶段进行的。
处于比较的目的,Hibernate的Entitymanager/Annotations使用MySQL的相应属性是:
…<property name="hibernate.connection.driver_class"value="org.gjt.mm.mysql.Driver"/><property name="hibernate.connection.url"value="jdbc:mysql://localhost:3306/example"/>
<property name="hibernate.dialect"value="org.hibernate.dialect.MySQLDialectDialect"/>
<property name="hibernate.connection.username" value="root"/><property name="hibernate.connection.password" value=""/>…
本月,我们探讨了EJB3/JPA里可用实体之间的更多批注和映射,把示例扩展到了Glassfish/Toplink,而且切换了数据库。下一个月,我们将研究一下如何让保持对象变得更容易,并告诉你如何使用EJB3/JPA来测试企业类,而不需要在单个的测试内调用整个框架。
你可以下载本教程的源代码。
在前几部分里,我们已经讨论过了Java SE的JPA的基本保持元素。在本文里,我们将看一个示例应用程序,并详细讨论如何在你的开发中应用JPA。
首先,让我们来看看示例应用程序的要求,这个程序可以在这里下载。这是一个关于许可证管理的应用程序。在这个例子里,有很多应用程序,每个程序都有多个版本,每个版本都有一个或者多个与之相关的许可证。还有一组用户,他们可能与这许可证中的任何一个相关联。我们想要创建一个能够管理所有这些元素的应用程序。
现在就让我们从实体开始。它们都在自己的程序包里,而没有与应用程序的代码混在一起。这样做是值得的;在大型项目里,你可以将实体作为单独的项目来处理,这样就可以更容易地在其他项目里重复使用它们。我们创建了4个实体:Application、Version、Licence和User,所以让我们具体看看每个实体的作用。
在Application类里,我们与Version类具有一对多的关系。下面是Application方法的一部分;我们跳过了其中的id和name属性,因为它们与我们先前讨论过的内容类似。
@Entity
public class Application {
...
private List<Version> versions=new ArrayList<Version>();
...
@OneToMany(mappedBy="application",cascade=CascadeType.ALL)
public List<Version> getVersions() {
return versions;
}
...
}
上个月,我们讲过了mappedBy参数。本文里的新东西是cascade(层叠)参数。这个cascade参数用来控制persistence引擎进行操作从而影响数据库其他表格的能力。在默认情况下是没有层叠的,所以对集合的更改要求你明确地管理集合的内容。查看一下其他CascadeType的值会发现其中隐含的操作:ALL、PERSIST、MERGE、REMOVE、REFRESH。例如,设置CascadeType.PERSIST将只会层叠保持对象,所以如果一个新的Version实例被加到版本列表里,那么更新Application实例就会进行层叠操作,以便在底层数据里保存新的Version。CascadeType.MERGE会应用相同的规则来进行更新,而CascadeType.REMOVE同样会删除集合里的内容。CascadeType.REFRESH用来从数据库里重新读取实例进行层叠操作;我们将在后面讨论它。
我们现在来看Version类。我们已经有了与Application的多对一(@ManyToOne)关系以及另外一个层叠集合,现在我们就来看看许可证。
@Entity
public class Version {
...
private Application application;
private Set<Licence> licences=new HashSet<Licence>();
...
@ManyToOne
public Application getApplication() {
return application;
}
...
@OneToMany(mappedBy="version",cascade=CascadeType.ALL)
public Set<Licence> getLicences() {
return licences;
}
...
}
顺着程序的思路走下来,我们看到了Licence类。层叠在这里结束。
@Entity
public class Licence {
...
private Version version;
private Set<User> users=new HashSet<User>();
...
我们还有一个集合来表示一组用户。到版本的映射由@ManyToOne批注来处理。
@ManyToOne
public Version getVersion() {
return version;
}
现在我们来到了这个示例中最重要的映射部分;很多许可证可以参考很多用户,所有我们使用@ManyToMany(多对多)批注来表示这种情况。
@ManyToMany
@JoinTable(name="LicenceUsers",
joinColumns=,
inverseJoinColumns=)
public Set<User> getUsers() {
return users;
}
}
@JoinTable标注让我们能够控制@ManyToMany并替代其默认值。Name参数是我们将要创建用来保存很多映射的表格的名称。JoinColumns参数可以让我们设置join。你可能想要知道为什么我们给它的自变量加了括号;这个自变量类型是数组,所以尽管只有一个值,我们仍需要加括号。其中的值是一个@JoinColumn标注,它用来设置数据库里数据列的名称。相同的句法可以用于inverseJoinColumns参数,虽然是多对多的关系。
最后,我们来看看User类。在本文的例子里,我们假设用户通过其唯一的用户名来识别。因此我们可以将其作为标识符,而不用@GeneratedId来生成我们可以用在其他实体类里的唯一ID值。
@Entity
public class User {
private String userName;
private List<Licence> licences=new ArrayList<Licence>();
public User() {}
@Id
public String getUserName() {
return userName;
}
...
现在我们可以看看如何创建一个到许可证的映射,这是一种多对多(ManyToMany)关系,当然也就是@ManyToMany。
@ManyToMany(mappedBy="users")
public List<Licence> getLicences() {
return licences;
}
...
}
这里我们使用mappedBy参数指向Licence类的用户属性。这让persistence引擎能够使用我们在Licence里指定的@JoinTable。
这就是实体;现在我们转到如何操控它们的话题上。我们编写的这个例子基于Java Standard Edition平台,这样我们可以像先前文章里那样创建一个manager类,并从这个类实例化EJB/JPA层。下面是我们要创建的一个单独的LicManStore.java类。
public class LicManStore {
private EntityManagerFactory emf;
private EntityManager em;
private static LicManStore myInstance;
public static LicManStore getStore() {
if(myInstance==null) {
myInstance=new LicManStore();
}
return myInstance;
}
private LicManStore() {
emf=Persistence.createEntityManagerFactory("appman",
new Properties());
em=emf.createEntityManager(PersistenceContextType.EXTENDED);
}
...
这个构造函数与前面的例子有一个明显的不同之处:它使用了PersistenceContextType.EXTENDED。在默认情况下,EntityManagers用TRANSACTION的PersistenceContextType来创建。这样做也就表示,只有当有活动的事务处理在进行时,实体才是可托管的。事务处理一结束,实体就与实体管理程序脱离,这样我们就可以丢弃它。EXTENDED上下文类型表示这种脱离不会发生,即使在事务处理结束后实体仍然是可托管的。这就意味着你不需要担心集合是否被暂缓取回,因为实体管理程序可以用来完成所需要的取回操作。当我们想要保持和更新/合并实体,或者从数据库里删除实体的时候,我们仍然需要获得EntityTransaction,例如我们想要保存一个新的Application实体:
public void saveApplication(Application a) {
EntityTransaction tx=em.getTransaction();
tx.begin();
em.persist(a);
tx.commit();
}
如果你看一下LicManStore的源代码,你会发现Application只有保存、更新和删除操作,User只有保存和删除操作,而Licence和Version没有方法。它们没有被忘记,但是也不需要它们。要记住,我们对Application和Version以及Licence之间的关系设置了层叠。这意味着我们要做的就是更新Application,而Version和Licence所需要的所有操作都已经完成了。如果你观察一下Controller.java的代码,你会看到上面的过程。首先让我们来创建一个Application:
void addApplication(String appname) throws UpdateException {
if(licmanStore.getApplication(appname)!=null)
throw new UpdateException("An application "
+ appname + " already exists");
Application app=new Application();
app.setApplicationname(appname);
licmanStore.saveApplication(app);
populateApplications();
ui.setSelectedApplication(app);
}
这里没有什么特别值得注意的;populateApplications方法用来取回所有的应用程序,填充用于显示列表的用户界面模型,然后我们在这个列表里选择刚刚保持的Application。现在让我们向Application加入一个Version。这里,currentApplication是当前选择的Application的实例,它用新版本名字字符串来命名:
void addVersion(String versionname) {
if(currentApplication==null) return;
Version ver=new Version();
ver.setVersionname(versionname);
ver.setApplication(currentApplication);
currentApplication.getVersions().add(ver);
licmanStore.updateApplication(currentApplication);
populateVersions();
ui.setSelectedVersion(ver);
}
我们要做的是创建一个新的Version,设置它的名称,将它的父类设置为currentApplication,然后把版本添加到currentApplication的版本列表里。要对数据库进行这些操作,我们要做的是更新/合并currentApplication。但是这段代码有一个问题;刚刚创建的版本没有在列表里被选中。如果实体管理程序已经接管了层叠,那么我们新添加的Version实例就无法被托管,它只是一个外壳,这与我们把Application保存在addApplication()里的时候对象明确地变成可托管的不一样。这里的代码不起作用,因为当我们试着在方法结束时在列表里选择版本的时候,列表里没有我们开始时使用的Version外壳,只有另外一个基于它的可托管Version实例。解决方案很简单,只用取回可托管的版本就行了。已有的getVersion方法会处理取回操作:
Version getVersion(Application app,String name) {
try {
Query q=em.createQuery("select ver from Version as ver
where application=:app and versionname=:name");
q.setParameter("app",app);
q.setParameter("name",name);
return (Version)q.getSingleResult();
} catch (NoResultException nre) {
return null; }
}
Query的getSingleResult方法只返回一个结果,在发现没有结果或者有多个结果的时候,它会引发异常。在这里,我们把没有返回结果解释为返回为“空”。现在我们可以更改Controller的addVersion方法来使用它,此外我们还可以用它来检查我们没有创建重复的名称,当我们真的重复创建了名称的时候它就引发异常:
void addVersion(String versionname) throws UpdateException {
if(currentApplication==null) return;
if(licmanStore.getVersion(currentApplication,
versionname) != null)
throw new UpdateException("Version "
+ versionname + " already exists");
...
populateVersions();
ver=licmanStore.getVersion(currentApplication,versionname);
ui.setSelectedVersion(ver);
}
现在,刚刚创建的版本会被自动地选中。作为对读者的一种练习,我们在创建Licence的代码中留了一个类似的错误供修复。
在使用已扩展的persistence上下文时需要记住的一件事是,你确实需要让保持实体同步。有两种方式可以做到这一点。例如,当我们把User指派给Licence的时候,我们必须要记住更新内存里User的许可证集合,并把User添加到Licence的用户集合里。在Controller的addUsersToLicence方法里,我们保证增加了User和Licence列表:
void addUsersToLicence(List<User> u) throws UpdateException {
if(currentLicence==null) throw
new UpdateException("No licence selected");
currentLicence.getUsers().addAll(u);
for(User ut:u) ut.getLicences().add(currentLicence);
licmanStore.updateApplication(currentApplication);
populateLicenceUsers();
}
当然这样做有可能不现实;另外一种方式是让实体管理程序来刷新可托管对象,让其与数据库进行同步。要这样做我们要向LicManStore加入一个refresh方法:
void refresh(Object o) { em.refresh(o); }
只刷新来自数据库的User实例:
void addUsersToLicence(List<User> u) throws UpdateException {
if(currentLicence==null) throw
new UpdateException("No licence selected");
currentLicence.getUsers().addAll(u);
licmanStore.updateApplication(currentApplication);
for(User ut:u) licmanStore.refresh(ut);
populateLicenceUsers();
}
如果我们不得不对实体管理器使用默认的事务处理上下文,那么实体管理器往往是新创建的,我们就不会有残留的、还未从数据库刷新的可托管对象。
最后,让我们来看看列出了一个用户所拥有的所有许可证的“许可证报告(Licence report)”;它会在你双击用户列表上的某个用户时显示。这完全是通过整理(元素之间的)关系,利用从Licences的User类列表里取得的应用程序的名称和版本名称而获得的。
...
StringBuilder sb=new StringBuilder();
…
for(Licence l:user.getLicences()) {
sb.append(l.getVersion().getApplication().getApplicationname());
sb.append(" ");
sb.append(l.getVersion().getVersionname());
sb.append(" ");
sb.append(l.getLicenceKey());
sb.append(" ");
} ...
这里没有对数据库进行直接的访问,因为实体管理程序负责在Licence、Version和Application类被访问的时候调用它们,这要感谢已扩展的persistence上下文。这当然是要付出代价的,尤其是在应用程序为被访问的对象提供缓冲的过程中内存的使用,这就是为什么在计划如何使用JPA的时候你总应该考虑使用事务处理上下文并根据需要创建实体管理程序。
本文把重点放在了在Java Standard Edition里使用JPA上。JPA不是只能用于Java SE,它根植于Java Enterprise Edition。我们在这里使用的类同样可以不加任何改变就用在企业应用程序里;真正的变化在于你如何取得实体管理程序,以及在哪里取得它的配置。体系结构的可移植性是JPA的真正亮点。在Java SE上学到的技术也可以移植到Java EE上。
你可以在这里下载本文所涉及的源代码。