在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”就能够解决这个问题。