面向Java开发人员db4o指南:数组和集合

  处理多样性关系
  舒适的家庭生活会导致一个或更多“小人儿”降临到这个家庭。但是,在增加小孩到家庭中之前,先确保Person真正有地方可住。给他们一个工作场所,或者还有一个很好的夏日度假屋。一个Address类型应该可以解决所有这三个地方。
  清单1.添加一个Address类型到Person类中
  package com.tedneward.model;
  public class Address
  {
  public Address()
  {
  }
  public Address(String street,String city,String state,String zip)
  {
  this.street=street;this.city=city;
  this.state=state;this.zip=zip;
  }
  public String toString()
  {
  return"[Address:"+
  "street="+street+""+
  "city="+city+""+
  "state="+state+""+
  "zip="+zip+"]";
  }
  public int hashCode()
  {
  return street.hashCode()&city.hashCode()&
  state.hashCode()&zip.hashCode();
  }
  public boolean equals(Object obj)
  {
  if(obj==this)
  return this;
  if(obj instanceof Address)
  {
  Address rhs=(Address)obj;
  return(this.street.equals(rhs.street)&&
  this.city.equals(rhs.city)&&
  this.state.equals(rhs.state)&&
  this.zip.equals(rhs.zip));
  }
  else
  return false;
  }
  public String getStreet(){return this.street;}
  public void setStreet(String value){this.street=value;}
  public String getCity(){return this.city;}
  public void setCity(String value){this.city=value;}
  public String getState(){return this.state;}
  public void setState(String value){this.state=value;}
  public String getZip(){return this.zip;}
  public void setZip(String value){this.zip=value;}
  private String street;
  private String city;
  private String state;
  private String zip;
  }
  可以看到,Address只是一个简单的数据对象。将它添加到Person类中意味着Person将有一个名为addresses的Address数组作为字段。第一个地址是家庭住址,第二个是工作地址,第三个(如果不为null的话)是度假屋地址。当然,这些都被设置为protected,以便将来通过方法来封装。
  完成这些设置后,现在可以增强Person类,使之支持小孩,所以为Person定义一个新字段:一个Person ArrayList,它同样也有一些相关的方法,以便进行适当的封装。
  接下来,由于大多数小孩都有父母,还将添加两个字段来表示母亲和父亲,并增加适当的accessor/mutator方法。将为Person类增加一个新的方法,使之可以创建一个新的Person,这个方法有一个贴切的名称,即haveBaby。此外还增加一些业务规则,以支持生小孩的生物学需求,并将这个新的小Person添加到为母亲和父亲字段创建的children ArrayList中。做完这些之后,再将这个婴儿返回给调用者。
  清单2显示,新定义的Person类可以处理这种多样性关系。
  清单2.定义为多样性关系的家庭生活
  package com.tedneward.model;
  import java.util.List;
  import java.util.ArrayList;
  import java.util.Iterator;
  public class Person
  {
  public Person()
  {}
  public Person(String firstName,String lastName,Gender gender,int age,Mood mood)
  {
  this.firstName=firstName;
  this.lastName=lastName;
  this.gender=gender;
  this.age=age;
  this.mood=mood;
  }
  public String getFirstName(){return firstName;}
  public void setFirstName(String value){firstName=value;}
  public String getLastName(){return lastName;}
  public void setLastName(String value){lastName=value;}
  public Gender getGender(){return gender;}
  public int getAge(){return age;}
  public void setAge(int value){age=value;}
  public Mood getMood(){return mood;}
  public void setMood(Mood value){mood=value;}
  public Person getSpouse(){return spouse;}
  public void setSpouse(Person value){
  //A few business rules
  if(spouse!=null)
  throw new IllegalArgumentException("Already married!");
  if(value.getSpouse()!=null&&value.getSpouse()!=this)
  throw new IllegalArgumentException("Already married!");
  spouse=value;
  //Highly sexist business rule
  if(gender==Gender.FEMALE)
  this.setLastName(value.getLastName());
  //Make marriage reflexive,if it's not already set that way
  if(value.getSpouse()!=this)
  value.setSpouse(this);
  }
  public Address getHomeAddress(){return addresses[0];}
  public void setHomeAddress(Address value){addresses[0]=value;}
  public Address getWorkAddress(){return addresses[1];}
  public void setWorkAddress(Address value){addresses[1]=value;}
  public Address getVacationAddress(){return addresses[2];}
  public void setVacationAddress(Address value){addresses[2]=value;}
  public Iterator<Person>getChildren(){return children.iterator();}
  public Person haveBaby(String name,Gender gender){
  //Business rule
  if(this.gender.equals(Gender.MALE))
  throw new UnsupportedOperationException("Biological impossibility!");
  //Another highly objectionable business rule
  if(getSpouse()==null)
  throw new UnsupportedOperationException("Ethical impossibility!");
  //Welcome to the world,little one!
  Person child=new Person(name,this.lastName,gender,0,Mood.CRANKY);
  //Well,wouldn't YOU be cranky if you'd just been pushed out of
  //a nice warm place?!?
  //These are your parents...
  child.father=this.getSpouse();
  child.mother=this;
  //...and you're their new baby.
  //(Everybody say"Awwww....")
  children.add(child);
  this.getSpouse().children.add(child);
  return child;
  }
  public String toString()
  {
  return
  "[Person:"+
  "firstName="+firstName+""+
  "lastName="+lastName+""+
  "gender="+gender+""+
  "age="+age+""+
  "mood="+mood+""+
  (spouse!=null?"spouse="+spouse.getFirstName()+"":"")+
  "]";
  }
  public boolean equals(Object rhs)
  {
  if(rhs==this)
  return true;
  if(!(rhs instanceof Person))
  return false;
  Person other=(Person)rhs;
  return(this.firstName.equals(other.firstName)&&
  this.lastName.equals(other.lastName)&&
  this.gender.equals(other.gender)&&
  this.age==other.age);
  }
  private String firstName;
  private String lastName;
  private Gender gender;
  private int age;
  private Mood mood;
  private Person spouse;
  private Address[]addresses=new Address[3];
  private List<Person>children=new ArrayList<Person>();
  private Person mother;
  private Person father;
  }
  即使包括所有这些代码,清单2提供的家庭关系模型还是过于简单。在这个层次结构中的某些地方,必须处理那些null值。但是,在db4o中,那个问题更应该在对象建模中解决,而不是在对象操作中解决。所以现在我可以放心地忽略它。
  填充和测试对象模型
  对于清单2中的Person类,需要重点注意的是,如果以关系的方式,使用父与子之间分层的、循环的引用来建模,那肯定会比较笨拙。通过一个实例化的对象模型可以更清楚地看到我所谈到的复杂性,所以我将编写一个探察测试来实例化Person类。注意,清单3中省略了JUnit支架(scaffolding)。
  清单3.幸福家庭测试
   Test public void testTheModel()
  {
  Person bruce=new Person("Bruce","Tate",
  Gender.MALE,29,Mood.HAPPY);
  Person maggie=new Person("Maggie","Tate",
  Gender.FEMALE,29,Mood.HAPPY);
  bruce.setSpouse(maggie);
  Person kayla=maggie.haveBaby("Kayla",Gender.FEMALE);
  Person julia=maggie.haveBaby("Julia",Gender.FEMALE);
  assertTrue(julia.getFather()==bruce);
  assertTrue(kayla.getFather()==bruce);
  assertTrue(julia.getMother()==maggie);
  assertTrue(kayla.getMother()==maggie);
  int n=0;
  for(Iterator<Person>kids=bruce.getChildren();kids.hasNext();)
  {
  Person child=kids.next();
  if(n==0)assertTrue(child==kayla);
  if(n==1)assertTrue(child==julia);
  n++;
  }
  }
  目前一切尚好。所有方面都能通过测试,包括小孩ArrayList的使用中的长嗣身份。但是,当增加 Before和 After条件,以便用我的测试数据填充db4o数据库时,事情开始变得更有趣。
  清单4.将孩子发送到数据库
   Before public void prepareDatabase()
  {
  db=Db4o.openFile("persons.data");
  Person bruce=new Person("Bruce","Tate",
  Gender.MALE,29,Mood.HAPPY);
  Person maggie=new Person("Maggie","Tate",
  Gender.FEMALE,29,Mood.HAPPY);
  bruce.setSpouse(maggie);
  bruce.setHomeAddress(
  new Address("5 Maple Drive","Austin",
  "TX","12345"));
  bruce.setWorkAddress(
  new Address("5 Maple Drive","Austin",
  "TX","12345"));
  bruce.setVacationAddress(
  new Address("10 Wanahokalugi Way","Oahu",
  "HA","11223"));
  Person kayla=maggie.haveBaby("Kayla",Gender.FEMALE);
  kayla.setAge(8);
  Person julia=maggie.haveBaby("Julia",Gender.FEMALE);
  julia.setAge(6);
  db.set(bruce);
  db.commit();
  }
  注意,存储整个家庭所做的工作仍然不比存储单个Person对象所做的工作多。您可能还记得,在上一篇文章中,由于存储的对象具有递归的性质,当把bruce引用传递给db.set()调用时,从bruce可达的所有对象都被存储。不过眼见为实,让我们看看当运行那个简单的探察测试时,实际上会出现什么情况。首先,测试当调用随Person存储的各种Address时,是否可以找到它们。然后,测试是否孩子们也被存储。
  清单5.搜索住房和家庭
   Test public void testTheStorageOfAddresses()
  {
  List<Person>maleTates=
  db.query(new Predicate<Person>(){
  public boolean match(Person candidate){
  return candidate.getLastName().equals("Tate")&&
  candidate.getGender().equals(Gender.MALE);
  }
  });
  Person bruce=maleTates.get(0);
  Address homeAndWork=
  new Address("5 Maple Drive","Austin",
  "TX","12345");
  Address vacation=
  new Address("10 Wanahokalugi Way","Oahu",
  "HA","11223");
  assertTrue(bruce.getHomeAddress().equals(homeAndWork));
  assertTrue(bruce.getWorkAddress().equals(homeAndWork));
  assertTrue(bruce.getVacationAddress().equals(vacation));
  }
   Test public void testTheStorageOfChildren()
  {
  List<Person>maleTates=
  db.query(new Predicate<Person>(){
  public boolean match(Person candidate){
  return candidate.getLastName().equals("Tate")&&
  candidate.getGender().equals(Gender.MALE);
  }
  });
  Person bruce=maleTates.get(0);
  int n=0;
  for(Iterator<Person>children=bruce.getChildren();
  children.hasNext();
  )
  {
  Person child=children.next();
  System.out.println(child);
  if(n==0)assertTrue(child.getFirstName().equals("Kayla"));
  if(n==1)assertTrue(child.getFirstName().equals("Julia"));
  n++;
  }
  }
  理解关系
  您可能会感到奇怪,清单5中显示的基于Collection的类型(ArrayList)没有被存储为Person类型的“dependents”,而是被存储为一个成熟的对象。这还说得过去,但是当对对象数据库中的ArrayList运行一个查询时,它可能,有时候也确实会导致返回奇怪的结果。由于目前数据库中只有一个ArrayList,所以还不值得运行一个探察测试,看看当对它运行一个查询时会出现什么情况。把这作为留给您的练习。
  自然地,存储在一个集合中的Person也被当作数据库中的一级实体,所以在查询符合某个特定标准(例如所有女性Person)的所有Person时,也会返回ArrayList实例中引用到的那些Person,如清单6所示。
  清单6.什么是Julia?
   Test public void findTheGirls()
  {
  List<Person>girls=
  db.query(new Predicate<Person>(){
  public boolean match(Person candidate){
  return candidate.getGender().equals(Gender.FEMALE);
  }
  });
  boolean maggieFound=false;
  boolean kaylaFound=false;
  boolean juliaFound=false;
  for(Person p:girls)
  {
  if(p.getFirstName().equals("Maggie"))
  maggieFound=true;
  if(p.getFirstName().equals("Kayla"))
  kaylaFound=true;
  if(p.getFirstName().equals("Julia"))
  juliaFound=true;
  }
  assertTrue(maggieFound);
  assertTrue(kaylaFound);
  assertTrue(juliaFound);
  }
  注意,对象数据库将尽量地使引用“correct”—至少在知道引用的情况下如此。例如,分别在两个不同的查询中检索一个Person(也许是母亲)和检索另一个Person(假设是女儿),仍然认为她们之间存在一个双向关系,如清单7所示。
  清单7.保持关系的真实性
   Test public void findJuliaAndHerMommy()
  {
  Person maggie=(Person)db.get(
  new Person("Maggie","Tate",Gender.FEMALE,0,null)).next();
  Person julia=(Person)db.get(
  new Person("Julia","Tate",Gender.FEMALE,0,null)).next();
  assertTrue(julia.getMother()==maggie);
  }
  当然,您正是希望对象数据库具有这样的行为。还应注意,如果返回女儿对象的查询的激活深度被设置得足够低,那么对getMother()的调用将返回null,而不是实际的对象。这是因为Person中的mother字段是相对于被检索的原本对象的另一个“跳跃(hop)”。
  更新和删除
  至此,您已经看到了db4o如何存储和取出多个对象,但是对象数据库如何处理更新和删除呢?就像结构化对象一样,多对象更新或删除期间的很多工作都与管理更新深度有关,或者与级联删除有关。现在您可能已经注意到,结构化对象与集合之间有很多相似之处,所以其中某一种实体的特性也适用于另一种实体。如果将ArrayList看作“另一种结构化对象”,而不是一个集合,就很好理解了。
  所以,根据到目前为止您学到的东西,我应该可以更新数据库中的某一个女孩。而且,为了更新这个对象,只需将她父母中的一个重新存储到数据库中,如清单8所示。
  清单8.生日快乐,Kayla!
   Test public void kaylaHasABirthday()
  {
  Person maggie=(Person)db.get(
  new Person("Maggie","Tate",Gender.FEMALE,0,null)).next();
  Person kayla=(Person)db.get(
  new Person("Kayla","Tate",Gender.FEMALE,0,null)).next();
  kayla.setAge(kayla.getAge()+1);
  int kaylasNewAge=kayla.getAge();
  db.set(maggie);
  db.close();
  db=Db4o.openFile("persons.data");
  kayla=(Person)db.get(
  new Person("Kayla","Tate",Gender.FEMALE,0,null)).next();
  assert(kayla.getAge()==kaylasNewAge);
  }
  对于多样性关系中的对象,其删除工作非常类似于上一篇文章介绍索的结构化对象的删除工作。只需注意级联删除,因为它对这两种对象可能都有影响。当执行级联删除时,将会从引用对象的每个地方彻底删除对象。如果执行一个级联删除来从数据库中删除一个Person,则那个Person的母亲和父亲在其children集合中突然有一个null引用,而不是有效的对象引用。
  结束语
  在很多方面,将数组和集合存储到对象数据库中并不总与存储常规的结构化对象不同,只是要注意数组不能被直接查询,而集合则可以。不管出于何种目的,这都意味着可以在建模时使用集合和数组,而不必等到持久引擎需要使用集合或数组时才使用它们。