双方向の一対一の所有関係オブジェクトの実装方法

この資料では双方向の一対一の所有関係 (Bidirectional Owned One-to-One Relationship) を実装する方法を示します。 「双方向」とか「一対一の所有関係」とは何か?というところについては、オブジェクトの関係に関する記事を参考にしてください。 また、この資料で出てくるコードの「単一方向」版については、 単一方向の一対一の所有関係オブジェクトの実装方法 をご覧ください。

さて、注意書きはこれくらいにして、さっそく双方向の一対一所有関係の実装です。

双方向の一対一所有関係の実装

"単一方向の所有関係" のときと同様に、車 (Car) とドア (Door) の例を取り上げます。 単一方向のときと違う点は、子オブジェクトである Door 側に、親オブジェクトへの参照が含まれる点です。これがすなわち「双方向」の参照ということになります。

双方向一対一所有関係

メリットとしては子オブジェクトに親への参照が含まれているので、オブジェクト間のデータ参照が非常に楽チンになります。

例えば、もしもオブジェクト自身の参照を保存できずに、「親のキー」だけしか保持できないとしたら、 子オブジェクトを先に取得してから、親を参照したい場合、キーを元にデータストアから親オブジェクトをロードするコードを自分で書かなければいけなくなってしまいます。

Google App Engine の JDO 実装では、双方向一対一所有関係をサポートしています。これはすなわち、親から子だけの参照ではなく、 子から親への参照も保持できるということです。

親側の実装方法

では、親オブジェクトとなる Car クラスからみてみましょう。

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import com.google.appengine.api.datastore.Key;

@PersistenceCapable
public class Car {
  @PrimaryKey
  @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
  private Key key;

  @Persistent
  private String name;

  @Persistent(dependent="true", defaultFetchGroup = "true")
  private Door door;

  public Car(String name, String doorColor){
    this.name = name;
    this.door = new Door(doorColor, this);
  }

  public Key getKey(){
    return key;
  }

  public String getName(){
    return name;
  }

  public Door getDoor(){
    return door;
  }
}

単一方向のときと違うのは2箇所だけです。子オブジェクトである Door オブジェクト作成時に、 親オブジェクトである Car オブジェクトの参照をコンストラクタで渡している点がひとつ。もうひとつは、 door フィールドに設定した defaultFetchGroup = "true" という属性です。

コンストラクタの変更は、単に親オブジェクトの情報を子に伝える方法として行ったものです。 これはコンストラクタでなくても、セッターを定義するなどでも良いです。

defaultFetchGroup = "true" とは?

ポイントは defaultFetchGroup = "true" という指定です。 実はこれは必須ではないのですが、重要な考え方なので説明します。

defaultFetchGroup = "true" を指定すると、子オブジェクトがロードされたときに自動的に親オブジェクトもロードされます。 これはどういうことか、具体的に説明します。

この例で言えば、子オブジェクトである door をデータストアから取り出してきたとします。 このとき、defaultFetchGroup = "true" を設定しなければ親オブジェクトはロードされません。 子オブジェクトから、親オブジェクトを参照したときにはじめて、親オブジェクトがロードされます。 このため、親オブジェクトをロードするために、パーシスタンスマネージャ (PersistenceManager) が開いていないといけません。

親をロードするために、はじめに一度何らかの方法で親の参照を使っておく、というのでもいいですが、もっとスマートな方法があります。 それが defaultFetchGroup = "true" という指定です。

defaultFetchGroup = "true" を指定すると、子オブジェクトがロードされたタイミングで親が自動的にデータストアからロードされます。

子側の実装

次に子オブジェクト側の実装方法をみてみます。

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import com.google.appengine.api.datastore.Key;

@PersistenceCapable
public class Door {
  @PrimaryKey
  @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
    private Key key;
  
  @Persistent
  private String color;
  
  @Persistent(mappedBy = "door")
  private Car car;
  
  public Door(String color, Car car){
    this.color = color;
    this.car = car;
  }
  
  public Key getKey(){
    return key;
  }
  
  public String getColor(){
    return color;
  }
  
  public Car getCar(){
    return car;
  }
}

こちらは親オブジェクトを受け取ったり取り出したりする点で細々と変更が多いですが、 ポイントは何と言っても親オブジェクトを保持するための、Car 型の car フィールドを追加した点です。 さらに、それに対して mappedBy = "door" 属性が指定してあります。

mappedBy 属性とは?

mappedBy 属性とは何でしょうか? この属性は親オブジェクトへの参照に指定します。mappedBy="XXX" と指定することによって、 「このフィールドは親オブジェクトへの参照であり、親オブジェクトの XXX フィールドから参照されています」 と指定するものです。

mappedBy を指定しないと、親から子への参照とみなされてしまいます。mappedBy を指定することによって、 親子のリレーションが明確になるのです。