XML Schema: やるべきこと、やってはいけないこと

Revision: 1.9.1
Kohsuke KAWAGUCHI
なんばりょうすけ 訳

原文はこちら

[]内は訳者による注である。

はじめに

W3C XML Schema は学ぶのも使うのも簡単だ … 落とし穴をよける方法を知っていればね。「やるべきこと」はこれだ。最低これだけおぼえておけばいい。

そして「やってはいけないこと」がこれだ。

後で説明するが、実際これらの「やってはいけない」ことをやらないことで失うものは何もない。

長くておぼえられない? それならこの一行バージョンをおぼえてほしい。

「XML Schema = DTD + データタイプ + 名前空間」だと思え!

この文書を書いた動機

すでにWeb上には似たような文書がいくつかある。しかし僕はそれらの文書は特殊な人たちによって書かれている、ということ気が付いた。彼らはものごとの限界に挑戦する素晴らしい人たちだ。彼らは XML Schema ワーキンググループのメンバーでさえ想像もしなかったようなクールな裏技を発明せずにはいられないんだ。

XML Schema は彼らの新しいお気に入りのオモチャというわけだ。

そういうのとは違う文書が必要だ。ビジネスのために W3C XML Schema を使う人たち、使おうとしているが途方にくれている人たち、のための文書が。

そういうわけで、この文書の目的は、あなたがなにをすべきで、なにをすべきでないかについての堅実なガイドラインを示すことにある。

コメントはいつでも歓迎。一つでもあれば是非しらせて欲しい。
[ Kohsuke KAWAGUCHI は日本語をとてもよく理解するので彼に直接メールするのがいいだろう。日本語が変だとか誤字脱字の類いを見つけた場合はなんばりょうすけまでどうぞ。]

なぜ複合型を避けるべきか

あなたが複合型が何であるか知らなくてもそんなことで悩まなくてもいい。この機能は、得られるものは少ないくせにやたらと複雑なんだ。

しかも複合型がなくてもあなたに失うものなど何もない。実のところ複合型を使って書いたスキーマは、いつだって複合型を使わずに書くことができる。

正確に言うと、複合型を理解しなくても書くことができるが、<complexType> とは書かなくてはならない。

<complexType> は <element> 要素にただ一つだけの子要素として書かなくてはならない何かお約束のようなものだと思えばいい。


<xs:element name="head">
<xs:complexType> <!-- consider this as a place holder -->
<!-- define content model by using model groups. -->
...
    
    <!-- then refer to attribute groups -->
<xs:attributeGroup ref="head.attributes" />
</xs:complexType>
</xs:element>

だとすれば、そんな使わないものを学ぶために貴重な時間を費やす必要なんてある?

納得した? それならここから先は読まなくていい。。。

一言で言えば、「複合型 = モデルグループ + 継承 - 使いやすさ」だ。複合型もモデルグループも内容モデルを定義するのに使われるという意味では兄弟みたいなものだ。複合型は他の複合型やモデルグループの中から使えないので、使いやすさの点で難がある。一方モデルグループはそういう制限なしで使える。

複合型のアドバンテージは「継承」だけだ。そこで、なぜあなたが継承を使わなくてよいか、を説明しよう。継承には二種類ある。拡張(extension)制限(restriction)の二つだ。

拡張によって基底型の内容モデルの後ろに要素を追加することができる。ということは、こんなモデルグループを書けば拡張と意味的に同じことができるんだ。


<xs:group name="extendedType">
  <xs:sequence>
    <xs:group ref="baseType"/>
    
    <!-- append things that you want -->
    ....
  </xs:sequence>
</xs:group>

制限によって基底型の内容モデルを制限することができる。しかし、この機能を使っても、新しい型のために内容モデル全体を書かなくてはいけないことに変りはないんだ[差分で書けるわけではないということ]。基本的に複合型を使おうがモデルグループを使おうが、同じものを書かなくてはならない。

それなら制限を使うと何が嬉しいんだろう。嬉しいのはたった一つ、エラーチェックができることだ。バリデータは内容モデルが正しく制限されていなければ、おそらくエラーを報告するだろう。

