EJB 3.0(Public Draft)入門記 Java Persistence API Chapter4 その3

抽象スキーマ型についてです。

4.3 Abstract Schema Types and Query Domains

EJB QLは型付け言語でEJB QLのすべての式が型をもっているらしいです。式の型は

  • 式の構成
  • 識別用変数定義の抽象スキーマ
  • 永続フィールドやリレーションシップが評価する型
  • リテラルの型

から得られるらしいです。
うーん、よくわからない。とにかく、EJB QLって型付け言語なんですね。まったく意識してませんでした。


エンティティの抽象スキーマ型はエンティティクラスのメタデータから取得できます。略式的に言うと抽象スキーマ型にステートフィールド(state-field)と関連フィールド(association-field)があるらしい。

  • ステートフィールド(state-field)。ステートフィールド(state-field)はエンティティクラスのすべての永続フィールドと永続プロパティのgetterメソッドの戻り値に対応する。
  • 関連フィールド(association-field)。関連フィールドはエンティティクラスのすべての永続関連フィールドと永続関連プロパティのgetterメソッドに対するもので、型は関連エンティティの抽象スキーマ型(リレーションシップがone-to-many、many-to-manyの場合、関連エンティティの抽象スキーマ型のコレクション)となる。

抽象スキーマ型はEJB QLのデータモデル固有のものだということです。永続プロバイダ(persistence provider)は抽象スキーマ型を実装する必要はないらしいです。

抽象スキーマ型ってわかったようでわからない、もやもやしてます。プログラミングレベルで考えた場合のJavaプログラム ←→ メタデータ ←→ 永続ストアという関係が、型レベルで考えるとJavaのクラス ←→ 抽象スキーマ型 ←→ 永続スキーマ型 となるということでしょうか。


EJB QLクエリのドメインは同一の永続ユニットに定義されたすべてのエンティティの抽象スキーマ型から構成されます。クエリのドメインはクエリがベースとするエンティティのリレーションシップの誘導可能性(navigability)によって制限されることがあるそうです。エンティティの抽象スキーマ型の関連フィールド(association-field)が誘導可能性を決定します。関連フィールドとその値を使うことで、クエリは関連するエンティティを取得しクエリ内で取得したエンティティの抽象スキーマ型を使うことができるらしいです。

EJB QLクエリのドメインEJB QLクエリがとりうる値の範囲ということでしょうか。いくら永続ユニットに100のエンティティが定義されていても関連フィールドからたどれなければ当然特定のエンティティにしかアクセスできないですね。そりゃそうだろうという感じですが、こういうことを言いたいんでしょうか。

4.3.1 Naming

エンティティはEJB QLのクエリ文字列の中で抽象スキーマ型名によって指定されます。開発者は一意な抽象スキーマ型名をエンティティに与えることができます。一意な名前のスコープは永続ユニットだそうです。永続ユニットが違えば違う名前でもOKということですね。ことなる永続ユニットに属するエンティティをひとつのクエリで取得することはできないから問題がないのですね。

抽象スキーマ型名はEntityアノテーションのname要素(もしくはXML記述子のentity-name要素)で定義します。デフォルトはエンティティクラスのパッケージ名なしのクラス名です。

4.3.2 Example

例が載ってます。例と言っても使用するいくつかのエンティティとそれらのカーディナリティと2つのクエリですが。足りないところを適当に補って実際に動かしてみます。

まず使用するエンティティとそのカーディナリティ。カーディナリティは括弧内に示してみました。

  • Order(1) ― (m)LineItem
  • LineItem(m) ― (1)Product
  • Order(m) ― (1)ShippingAddress
  • Order(m) ― (1)BillingAddress

ドキュメントにあるクエリですが次のようなものです。(そういえばHibernateのドキュメントに、クエリにはエンティティ名以外で大文字使わないほうが見やすいよみたいなことが書いてあったのでそれに従っていこうと思います)

select distinct o from Order as o join o.lineItems as l 
where l.shipped = false
select distinct o from Order o join o.lineItems l join l.product p
where p.productType = 'office_supplies'

上の情報から以下のコードを作成して動かしてみました。

@Entity(access = AccessType.FIELD)
@Table(name = "ODR")
public class Order {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  @OneToMany(mappedBy = "order", cascade=CascadeType.ALL)
  private Collection lineItems = new HashSet();

  @ManyToOne
  private ShippingAddress shippingAddress;

  @ManyToOne
  private BillingAddress billingAddress;

  public Order() {
  }
  
  public Order(ShippingAddress shipping, BillingAddress billing) {
    this.shippingAddress = shipping;
    this.billingAddress = billing;
  }
  
  public void addLineItem(LineItem lineItem) {
    lineItem.setOrder(this);
    lineItems.add(lineItem);
  }
  
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    Formatter f = new Formatter(sb);
    f.format("Order: [shippingAddress=%s, billingAddress=%s", shippingAddress, billingAddress);
    for(LineItem l : lineItems) {
      sb.append("\n ")
      .append("lineItem=")
      .append(l);
    }
    sb.append("]");
    return sb.toString();
  }  
}
@Entity(access = AccessType.FIELD)
public class LineItem {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  private int quantity;

