EJB 3.0(Public Draft)入門記 Java Persistence API おまけ Composite構造の永続化

今回はドキュメントからちょっと離れて別のことをしてみたいと思います。
Chapter 2 で継承関係を永続化できることがわかったのですが、Compositeパターンであらわされた構造を永続化したらどんな感じなのかなーと気になり始めたのでこれを試してみます。デザパタ苦手なんですけど。

  • 今回試すことのポイント。
    • エンティティはComponent、Composite、Leaf
    • リレーションシップには双方向のManyToOne/OneToManyを使う。
    • 継承のマッピングにはJoined Subclass Strategyを使う。
    • Composit構造全体を一度永続化してから取得してみる。
    • persistをカスケードするためにcascade=CascadeType.ALLつかう(ALLでなくてPERSISTでもいいんですけど)。

まず、エンティティの定義。親を辿れるようにparentフィールドをもちます。メソッドにはaddとremoveとprintをもちます。

@Entity(access = AccessType.FIELD)
@Inheritance(strategy=InheritanceType.JOINED)
public abstract class Component {

  @Id(generate=GeneratorType.AUTO)
  protected int id;

  protected String name;
  
  @ManyToOne
  protected Composite parent;

  public void add(Component component){
  }

  public void remove(Component component){
  }

  public void print() {
    print("");
  }
  
  protected abstract void print(String prefix);
  
  //getter, setter省略

}
@Entity(access=AccessType.FIELD)
public class Composite extends Component {
  
  @OneToMany(mappedBy="parent", cascade=CascadeType.ALL)
  private Collection components = new HashSet();

  public Composite(){
  }

  public Composite(String name){
    this.name = name;
  }

  @Override
  public void add(Component component) {
    component.setParent(this);
    components.add(component);
  }

  @Override
  public void remove(Component component) {
    component.setParent(null);
    components.remove(component);
  }

  @Override
  public void print(String prefix) {
    System.out.println(prefix + "/" + name);
    for(Component c :components) {
      c.print(prefix + "/" + name);
    }
  }

  //getter, setter省略
}
@Entity(access=AccessType.FIELD)
public class Leaf extends Component {
  
  public Leaf(){
  }

  public Leaf(String name){
    this.name = name;
  }
  
  @Override
  public void print(String prefix) {
    System.out.println(prefix + "/" + name);
  }  
}

これらのエンティティのテーブルへのマッピングをあらわすDDLは次のようになります。COMPONENTとCOMPOSITEが相互参照しちゃいますね。

CREATE TABLE COMPONENT(
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1)  NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  PARENT_ID INTEGER)

CREATE TABLE COMPOSITE(
  ID INTEGER NOT NULL PRIMARY KEY,
  CONSTRAINT FK24039267B1EB7A23 FOREIGN KEY(ID) REFERENCES COMPONENT(ID))

CREATE TABLE LEAF(
  ID INTEGER NOT NULL PRIMARY KEY,
  CONSTRAINT FK24137EB1EB7A23 FOREIGN KEY(ID) REFERENCES COMPONENT(ID))

ALTER TABLE COMPONENT ADD CONSTRAINT FK24013CDD2D547362 FOREIGN KEY(PARENT_ID) REFERENCES COMPOSITE(ID)

クライアントです。persistとprintとretrieveは別トランザクションにしてみました。関連が双方から手繰れるか確認するためにleafを指定してrootを取得するといったこともしてます。

@Stateless
public class ClientBean implements Client {

  @PersistenceContext
  private EntityManager em;

  public void persist() {
    Composite root = new Composite("root");
    Composite composite1 = new Composite("composite1");
    Leaf leaf1 = new Leaf("leaf1");
    Leaf leaf2 = new Leaf("leaf2");

    root.add(composite1);
    root.add(leaf1);
    composite1.add(leaf2);

    em.persist(root);
  }

  public void print(String name) {
    retrieve(name).print();
  }
  
  public Component retrieve(String name) {
    Query query = em.createQuery("from Component c where c.name = :name")
        .setParameter("name", name);
    return (Component) query.getSingleResult();
  }
  
  public Component retrieveRoot(String name) {
    Component component = retrieve(name);
    return findRoot(component);
  }
  
  private Component findRoot(Component component) {
    if (component.getParent() == null) {
      return component;
    } else {
      return findRoot(component.getParent());
    }
  }