しかし不幸なことに、これはほとんどアドバンテージにはならないんだ。

第一に、バリデータにとってこのチェックを厳密に実施することは大変なことなんだ。 仕様書のこの制約を定義している箇所をちょっと見て欲しい。何が許され何が許されないかを指定するのに 3.9.6 章が丸ごと割かれている。[バリデータを実装する]開発者がこの制約チェックを実施しないで済ませたい、という強い誘惑に駆られたとしても不思議じゃないのがわかるだろう。たいていの人はこのチェックが実施されなくても気付きはしないだろうから。このあたりは XML Schema ワーキンググループのメンバーが開発した XSV でさえ、部分的にしかサポートしていないんだ。

そういうわけで、あなたが使うバリデータがこの[制限による継承に関する]制約チェックを完全に実施しないということは、多いにありそうなことだ。これで制限の持っていた唯一のアドバンテージは消え去ってしまったわけだ。

第二に、せっかく制限を正しく書いても、あなたの使うバリデータではエラーになることがあるんだ。次の例で考えよう。


基底型:
<xs:all>
  <xs:element name="a" />
  <xs:element name="b" />
  <xs:element name="c" minOccurs="0" />
</xs:all>

制限による派生型:
<xs:all>
  <xs:element name="b" />
  <xs:element name="a" />
</xs:all>

後者は前者の正しい制限のように見える。実際、派生型によって受理されるどんな内容モデルでも基底型によっても受理される。しかし W3C XML Schema はこれを禁止している。仕様では上の派生は "schema component constraint: particle derivation OK (all:all,sequence:sequence -- recurse)"を違反する。こんなのは氷山の一角だ。この件に興味があるなら MSL の最後のページにあたって欲しい。

モデルグループを複合型のかわりに使っていればこんな問題とは無縁でいられるんだ。

制限による派生があった時は(一般には十分理解されていないけれど)それがどのように機能するか非常に詳細まで理解しないといけない。

なぜ属性宣言を避けるべきか

正確に言うと、避けるべきなのはグローバル属性宣言であって、ローカル属性宣言は使ってよい。グローバル属性宣言というのは例えばこういうものだ。


<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema"
      targetNamespace="http://example.com">
  <!-- attribute whose name is foo -->
  <xs:attribute name="foo" type="xs:float" />
  
  <xs:element name="root">
    <xs:complexType>
      <xs:attribute ref="foo" />
    </xs:complexType>
  </xs:element>
</xs:schema>

実は、このスキーマはこんな文書インスタンスを受理しない

<root xmlns="http://example.com" foo="5.12"/>

そのかわり、(たぶんあなたが受理して欲しいのとは違うと思うけど)こんなインスタンスなら受理する。

<root xmlns="http://example.com" ns:foo="5.12" xmlns:ns="http://example.com" />

属性グループならこんな問題はない。だから属性宣言のかわりに属性グループを使うべきだ。


<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema"
      targetNamespace="http://example.com">
  <xs:attributeGroup name="root.attributes">
    <!-- attribute whose name is foo -->
    <xs:attribute name="foo" type="xs:float" />
  </xs:attributeGroup>
  
  <xs:element name="root">
    <xs:complexType>
      <!-- content model -->
      ....
      
      <xs:attributeGroup ref="root.attributes" />
    </xs:complexType>
  </xs:element>
</xs:schema>

属性グループからは他の属性グループを参照できる。こうやって、共通属性を一つの属性グループに書いて、よそから参照することができるんだ。

なぜ記法宣言を避けるべきか

記法のことなんて聞いたことなかったとしても大丈夫。それによってあなたは何も失ってはいないんだから。記法は[SGMLとの]後方互換性のためだけにある。いまさら学ぶ必要なんて全くない。

それでも敢えて記法のことを知りたいなら、W3C XML Schema の記法は DTD のそれとは互換性がないことは知っておいた方がいい。なぜ互換性がないかといえば、XML Schema の記法は QName [プレフィクスで修飾された名前] なんだ。

仕様書にある例を見てみよう。