  private boolean shipped;

  @ManyToOne
  private Order order;

  @ManyToOne
  private Product product;

  public LineItem() {
  }

  public LineItem(Product product, int quantity) {
    this.product = product;
    this.quantity = quantity;
  }

  @Override
  public String toString() {
    return new Formatter().format("[product=%s, quantity=%s, shipped=%s]",
        product, quantity, shipped).toString();
  }
}
@Entity(access = AccessType.FIELD)
public class Product {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  private String name;

  private int price;

  private String productType;

  public Product() {
  }

  public Product(String name, int price, String productType) {
    this.name = name;
    this.price = price;
    this.productType = productType;
  }

  @Override
  public String toString() {
    return new Formatter().format("[name=%s, price=%s, productType=%s]",
        name, price, productType).toString();
  }
}

ShippingAddressとBillingAddressはAddressのサブクラスにしました。

@Entity(access = AccessType.FIELD)
@Inheritance(strategy=InheritanceType.SINGLE_TABLE, discriminatorValue="A")
public class Address {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  private String name;

  public Address() {
  }

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

  @Override
  public String toString() {
    return name;
  }
}
@Entity(access = AccessType.FIELD)
@Inheritance(discriminatorValue="B")
public class BillingAddress extends Address {
  public BillingAddress() {
  }

  public BillingAddress(String name) {
    super(name);
  }
}
@Entity(access = AccessType.FIELD)
@Inheritance(discriminatorValue="S")
public class ShippingAddress extends Address {
  public ShippingAddress() {
  }

  public ShippingAddress(String name) {
    super(name);
  }
}

上記のエンティティをデプロイしてテーブル定義を自動生成します。そのときのDDL。(アプリケーションサーバに配置することだけをデプロイと呼ぶわけではないんですよね。これからはEmbeddable EJBで使われているmicrocontainerにエンティティを認識させることもデプロイと言っていこうと思います。)

CREATE TABLE ADDRESS(
  TYPE VARCHAR(255) NOT NULL,
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,
  NAME VARCHAR(255))
CREATE TABLE LINEITEM(
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,
  QUANTITY INTEGER NOT NULL,
  SHIPPED BOOLEAN NOT NULL,
  ORDER_ID INTEGER,
  PRODUCT_ID INTEGER)
CREATE TABLE ODR(
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,
  SHIPPINGADDRESS_ID INTEGER,
  BILLINGADDRESS_ID INTEGER,
  CONSTRAINT FK1311D224F0FA5 FOREIGN KEY(SHIPPINGADDRESS_ID) REFERENCES ADDRESS(ID),
  CONSTRAINT FK1311DA89FC5CF FOREIGN KEY(BILLINGADDRESS_ID) REFERENCES ADDRESS(ID))
CREATE TABLE PRODUCT(
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  PRICE INTEGER NOT NULL,
  PRODUCTTYPE VARCHAR(255))
ALTER TABLE LINEITEM ADD CONSTRAINT FK4AAEE9476DC16E25 FOREIGN KEY(PRODUCT_ID) REFERENCES PRODUCT(ID)
ALTER TABLE LINEITEM ADD CONSTRAINT FK4AAEE94778679485 FOREIGN KEY(ORDER_ID) REFERENCES ODR(ID)

ステートレスセッションBean。データは適当です。

@Stateless
public class ClientBean implements Client {

  @PersistenceContext
  private EntityManager em;

  public void prepare() {
    // 住所
    ShippingAddress tokyo = new ShippingAddress("東京都");
    em.persist(tokyo);
    BillingAddress shizuoka = new BillingAddress("静岡県");
    em.persist(shizuoka);

    // 商品
    Product potatoChip = new Product("ポテチ", 150, "食");
    em.persist(potatoChip);
    Product coke = new Product("コーラ", 120, "飲");
    em.persist(coke);
    Product tea = new Product("お茶", 100, "飲");
    em.persist(tea);

    // 注文1
    Order order1 = new Order(tokyo, shizuoka);
    order1.addLineItem(new LineItem(potatoChip, 10));
    order1.addLineItem(new LineItem(coke, 20));
    order1.addLineItem(new LineItem(tea, 20));
    em.persist(order1);

    // 注文2
    Order order2 = new Order(tokyo, shizuoka);
    order2.addLineItem(new LineItem(coke, 50));
    em.persist(order2);
  }

  public List query1() {
    return em.createQuery(
        "select distinct o from Order as o join o.lineItems as l "
            + "where l.shipped = false").getResultList();
  }

  public List query2() {
    return em.createQuery(
        "select distinct o from Order o join o.lineItems l join l.product p "
            + "where p.productType = '食'").getResultList();
  }

  public void print() {
    System.out.println("\n##未出荷注文");
    for (Object each : query1()) {
      System.out.println(each);
    }
    
    System.out.println("\n##食物を含む注文");
    for (Object each : query2()) {
      System.out.println(each);
    }
  }

  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());

    client.prepare();
    client.print();

    EJB3StandaloneBootstrap.shutdown();
  }
}

