外部キー=主キーのスキーマを使う場合のowningSide

Twitter@s_kanedaさんがスキーマのowningSideについてつぶやいていました。

symfonyスキーマファイルに出てくるowningSideってなんだろう
http://twitter.com/s_kaneda/status/10203798714

@hidenorigoto ありがとうございます!外部キーが主キーになっているカラムをonDelete: cascadeにしたくて、owningSideを使ってみたらうまくいきました。でもまだよくわかっていません・・・。
http://twitter.com/s_kaneda/status/10209810165

この「owningSide」ですが、どういう時に指定するものなのか分かりやすいドキュメントを見つけられませんでしたが、doctrineのサイトでは以下のページがありました。

http://www.doctrine-project.org/documentation/manual/2_0/en/association-mapping:owning-side-and-inverse-side

抜粋すると、次のように説明があります。

・The many side of one-to-many/many-to-one bidirectional relationships must be the owning side, hence the mappedBy element cannot be specified on the ManyToOne side.
・For one-to-one bidirectional relationships, the owning side corresponds to the side that contains the corresponding foreign key.

一対多のリレーションでは、「多」の側がowning side、一対一のリレーションでは、外部キーを含む側がowning sideとあります。


で、@s_kanedaさんの書かれているような「外部キーが主キーになっている場合」なのですが、これは以前から私も気になっていることでしたので、ちょっと調べてみました。

Doctrineのソース(/Relation/Parser.php

いろいろソースを追いかけたところ、まず次のソースにたどり着きました。
http://trac.doctrine-project.org/browser/branches/1.2/lib/Doctrine/Relation/Parser.php
このパーサークラスのcompleteDefinition()メソッドで、リレーションの定義をテーブルに読み込んでいます。
$def['localKey']という変数にtrueが設定されると、リレーションがDoctrine_Relation_LocalKeyクラスで読み込まれ、そうでない場合はDoctrine_Relation_ForeignKeyクラスで読み込まれます。
リレーションとして正しく動作するためには、リレーションの両端のいずれかがLocalKeyになっていないといけないのですが、主キー=外部キーというテーブルをそのまま設定すると、LocalKeyになりません。
そこで、436行目あたりにあるように、owningSideをtrueに設定すると、うまくLocaKeyと認識してくれるようです。

実際の例

簡単なスキーマで試してみます。

ebb6d70899c8235dd02a0a7bce9f6b81

TableAは単純なテーブル、TableBの主キーは、TableAのidを参照しています。

schema.ymlは以下のようになります。

TableA:
  actAs:
    Timestampable:
  columns:
    id:
      primary: true
      type: integer
      autoincrement: true
    name:
      type: string(32)
      notnull: true
TableB:
  actAs:
    Timestampable:
  columns:
    table_a_id:
      primary: true
      type: integer
    detail:
      type: string
  relations:
    TableA:
      class: TableA
      foreignAlias: TableB
      foreignType: one
      onDelete: CASCADE
      type: one
      local: table_a_id
      foreign: id
      owningSide: true

★一番最後の行のowningSideに注意

これでモデルの生成・テーブルの生成(doctrine:build --all)を実行すると、データベース上にもきちんとリレーションが定義されました。

おまけ

テーブルに定義されているリレーションを調べる

<?php
    $table = Doctrine_Core::getTable('TableB');
    $relations = $table->getRelations();
    foreach ($relations as $relation_name=>$relation_obj)
    {
      echo $relation_name . "\n";
      echo get_class($relation_obj) . "\n";
    }

出力

TableA
Doctrine_Relation_LocalKey


テーブルのスキーマのエクスポート形式を出力する

<?php
    $table = Doctrine_Core::getTable('TableB');
    var_dump($table->getExportableFormat());

出力

array(3) {
  ["tableName"]=>
  string(7) "table_b"
  ["columns"]=>
  array(4) {
    ["table_a_id"]=>
    array(3) {
      ["primary"]=>
      bool(true)
      ["type"]=>
      string(7) "integer"
      ["length"]=>
      int(8)
    }
    ["detail"]=>
    array(2) {
      ["type"]=>
      string(6) "string"
      ["length"]=>
      NULL
    }
    ["created_at"]=>
    array(3) {
      ["notnull"]=>
      bool(true)
      ["type"]=>
      string(9) "timestamp"
      ["length"]=>
      int(25)
    }
    ["updated_at"]=>
    array(3) {
      ["notnull"]=>
      bool(true)
      ["type"]=>
      string(9) "timestamp"
      ["length"]=>
      int(25)
    }
  }
  ["options"]=>
  array(20) {
    ["name"]=>
    string(6) "TableB"
    ["tableName"]=>
    string(7) "table_b"
    ["sequenceName"]=>
    NULL
    ["inheritanceMap"]=>
    array(0) {
    }
    ["enumMap"]=>
    array(0) {
    }
    ["type"]=>
    NULL
    ["charset"]=>
    string(4) "UTF8"
    ["collate"]=>
    NULL
    ["treeImpl"]=>
    NULL
    ["treeOptions"]=>
    array(0) {
    }
    ["indexes"]=>
    array(0) {
    }
    ["parents"]=>
    array(2) {
      [0]=>
      string(16) "sfDoctrineRecord"
      [1]=>
      string(10) "BaseTableB"
    }
    ["joinedParents"]=>
    array(0) {
    }
    ["queryParts"]=>
    array(0) {
    }
    ["versioning"]=>
    NULL
    ["subclasses"]=>
    array(0) {
    }
    ["orderBy"]=>
    NULL
    ["declaringClass"]=>
    object(ReflectionClass)#285 (1) {
      ["name"]=>
      string(10) "BaseTableB"
    }
    ["foreignKeys"]=>
    array(1) {
      ["table_b_table_a_id_table_a_id"]=>
      array(6) {
        ["name"]=>
        string(29) "table_b_table_a_id_table_a_id"
        ["local"]=>
        string(10) "table_a_id"
        ["foreign"]=>
        string(2) "id"
        ["foreignTable"]=>
        string(7) "table_a"
        ["onUpdate"]=>
        NULL
        ["onDelete"]=>
        string(7) "CASCADE"
      }
    }
    ["primary"]=>
    array(1) {
      [0]=>
      string(10) "table_a_id"
    }
  }
}

Doctrineでは、このエクスポート形式を元にデータベースのテーブル生成用SQLが作成されます。
owningSideを定義していない場合、エクスポート形式を調べると該当リレーション情報が出力されません。


P.S.
データモデルはORM Designerで作ってます。(上の画像も)
何度かバージョンアップされて、かなりいい感じになってきました。(まだ不満点もありますが・・・)