<xs:notation name="jpeg"
             public="image/jpeg" system="viewer.exe" />

<xs:element name="picture">
 <xs:complexType>
  <xs:simpleContent>
   <xs:extension base="xs:hexBinary">
    <xs:attribute name="pictype">
     <xs:simpleType>
      <xs:restriction base="xs:NOTATION">
       <xs:enumeration value="jpeg"/>
       <xs:enumeration value="png"/>
       . . .
      </xs:restriction>
     </xs:simpleType>
    </xs:attribute>
   </xs:extension>
  </xs:simpleContent>
 </xs:complexType>
</xs:element>

<picture pictype="jpeg">...</picture>

この例はオッケーだ。しかし次の例は上のスキーマには受理されない。たとえプレフィクス "pic" が正しく宣言されていてもダメだ。


<pic:picture pictype="jpeg"> ... </pic:picture>

頭が混乱してきた? 記法は QName だから正しくはこう書かなくちゃいけないんだ。


<pic:picture pictype="pic:jpeg"> ... </pic:picture>

明らかにこれは唯一の存在意義[後方互換性]を失わせている。

記法にこだわる理由なんて全く何もない。記法は SGML のためのものだ。

なぜローカル宣言を避けるべきか

W3C XML Schema では要素宣言の内側にさらに要素宣言が書ける。


<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema"
      targetNamespace="http://example.com">
  <xs:element name="person">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="familyName" type="xs:string" />
        <xs:element name="lastName" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

しかし一般には可能な限りこれを避けるべきだ。なぜなら上のスキーマはこんな文書インスタンスを受理しないからだ。


<person xmlns="http://example.com">
  <familyName> KAWAGUCHI </familyName>
  <lastName> Kohsuke </lastName>
</person>

かわりにこんなふうに書かなくてはいけない。


<foo:person xmlns:foo="http://example.com">
  <familyName> KAWAGUCHI </familyName>
  <lastName> Kohsuke </lastName>
</foo:person>

これはタイプ量が増えるだけではなく、XML 名前空間の悪い使い方だ。この問題を避けるには、こう書くとよい。

<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema"
      targetNamespace="http://example.com"
      xmlns="http://example.com">
  <xs:element name="person">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="familyName" />
        <xs:element ref="lastName" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  
  <xs:element name="familyName" type="xs:string"/>
  <xs:element name="lastName" type="xs:string"/>
</xs:schema>

[原文では3行目の名前空間宣言が欠落している。誤りなので原著者に確認の上修正した。]

この問題を解決するには別の方法もある。とにかく何も考えずに elementFormDefault="qualified"schema 要素に追加するんだ。こうするとローカル要素宣言が安全に使える。

でもこの努力が正確には何を意味するのか理解する必要はない。そうするだけの価値は多分ないから。これがスキーマが「正しく」振る舞うようにすることだけ理解すればよい。

なぜ置換グループを避けるべきか

一言で言うと、この機能は実用的に使うにはあまりに複雑すぎる。一番難しいのは

簡単に言うと、置換グループは <choice> の別の書き方だ。だからいつでも置換グループを使うかわりに <choice> を使うことができる。そしていずれにせよ <choice> は必要だ。

置換グループを正しく使うには、まず複合型を学んで、次に追加されたいくつかの属性を学んで、さらにそれらの使い方の規則を学んで、最後にそれらを使うことによる効果を学ばなくてはならない。スキーマ作者たるあなたが、勇敢にもこれをやり通して新天地に到達したとしても、そのスキーマに従った文書を作成する人たちも正しい文書を書くためには全く同じ道を辿らなくてはならないんだ。かわいそうに。。。

それでもまだ置換グループを使いたいと言うのなら、それがあなたが思うほど容易ではないということを見せてあげよう。

第一に、置換グループのメンバーである内容モデルは型の派生によって互いに関連づけられないといけない。これは自由に内容モデルを書けないことを意味する。すぐにあなたは、単にメンバー間に正しい継承関係を維持するためだけに、変な内容モデルを持った抽象要素を置換グループの先頭に書かなくてはいけないと思うだろう。でもそれは正しくない。