実行結果。

Hibernate: insert into Address (name, TYPE, id) values (?, 'S', null)
Hibernate: call identity()
Hibernate: insert into Address (name, TYPE, id) values (?, 'B', null)
Hibernate: call identity()
Hibernate: insert into Product (name, price, productType, id) values (?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into Product (name, price, productType, id) values (?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into Product (name, price, productType, id) values (?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into ODR (shippingAddress_id, billingAddress_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into LineItem (quantity, shipped, order_id, product_id, id) values (?, ?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into LineItem (quantity, shipped, order_id, product_id, id) values (?, ?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into LineItem (quantity, shipped, order_id, product_id, id) values (?, ?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into ODR (shippingAddress_id, billingAddress_id, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into LineItem (quantity, shipped, order_id, product_id, id) values (?, ?, ?, ?, null)
Hibernate: call identity()

##未出荷注文
Hibernate: select distinct order0_.id as id2_, order0_.shippingAddress_id as shipping2_2_, order0_.billingAddress_id as billingA3_2_ from ODR order0_ inner join LineItem lineitems1_ on order0_.id=lineitems1_.order_id where lineitems1_.shipped=0
Hibernate: select shippingad0_.id as id0_0_, shippingad0_.name as name0_0_ from Address shippingad0_ where shippingad0_.id=? and shippingad0_.TYPE='S'
Hibernate: select billingadd0_.id as id0_0_, billingadd0_.name as name0_0_ from Address billingadd0_ where billingadd0_.id=? and billingadd0_.TYPE='B'
Hibernate: select lineitems0_.order_id as order4_2_, lineitems0_.id as id2_, lineitems0_.id as id1_1_, lineitems0_.quantity as quantity1_1_, lineitems0_.shipped as shipped1_1_, lineitems0_.order_id as order4_1_1_, lineitems0_.product_id as product5_1_1_, product1_.id as id3_0_, product1_.name as name3_0_, product1_.price as price3_0_, product1_.productType as productT4_3_0_ from LineItem lineitems0_ left outer join Product product1_ on lineitems0_.product_id=product1_.id where lineitems0_.order_id=?
Order: [shippingAddress=東京都, billingAddress=静岡県
 lineItem=[product=[name=コーラ, price=120, productType=飲], quantity=20, shipped=false]
 lineItem=[product=[name=お茶, price=100, productType=飲], quantity=20, shipped=false]
 lineItem=[product=[name=ポテチ, price=150, productType=食], quantity=10, shipped=false]]
Hibernate: select lineitems0_.order_id as order4_2_, lineitems0_.id as id2_, lineitems0_.id as id1_1_, lineitems0_.quantity as quantity1_1_, lineitems0_.shipped as shipped1_1_, lineitems0_.order_id as order4_1_1_, lineitems0_.product_id as product5_1_1_, product1_.id as id3_0_, product1_.name as name3_0_, product1_.price as price3_0_, product1_.productType as productT4_3_0_ from LineItem lineitems0_ left outer join Product product1_ on lineitems0_.product_id=product1_.id where lineitems0_.order_id=?
Order: [shippingAddress=東京都, billingAddress=静岡県
 lineItem=[product=[name=コーラ, price=120, productType=飲], quantity=50, shipped=false]]

##食べ物を含む注文
Hibernate: select distinct order0_.id as id2_, order0_.shippingAddress_id as shipping2_2_, order0_.billingAddress_id as billingA3_2_ from ODR order0_ inner join LineItem lineitems1_ on order0_.id=lineitems1_.order_id inner join Product product2_ on lineitems1_.product_id=product2_.id where product2_.productType='食'
Order: [shippingAddress=東京都, billingAddress=静岡県
 lineItem=[product=[name=コーラ, price=120, productType=飲], quantity=20, shipped=false]
 lineItem=[product=[name=お茶, price=100, productType=飲], quantity=20, shipped=false]
 lineItem=[product=[name=ポテチ, price=150, productType=食], quantity=10, shipped=false]]

OK、なのか?
ところで今回のクエリにdistinctつけないとLineItemの数だけOrderが返ってきます。SQLで考えればそうなるのはわかるのですが(joinしてるし)、EJB QLはエンティティとしては同じものを一度のクエリで返さないものだと思い込んでました。永続コンテキストとごっちゃにしているからかなぁ?永続コンテキストに同じエンティティが複数存在することはあり得ないはずですが、クエリの結果は永続コンテキストからの取得とちがうから複数返ってきてもおかしくないんですね、きっと。distinctの存在理由を考えれば当然なのかも。

2番目のクエリはwhere句でproductのproductTypeを指定して抽出条件を絞っていますが、Orderとして取得しているのでOrderの中には条件指定した以外のデータも含まれます。このクエリの結果にも最初とまどっちゃいました。


この節では2つのクエリについていろいろ説明を加えているのですが、まとめると

  • ステートフィールド(state-field)と関連フィールド(association-field)を使ってクエリが書ける
  • 関連フィールド(association-field)を使って関連をたどること(navigation)ができる

みたいなことを言っています。