  public static void main(String[] args) throws Exception {
    EJB3StandaloneBootstrap.boot(null);
    EJB3StandaloneBootstrap.deployXmlResource("ejb3-deployment.xml");
    InitialContext ctx = new InitialContext();
    Client client = (Client) ctx.lookup(Client.class.getName());
    
    System.out.println("## PERSIST");
    client.persist();
    System.out.println("\n## PRINT");
    client.print("root");
    System.out.println("\n## RETRIEVE");
    System.out.println(client.retrieve("root").getName());
    System.out.println("\n## RETRIEVE ROOT FROM LEAF");
    System.out.println(client.retrieveRoot("leaf2").getName());
    
    EJB3StandaloneBootstrap.shutdown();
  }

実行結果。SQLのあたりは適当に整形してます。出力結果は太字にしてみました。

## PERSIST
Hibernate: insert into Component (name, parent_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Composite (id) values (?)
Hibernate: insert into Component (name, parent_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Composite (id) values (?)
Hibernate: insert into Component (name, parent_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Leaf (id) values (?)
Hibernate: insert into Component (name, parent_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Leaf (id) values (?)

## PRINT
Hibernate: select component0_.id as id1_, component0_.name as name1_, component0_.parent_id as parent3_1_, 
 case 
 when component0_1_.id is not null then 1 when component0_2_.id is not null then 2 
 when component0_.id is not null then 0 end as clazz_ 
 from Component component0_ 
 left outer join Composite component0_1_ on component0_.id=component0_1_.id 
 left outer join Leaf component0_2_ on component0_.id=component0_2_.id  where component0_.name=?
/root
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ 
 from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id where components0_.parent_id=?
/root/composite1
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id where components0_.parent_id=?
/root/composite1/leaf2
/root/leaf1

## RETRIEVE
Hibernate: select component0_.id as id1_, component0_.name as name1_, component0_.parent_id as parent3_1_, 
 case when component0_1_.id is not null then 1 
 when component0_2_.id is not null then 2 
 when component0_.id is not null then 0 
 end as clazz_ 
 from Component component0_ 
 left outer join Composite component0_1_ on component0_.id=component0_1_.id 
 left outer join Leaf component0_2_ on component0_.id=component0_2_.id 
 where component0_.name=?
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ 
 from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id 
 where components0_.parent_id=?
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ 
 from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id 
 where components0_.parent_id=?
root

## RETRIEVE ROOT FROM LEAF
Hibernate: select component0_.id as id1_, component0_.name as name1_, component0_.parent_id as parent3_1_, 
 case 
 when component0_1_.id is not null then 1 
 when component0_2_.id is not null then 2 
 when component0_.id is not null then 0 
 end as clazz_ 
 from Component component0_ 
 left outer join Composite component0_1_ on component0_.id=component0_1_.id 
 left outer join Leaf component0_2_ on component0_.id=component0_2_.id 
 where component0_.name=?
Hibernate: select composite0_.id as id1_1_, composite0_1_.name as name1_1_, composite0_1_.parent_id as parent3_1_1_, composite1_.id as id1_0_, composite1_1_.name as name1_0_, composite1_1_.parent_id as parent3_1_0_ 
 from Composite composite0_ 
 inner join Component composite0_1_ on composite0_.id=composite0_1_.id 
 left outer join Composite composite1_ on composite0_1_.parent_id=composite1_.id 
 left outer join Component composite1_1_ on composite1_.id=composite1_1_.id 
 where composite0_.id=?
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ 
 from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id 
 where components0_.parent_id=?
Hibernate: select components0_.parent_id as parent3_1_, components0_.id as id1_, components0_.id as id1_0_, components0_.name as name1_0_, components0_.parent_id as parent3_1_0_, 
 case 
 when components0_1_.id is not null then 1 
 when components0_2_.id is not null then 2 
 when components0_.id is not null then 0 
 end as clazz_0_ 
 from Component components0_ 
 left outer join Composite components0_1_ on components0_.id=components0_1_.id 
 left outer join Leaf components0_2_ on components0_.id=components0_2_.id 
 where components0_.parent_id=?
root

テーブルの中身はこうなってます。
COMPONENT

ID NAME        PARENT_ID
                                              • -
1 root NULL 2 composite1 1 3 leaf2 2 4 leaf1 1

COMPOSITE

ID
  • -
1 2

LEAF

ID
  • -
3 4

とりあえず、Compositeをそのまま永続化できているようです。でもCOMPONENTとCOMPOSITEのテーブルが相互参照しちゃいます、OKなんでしょうか。

ところで、ちょっと驚きなところを発見しました。printメソッド経由でretrieveメソッドを呼ぶときとretrieveメソッド単独で呼ぶときで返されるComponentエンティティの状態が違います。出力のされ方を見るとprintメソッド内でretriveメソッドを呼んで取得しているエンティティは関連先のエンティティをまだ持っていないようです(必要になったらLazy Loadingを行っているようです)。でも、retrieveメソッドを単独で呼んだ場合、エンティティは関連先を取得した後に返されているようです(試してみたらそうでした)。もしかしてトランザクションの外に何が返されるかでアクセスのされ方が変わるんでしょうか?これは興味深いです。
あっ、用意したremoveメソッドを試すのを忘れた。