第二に置換の振る舞いをコントロールする属性は、使うのも理解するのも難しい。置換グループをコントロールする属性の一つに block と呼ばれる属性がある。また基本的に値として "extension", "restriction", "#all" のいずれかを取る final と呼ばれる属性がある。

final は置換グループには不適切のように見えるが、それは内部的には「置換グループの排除」と呼ばれ、その名が示唆するように置換グループをコントロールするというのが真実だ。block 属性が内部的にはどう呼ばれているか知ってる? それは「許可されない置換」だ。違いがよくわからない? そう! 僕にもよくわかんない。実際はどちらも置換の振る舞いをコントロールするのに使われるが、違ったやりかたでそうするんだ。

例えば要素 Y が Z で置換されるのを禁止したければ、唯一の方法は block="substitution" を Y に追加することだ。しかしこの属性があっても Z が置換グループに含まれることはエラーではない。文書の中で Y を Z で置換できないというだけのことだ。

さらにまずいことに、Y が置換グループの先頭として別の要素 X を指定すると(X <- Y <- Z) X を Z で置換してもオッケーということになる。

これらはすべて、実験で使った時には無害だと思ったかも知れないけれども、置換グループを現実世界で使うことを非実用的にしている。そしてそれが置換グループを避けるべき理由だ。

なぜカメレオンスキーマを避けるべきか

W3C XML Schema は targetNamespace 属性なしの schema 要素を許している。そしてそんなスキーマをカメレオンスキーマと呼ぶ人たちがいる。
なぜカメレオンスキーマが不適切か… いや、知らなくちゃいけないのはそれを避けるべきということだ。

一つの理由は、ここでバリデータ間の相互運用性の問題がでてくる可能性が大きいからだ。

もう一つの理由はカメレオンスキーマを使ったクールな裏技を発明する人がいるからだ。だけどそんな裏技に手をだしちゃいけない。それはスキーマハッカーのやることだ。善良なる市民がやることじゃない。

カメレオンスキーマをなぜ避けるべきなのか正確な理由を知りたいなら、残念ながら、それが何かを学ぶ必要がある。

こんなカメレオンスキーマを考えてみよう。

<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema">
  <!-- note that targetNamespace attribute is absent. -->
  
  <xs:element name="person">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="familyName" />
        <xs:element ref="lastName" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="familyName" type="xs:string"/>
  <xs:element name="lastName" type="xs:string"/>
</xs:schema>

ここで他のスキーマファイルを書いて上のスキーマを include 要素を使ってインクルードするとしよう。

<xs:schema xmlns:xs="http://www.w3.org/2001/XMSchema"
           targetNamespace="http://example.com">
  
  <xs:include schemaLocation="above.xsd" />
  
  <xs:element name="root">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="person" maxOccurs="unbounded" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

一見これでオッケーのようだが、実はこれではダメだ。赤で書かれた行を見てほしい。これは familyName 要素を参照しているように見える。しかしこれは間違いだ。
このカメレオンスキーマは targetNamespace="http://example.com/" のスキーマからインクルードされているので、familyName 要素はこの名前空間の要素になっているんだ。だからこの要素宣言を参照するには、赤い行をこんなふうに書き換えなくちゃいけない。


<xs:element ref="bp:familyName" xmlns:bp="http://example.com" />

ここで対象名前空間がhttp://www.foo.comのスキーマからこのカメレオンスキーマを再利用したかったらどうしたらいいだろう? 答えは「それは不可能」だ。

これでカメレオンスキーマの唯一のメリットが失われたのがわかっただろう。

さらに困るのは、このエラーを検出しないバリデータがあるということだ。そのようなバリデータは見つからない部品が後から出てくるかもしれないと考えるからだ。

結論

見ての通り避けるべき落とし穴はたくさんある。でもこれらの落とし穴を避ければ学ぶべき事も少なくなるので、あなたの人生はもっとラクになるだろう。しかもそれによって W3C XML Schema の表現力を損なうこともない。

スキーマはシンプルに使って、ハッピーな人生を送ろう!