在前几部分里,我们已经讨论过了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上。