OSDN Git Service

Modified method behavior Ethna_Plugin_Cachemanager::getNamespace
[ethna/ethna.git] / class / Ethna_AppObject.php
1 <?php
2 // vim: foldmethod=marker
3 /**
4  *  Ethna_AppObject.php
5  *
6  *  @author     Masaki Fujimoto <fujimoto@php.net>
7  *  @license    http://www.opensource.org/licenses/bsd-license.php The BSD License
8  *  @package    Ethna
9  *  @version    $Id$
10  */
11
12 // {{{ Ethna_AppObject
13 /**
14  *  アプリケーションオブジェクトのベースクラス
15  *
16  *  @author     Masaki Fujimoto <fujimoto@php.net>
17  *  @access     public
18  *  @package    Ethna
19  *  @todo       複数テーブルの対応
20  *  @todo       remove dependency on PEAR::DB
21  *  @todo       quoteidentifier は Ethna_AppSQL に持っていくべき
22  */
23 class Ethna_AppObject
24 {
25     // {{{ properties
26     /**#@+
27      *  @access private
28      */
29
30     /** @var    object  Ethna_Backend       backendオブジェクト */
31     var $backend;
32
33     /** @var    object  Ethna_Config        設定オブジェクト */
34     var $config;
35
36     /** @var    object  Ethna_I18N          i18nオブジェクト */
37     var $i18n;
38
39     /** @var    object  Ethna_ActionForm    アクションフォームオブジェクト */
40     var $action_form;
41
42     /** @var    object  Ethna_ActionForm    アクションフォームオブジェクト(省略形) */
43     var $af;
44
45     /** @var    object  Ethna_Session       セッションオブジェクト */
46     var $session;
47
48     /** @var    string  DB定義プレフィクス */
49     var $db_prefix = null;
50
51     /** @var    array   テーブル定義。対応するDB上のテーブル名を指定します。*/
52     var $table_def = null;
53
54     /** @var    array   プロパティ定義。テーブルのカラム定義を記述します。 */
55     var $prop_def = null;
56
57     /** @var    array   プロパティ。各カラムに対応する実際の値です。 */
58     var $prop = null;
59
60     /** @var    array   プロパティ(バックアップ) */
61     var $prop_backup = null;
62
63     /** @var    int     プロパティ定義キャッシュ有効期間(sec) */
64     var $prop_def_cache_lifetime = 86400;
65
66     /** @var    array   プライマリキー定義 */
67     var $id_def = null;
68
69     /** @var    int     オブジェクトID (プライマリーキーの値) */
70     var $id = null;
71
72     /**#@-*/
73     // }}}
74
75     // {{{ Ethna_AppObject
76     /**
77      *  Ethna_AppObjectクラスのコンストラクタ
78      *
79      *  @access public
80      *  @param  object  Ethna_Backend   &$backend   Ethna_Backendオブジェクト
81      *  @param  mixed   $key_type   レコードを特定するためのカラム名
82      *                              (通常はプライマリーキーのフィールド)
83      *  @param  mixed   $key        レコードを特定するためのカラム値
84      *  @param  array   $prop       プロパティ(レコードの値)一覧
85      *  @return mixed   0:正常終了 -1:キー/プロパティ未指定 Ethna_Error:エラー
86      */
87     function Ethna_AppObject(&$backend, $key_type = null, $key = null, $prop = null)
88     {
89         $this->backend =& $backend;
90         $this->config =& $backend->getConfig();
91         $this->action_form =& $backend->getActionForm();
92         $this->af =& $this->action_form;
93         $this->session =& $backend->getSession();
94         $ctl =& $backend->getController();
95
96         // DBオブジェクトの設定
97         $db_list = $this->_getDBList();
98         if (Ethna::isError($db_list)) {
99             return $db_list;
100         } else if (is_null($db_list['rw'])) {
101             return Ethna::raiseError(
102                 "Ethna_AppObjectを利用するにはデータベース設定が必要です",
103                 E_DB_NODSN);
104         }
105         $this->my_db_rw =& $db_list['rw'];
106         $this->my_db_ro =& $db_list['ro'];
107         // XXX: app objはdb typeを知らなくても動くべき
108         $this->my_db_type = $this->my_db_rw->getType();
109
110         // テーブル定義自動取得
111         // 現在、記述可能なテーブルは常に一つで、primaryはtrue
112         if (is_null($this->table_def)) {
113             $this->table_def = $this->_getTableDef();
114         }
115         if (is_string($this->table_def)) {
116             $this->table_def = array($this->table_def => array('primary' => true));
117         }
118         // プロパティ定義(テーブルのカラム定義)自動取得
119         // データベースから自動取得され、キャッシュされる
120         if (is_null($this->prop_def)) {
121             $this->prop_def = $this->_getPropDef();
122         }
123
124         // プロパティ定義の必須キーを補完
125         foreach (array_keys($this->prop_def) as $k) {
126             if (isset($this->prop_def[$k]['primary']) == false) {
127                 $this->prop_def[$k]['primary'] = false;
128             }
129         }
130
131         // オブジェクトのプライマリキー定義構築
132         foreach ($this->prop_def as $k => $v) {
133             if ($v['primary'] == false) {
134                 continue;
135             }
136             if (is_null($this->id_def)) {
137                 $this->id_def = $k;
138             } else if (is_array($this->id_def)) {
139                 $this->id_def[] = $k;
140             } else {  // scalar の場合
141                 $this->id_def = array($this->id_def, $k);
142             }
143         }
144         
145         // キー妥当性チェック
146         if (is_null($key_type) && is_null($key) && is_null($prop)) {
147             // perhaps for adding object
148             return 0;
149         }
150
151         // プロパティ設定
152         // $key_type, $key が指定されたらDBから値を取得し、設定する
153         // $prop が設定された場合はそれを設定する
154         if (is_null($prop)) {
155             $this->_setPropByDB($key_type, $key);
156         } else {
157             $this->_setPropByValue($prop);
158         }
159
160         $this->prop_backup = $this->prop;
161
162         //   プライマリーキーの値を設定
163         if (is_array($this->id_def)) {
164             $this->id = array();
165             foreach ($this->id_def as $k) {
166                 $this->id[] = $this->prop[$k];
167             }
168         } else {
169             $this->id = $this->prop[$this->id_def];
170         }
171
172         return 0;
173     }
174     // }}}
175
176     // {{{ isValid
177     /**
178      *  有効なオブジェクトかどうかを返す
179      *  プライマリーキーの値が設定されてなければ不正なオブジェクトです。
180      *
181      *  @access public
182      *  @return bool    true:有効 false:無効
183      */
184     function isValid()
185     {
186         if (is_array($this->id)) {
187             return is_null($this->id[0]) ? false : true;
188         } else {
189             return is_null($this->id) ? false : true;
190         }
191     }
192     // }}}
193
194     // {{{ isActive
195     /**
196      *  アクティブなオブジェクトかどうかを返す
197      *
198      *  isValid()メソッドはオブジェクト自体が有効かどうかを判定するのに対し
199      *  isActive()はオブジェクトがアプリケーションとして有効かどうかを返す
200      *
201      *  @access public
202      *  @return bool    true:アクティブ false:非アクティブ
203      */
204     function isActive()
205     {
206         if ($this->isValid() == false) {
207             return false;
208         }
209         return $this->prop['state'] == OBJECT_STATE_ACTIVE ? true : false;
210     }
211     // }}}
212
213     // {{{ getDef
214     /**
215      *  オブジェクトのプロパティ定義(カラム定義)を返す
216      *
217      *  @access public
218      *  @return array   オブジェクトのプロパティ定義
219      */
220     function getDef()
221     {
222         return $this->prop_def;
223     }
224     // }}}
225
226     // {{{ getIdDef
227     /**
228      *  プライマリキー定義を返す
229      *
230      *  @access public
231      *  @return mixed   プライマリキーとなるプロパティ名
232      */
233     function getIdDef()
234     {
235         return $this->id_def;
236     }
237     // }}}
238
239     // {{{ getId
240     /**
241      *  オブジェクトID(primary keyの値)を返す
242      *
243      *  @access public
244      *  @return mixed   オブジェクトID
245      */
246     function getId()
247     {
248         return $this->id;
249     }
250     // }}}
251
252     // {{{ get
253     /**
254      *  オブジェクトプロパティへのアクセサ(R)
255      *
256      *  @access public
257      *  @param  string  $key    プロパティ名(カラム名)
258      *  @return mixed   プロパティ(カラムの値)
259      */
260     function get($key)
261     {
262         if (isset($this->prop_def[$key]) == false) {
263             trigger_error(sprintf("Unknown property [%s]", $key), E_USER_ERROR);
264             return null;
265         }
266         if (isset($this->prop[$key])) {
267             return $this->prop[$key];
268         }
269         return null;
270     }
271     // }}}
272
273     // {{{ getName
274     /**
275      *  オブジェクトプロパティ表示名へのアクセサ
276      *  プロパティ値と、表示用の値が違う場合 (「県」等)に、
277      *  オーバーライドして下さい。
278      *
279      *  表示用の値を返す形で実装します。 
280      *
281      *  @access public
282      *  @param  string  $key    プロパティ(カラム)名
283      *  @return string  プロパティ(カラム)の表示名
284      */
285     function getName($key)
286     {
287         return $this->get($key);
288     }
289     // }}}
290
291     /**
292      *  オブジェクトプロパティ表示名(詳細)へのアクセサ
293      *  プロパティ値と、表示用の値が違う場合 (「県」等)に、
294      *  オーバーライドして下さい。
295      *
296      *  @access public
297      *  @param  string  $key    プロパティ(カラム)名
298      *  @return string  プロパティ(カラム)の表示名(詳細)
299      */
300     function getLongName($key)
301     {
302         return $this->get($key);
303     }
304     // }}}
305
306     // {{{ getNameObject
307     /**
308      *  プロパティ表示名を格納した連想配列を取得する
309      *  すべての getName メソッドの戻り値を配列として返します。
310      *
311      *  @access public
312      *  @return array   プロパティ表示名を格納した連想配列
313      */
314     function getNameObject()
315     {
316         $object = array();
317
318         foreach ($this->prop_def as $key => $elt) {
319             $object[$elt['form_name']] = $this->getName($key);
320         }
321
322         return $object;
323     }
324     // }}}
325
326     // {{{ set
327     /**
328      *  オブジェクトプロパティ(カラムに対応した値)を設定します。
329      *
330      *  @access public
331      *  @param  string  $key    プロパティ(カラム)名
332      *  @param  string  $value  プロパティ値
333      */
334     function set($key, $value)
335     {
336         if (isset($this->prop_def[$key]) == false) {
337             trigger_error(sprintf("Unknown property [%s]", $key), E_USER_ERROR);
338             return null;
339         }
340         $this->prop[$key] = $value;
341     }
342     // }}}
343
344     // {{{ dump
345     /**
346      *  オブジェクトプロパティを指定の形式でダンプする(現在はCSV形式のみサポート)
347      *
348      *  @access public
349      *  @param  string  $type   ダンプ形式("csv"...)
350      *  @return string  ダンプ結果(エラーの場合はnull)
351      */
352     function dump($type = "csv")
353     {
354         $method = "_dump_$type";
355         if (method_exists($this, $method) == false) {
356             return Ethna::raiseError("Undefined Method [%s]", E_APP_NOMETHOD, $method);
357         }
358
359         return $this->$method();
360     }
361     // }}}
362
363     // {{{ importForm
364     /**
365      *  フォーム値からオブジェクトプロパティをインポートする
366      *
367      *  @access public
368      *  @param  int     $option インポートオプション
369      *                  OBJECT_IMPORT_IGNORE_NULL: フォーム値が送信されていない場合はスキップ
370      *                  OBJECT_IMPORT_CONVERT_NULL: フォーム値が送信されていない場合、空文字列に変換
371      */
372     function importForm($option = null)
373     {
374         foreach ($this->getDef() as $k => $def) {
375             $value = $this->af->get($def['form_name']);
376             if (is_null($value)) {
377                 // フォームから値が送信されていない場合の振舞い
378                 if ($option == OBJECT_IMPORT_IGNORE_NULL) {
379                     // nullはスキップ
380                     continue;
381                 } else if ($option == OBJECT_IMPORT_CONVERT_NULL) {
382                     // 空文字列に変換
383                     $value = '';
384                 }
385             }
386             $this->set($k, $value);
387         }
388     }
389     // }}}
390
391     // {{{ exportForm
392     /**
393      *  オブジェクトプロパティをフォーム値にエクスポートする
394      *
395      *  @access public
396      */
397     function exportForm()
398     {
399         foreach ($this->getDef() as $k => $def) {
400             $this->af->set($def['form_name'], $this->get($k));
401         }
402     }
403     // }}}
404
405     // {{{ add
406     /**
407      *  オブジェクトを追加する(INSERT)
408      *
409      *  @access public
410      *  @return mixed   0:正常終了 Ethna_Error:エラー
411      *  @todo remove dependency on PEAR::DB
412      */
413     function add()
414     {
415         // primary key 定義が sequence の場合、
416         // next idの取得: (pgsqlの場合のみ)
417         // 取得できた場合はこのidを使う
418         foreach (to_array($this->id_def) as $id_def) {
419             if (isset($this->prop_def[$id_def]['seq'])
420                 && $this->prop_def[$id_def]['seq']) {
421                 // NOTE: このapp object以外からinsertがないことが前提
422                 $next_id = $this->my_db_rw->getNextId(
423                     $this->prop_def[$id_def]['table'], $id_def);
424                 if ($next_id !== null && $next_id >= 0) {
425                     $this->prop[$id_def] = $next_id;
426                 }
427                 break;
428             }
429         }
430
431         //    INSERT 文を取得し、実行
432         $sql = $this->_getSQL_Add();
433         for ($i = 0; $i < 4; $i++) {
434             $r =& $this->my_db_rw->query($sql);
435             //   エラーの場合 -> 重複キーエラーの場合はリトライ
436             if (Ethna::isError($r)) {
437                 if ($r->getCode() == E_DB_DUPENT) {
438                     // 重複エラーキーの判別
439                     $duplicate_key_list = $this->_getDuplicateKeyList();
440                     if (Ethna::isError($duplicate_key_list)) {
441                         return $duplicate_key_list;
442                     }
443                     if (is_array($duplicate_key_list)
444                         && count($duplicate_key_list) > 0) {
445                         foreach ($duplicate_key_list as $k) {
446                             return Ethna::raiseNotice('Duplicate Key Error [%s]',
447                                                       E_APP_DUPENT, $k);
448                         }
449                     }
450                 } else {
451                     return $r;
452                 }
453             } else {
454                 break;
455             }
456         }
457         if ($i == 4) {
458             // cannot be reached
459             return Ethna::raiseError('Cannot detect Duplicate key Error', E_GENERAL);
460         }
461
462         // last insert idの取得: (mysql, sqliteのみ)
463         // primary key の 'seq' フラグがある(最初の)プロパティに入れる
464         $insert_id = $this->my_db_rw->getInsertId(); 
465         if ($insert_id !== null && $insert_id >= 0) {
466             foreach (to_array($this->id_def) as $id_def) {
467                 if (isset($this->prop_def[$id_def]['seq'])
468                     && $this->prop_def[$id_def]['seq']) {
469                     $this->prop[$id_def] = $insert_id;
470                     break;
471                 }
472             }
473         }
474
475         // ID(Primary Key)の値を設定
476         if (is_array($this->id_def)) {
477             $this->id = array();
478             foreach ($this->id_def as $k) {
479                 $this->id[] = $this->prop[$k];
480             }
481         } else if (isset($this->prop[$this->id_def])) {
482             $this->id = $this->prop[$this->id_def];
483         } else {
484             trigger_error("primary key is missing", E_USER_ERROR);
485         }
486
487         // バックアップ/キャッシュ更新
488         $this->prop_backup = $this->prop;
489         $this->_clearPropCache();
490
491         return 0;
492     }
493     // }}}
494
495     // {{{ update
496     /**
497      *  オブジェクトを更新する(UPDATE)
498      *
499      *  @access public
500      *  @return mixed   0:正常終了 Ethna_Error:エラー
501      *  @todo remove dependency on PEAR::DB
502      */
503     function update()
504     {
505         $sql = $this->_getSQL_Update();
506         //   エラーの場合 -> 重複キーエラーの場合はリトライ(4回)
507         for ($i = 0; $i < 4; $i++) {  //  magic number
508             $r =& $this->my_db_rw->query($sql);
509             if (Ethna::isError($r)) {
510                 if ($r->getCode() == E_DB_DUPENT) {
511                     // 重複エラーキーの判別
512                     $duplicate_key_list = $this->_getDuplicateKeyList();
513                     if (Ethna::isError($duplicate_key_list)) {
514                         return $duplicate_key_list;
515                     }
516                     if (is_array($duplicate_key_list)
517                         && count($duplicate_key_list) > 0) {
518                         foreach ($duplicate_key_list as $k) {
519                             return Ethna::raiseNotice('Duplicate Key Error [%s]',
520                                                       E_APP_DUPENT, $k);
521                         }
522                     }
523                 } else {
524                     return $r;
525                 }
526             } else {
527                 break;
528             }
529         }
530         if ($i == 4) {
531             // cannot be reached
532             return Ethna::raiseError('Cannot detect Duplicate key Error', E_GENERAL);
533         }
534
535         $affected_rows = $this->my_db_rw->affectedRows();
536         if ($affected_rows <= 0) {
537             $this->backend->log(LOG_DEBUG, "update query with 0 updated rows");
538         }
539
540         // バックアップ/キャッシュ更新
541         $this->prop_backup = $this->prop;
542         $this->_clearPropCache();
543
544         return 0;
545     }
546     // }}}
547
548     // {{{ replace
549     /**
550      *  オブジェクトを置換する
551      *
552      *  MySQLのREPLACE文に相当する動作を行う(add()で重複エラーが発生したら
553      *  update()を行う)
554      *
555      *  @access public
556      *  @return mixed   0:正常終了 >0:オブジェクトID(追加時) Ethna_Error:エラー
557      *  @todo remove dependency on PEAR::DB
558      */
559     function replace()
560     {
561         $sql = $this->_getSQL_Select($this->getIdDef(), $this->getId());
562
563         //   重複機ーエラーの場合はリトライ(4回) 
564         for ($i = 0; $i < 3; $i++) {  // magic number
565             $r = $this->my_db_rw->query($sql);
566             if (Ethna::isError($r)) {
567                 return $r;
568             }
569             $n = $r->numRows();
570
571             if ($n > 0) {
572                 $r = $this->update();
573                 return $r;
574             } else {
575                 $r = $this->add();
576                 if (Ethna::isError($r) == false) {
577                     return $r;
578                 } else if ($r->getCode() != E_APP_DUPENT) {
579                     return $r;
580                 }
581             }
582         }
583         
584         return $r;
585     }
586     // }}}
587
588     // {{{ remove
589     /**
590      *  オブジェクト(レコード)を削除する
591      *
592      *  @access public
593      *  @return mixed   0:正常終了 Ethna_Error:エラー
594      *  @todo remove dependency on PEAR::DB
595      */
596     function remove()
597     {
598         $sql = $this->_getSQL_Remove();
599         $r =& $this->my_db_rw->query($sql);
600         if (Ethna::isError($r)) {
601             return $r;
602         }
603
604         // プロパティ/バックアップ/キャッシュクリア
605         $this->id = $this->prop = $this->prop_backup = null;
606         $this->_clearPropCache();
607
608         return 0;
609     }
610     // }}}
611
612     // {{{ searchId
613     /**
614      *  オブジェクトID(プライマリーキーの値)を検索する
615      *
616      *  @access public
617      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
618      *  @param  array   $order      検索結果ソート条件
619      *                              (カラム名をキー。値には、昇順の場合は OBJECT_SORT_ASC, 降順の場合は OBJECT_SORT_DESC)
620      *  @param  int     $offset     検索結果取得オフセット
621      *  @param  int     $count      検索結果取得数
622      *  @return mixed   array(0 => 検索条件にマッチした件数,
623      *                  1 => $offset, $countにより指定された件数のオブジェクトID一覧)
624      *                  Ethna_Error:エラー
625      *  TODO: remove dependency on PEAR::DB
626      */
627     function searchId($filter = null, $order = null, $offset = null, $count = null)
628     {
629        //   プライマリーキー件数検索
630        if (is_null($offset) == false || is_null($count) == false) {
631             $sql = $this->_getSQL_SearchLength($filter);
632             $r =& $this->my_db_ro->query($sql);
633             if (Ethna::isError($r)) {
634                 return $r;
635             }
636             $row = $this->my_db_ro->fetchRow($r, DB_FETCHMODE_ASSOC);
637             $length = $row['id_count'];
638         } else {
639             $length = null;
640         }
641
642         $id_list = array();
643         $sql = $this->_getSQL_SearchId($filter, $order, $offset, $count);
644         $r =& $this->my_db_ro->query($sql);
645         if (Ethna::isError($r)) {
646             return $r;
647         }
648         $n = $r->numRows();
649         for ($i = 0; $i < $n; $i++) {
650             $row = $this->my_db_ro->fetchRow($r, DB_FETCHMODE_ASSOC);
651
652             // プライマリキーが1カラムならスカラー値に変換
653             if (is_array($this->id_def) == false) {
654                 $row = $row[$this->id_def];
655             }
656             $id_list[] = $row;
657         }
658         if (is_null($length)) {
659             $length = count($id_list);
660         }
661
662         return array($length, $id_list);
663     }
664     // }}}
665
666     // {{{ searchProp
667     /**
668      *  オブジェクトプロパティ(レコード)を検索する
669      *
670      *  @access public
671      *  @param  array   $keys       取得するプロパティ(カラム名)
672      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
673      *  @param  array   $order      検索結果ソート条件
674      *                              (カラム名をキー。値には、昇順の場合は OBJECT_SORT_ASC, 降順の場合は OBJECT_SORT_DESC)
675      *  @param  int     $offset     検索結果取得オフセット
676      *  @param  int     $count      検索結果取得数
677      *  @return mixed   array(0 => 検索条件にマッチした件数,
678      *                  1 => $offset, $countにより指定された件数のオブジェクトプロパティ一覧)
679      *                  Ethna_Error:エラー
680      *  TODO: remove dependency on PEAR::DB
681      */
682     function searchProp($keys = null, $filter = null, $order = null,
683                         $offset = null, $count = null)
684     {
685         //   プライマリーキー件数検索
686         if (is_null($offset) == false || is_null($count) == false) {
687             $sql = $this->_getSQL_SearchLength($filter);
688             $r =& $this->my_db_ro->query($sql);
689             if (Ethna::isError($r)) {
690                 return $r;
691             }
692             $row = $this->my_db_ro->fetchRow($r, DB_FETCHMODE_ASSOC);
693             $length = $row['id_count'];
694         } else {
695             $length = null;
696         }
697
698         $prop_list = array();
699         $sql = $this->_getSQL_SearchProp($keys, $filter, $order, $offset, $count);
700         $r =& $this->my_db_ro->query($sql);
701         if (Ethna::isError($r)) {
702             return $r;
703         }
704         $n = $r->numRows();
705         for ($i = 0; $i < $n; $i++) {
706             $row = $this->my_db_ro->fetchRow($r, DB_FETCHMODE_ASSOC);
707             $prop_list[] = $row;
708         }
709         if (is_null($length)) {
710             $length = count($prop_list);
711         }
712
713         return array($length, $prop_list);
714     }
715     // }}}
716
717     // {{{ _setDefault
718     /**
719      *  オブジェクトのアプリケーションデフォルトプロパティを設定する
720      *
721      *  コンストラクタにより指定されたキーにマッチするエントリがなかった場合の
722      *  デフォルトプロパティをここで設定することが出来る
723      *
724      *  @access protected
725      *  @param  mixed   $key_type   検索キー名
726      *  @param  mixed   $key        検索キー
727      *  @return int     0:正常終了
728      */
729     function _setDefault($key_type, $key)
730     {
731         return 0;
732     }
733     // }}}
734
735     // {{{ _setPropByDB
736     /**
737      *  オブジェクトプロパティをDBから取得する
738      *
739      *  @access private
740      *  @param  mixed   $key_type   検索キー名
741      *  @param  mixed   $key        検索キー
742      *  TODO: depend on PEAR::DB
743      */
744     function _setPropByDB($key_type, $key)
745     {
746         global $_ETHNA_APP_OBJECT_CACHE;
747
748         $key_type = to_array($key_type);
749         $key = to_array($key);
750         if (count($key_type) != count($key)) {
751             trigger_error(sprintf("Unmatched key_type & key length [%d-%d]",
752                           count($key_type), count($key)), E_USER_ERROR);
753             return;
754         }
755         foreach ($key_type as $elt) {
756             if (isset($this->prop_def[$elt]) == false) {
757                 trigger_error("Invalid key_type [$elt]", E_USER_ERROR);
758                 return;
759             }
760         }
761
762         // キャッシュチェック
763         $class_name = strtolower(get_class($this));
764         if (is_array($_ETHNA_APP_OBJECT_CACHE) == false
765             || array_key_exists($class_name, $_ETHNA_APP_OBJECT_CACHE) == false) {
766             $_ETHNA_APP_OBJECT_CACHE[$class_name] = array();
767         }
768         $cache_key = serialize(array($key_type, $key));
769         if (array_key_exists($cache_key, $_ETHNA_APP_OBJECT_CACHE[$class_name])) {
770             $this->prop = $_ETHNA_APP_OBJECT_CACHE[$class_name][$cache_key];
771             return;
772         }
773
774         // SQL文構築
775         $sql = $this->_getSQL_Select($key_type, $key);
776
777         // プロパティ取得
778         $r =& $this->my_db_ro->query($sql);
779         if (Ethna::isError($r)) {
780             return;
781         }
782         $n = $r->numRows();
783         if ($n == 0) {
784             // try default
785             if ($this->_setDefault($key_type, $key) == false) {
786                 // nop
787             }
788             return;
789         } else if ($n > 1) {
790             trigger_error("Invalid key (multiple rows found) [$key]", E_USER_ERROR);
791             return;
792         }
793         $this->prop = $this->my_db_ro->fetchRow($r, DB_FETCHMODE_ASSOC);
794
795         // キャッシュアップデート
796         $_ETHNA_APP_OBJECT_CACHE[$class_name][$cache_key] = $this->prop;
797     }
798     // }}}
799
800     // {{{ _setPropByValue
801     /**
802      *  コンストラクタで指定されたプロパティを設定する
803      *
804      *  @access private
805      *  @param  array   $prop   プロパティ一覧
806      */
807     function _setPropByValue($prop)
808     {
809         $def = $this->getDef();
810         foreach ($def as $key => $value) {
811             if ($value['primary'] && isset($prop[$key]) == false) {
812                 // プライマリキーは省略不可
813                 trigger_error("primary key is not identical", E_USER_ERROR);
814             }
815             $this->prop[$key] = $prop[$key];
816         }
817     }
818     // }}}
819
820     // {{{ _getPrimaryTable
821     /**
822      *  オブジェクトのプライマリテーブルを取得する
823      *
824      *  @access private
825      *  @return string  オブジェクトのプライマリテーブル名
826      */
827     function _getPrimaryTable()
828     {
829         $tables = array_keys($this->table_def);
830         $table = $tables[0];
831         
832         return $table;
833     }
834     // }}}
835
836     // {{{ _getDuplicateKeyList
837     /**
838      *  重複キーを取得する
839      *
840      *  @access private
841      *  @return mixed   0:重複なし Ethna_Error:エラー array:重複キーのプロパティ名一覧
842      *  TODO: depend on PEAR::DB
843      */
844     function _getDuplicateKeyList()
845     {
846         $duplicate_key_list = array();
847
848         // 現在設定されているプライマリキーにNULLが含まれる場合は検索しない
849         $check_pkey = true;
850         foreach (to_array($this->id_def) as $k) {
851             if (isset($this->prop[$k]) == false || is_null($this->prop[$k])) {
852                 $check_pkey = false;
853                 break;
854             }
855         }
856
857         // プライマリキーはmulti columnsになり得るので別扱い
858         if ($check_pkey) {
859             $sql = $this->_getSQL_Duplicate($this->id_def);
860             $r =& $this->my_db_rw->query($sql);
861             if (Ethna::isError($r)) {
862                 return $r;
863             } else if ($r->numRows() > 0) {
864                 // we can overwrite $key_list here
865                 $duplicate_key_list = to_array($this->id_def);
866             }
867         }
868
869         // ユニークキー
870         foreach ($this->prop_def as $k => $v) {
871             if ($v['primary'] == true || $v['key'] == false) {
872                 continue;
873             }
874             $sql = $this->_getSQL_Duplicate($k);
875             $r =& $this->my_db_rw->query($sql);
876             if (Ethna::isError($r)) {
877                 return $r;
878             } else if ($r->NumRows() > 0) {
879                 $duplicate_key_list[] = $k;
880             }
881         }
882
883         if (count($duplicate_key_list) > 0) {
884             return $duplicate_key_list;
885         } else {
886             return 0;
887         }
888     }
889     // }}}
890
891     // {{{ _getSQL_Select
892     /**
893      *  オブジェクトプロパティを取得するSQL文を構築する
894      *
895      *  @access private
896      *  @param  array   $key_type   検索キーとなるプロパティ(カラム)名一覧
897      *  @param  array   $key        $key_typeに対応するキー一覧
898      *  @return string  SELECT文
899      */
900     function _getSQL_Select($key_type, $key)
901     {
902         $key_type = to_array($key_type);
903         if (is_null($key)) {
904             // add()前
905             $key = array();
906             for ($i = 0; $i < count($key_type); $i++) {
907                 $key[$i] = null;
908             }
909         } else {
910             $key = to_array($key);
911         }
912
913         // SQLエスケープ
914         Ethna_AppSQL::escapeSQL($key, $this->my_db_type);
915
916         $tables = implode(',',
917             $this->my_db_ro->quoteIdentifier(array_keys($this->table_def)));
918         $columns = implode(',',
919             $this->my_db_ro->quoteIdentifier(array_keys($this->prop_def)));
920
921         // 検索条件
922         $condition = null;
923         for ($i = 0; $i < count($key_type); $i++) {
924             if (is_null($condition)) {
925                 $condition = "WHERE ";
926             } else {
927                 $condition .= " AND ";
928             }
929             $condition .= Ethna_AppSQL::getCondition(
930                 $this->my_db_ro->quoteIdentifier($key_type[$i]), $key[$i]);
931         }
932
933         $sql = "SELECT $columns FROM $tables $condition";
934
935         return $sql;
936     }
937     // }}}
938
939     // {{{ _getSQL_Add
940     /**
941      *  オブジェクトと追加するSQL文を構築する
942      *
943      *  @access private
944      *  @return string  オブジェクトを追加するためのINSERT文
945      */
946     function _getSQL_Add()
947     {
948         $tables = implode(',',
949             $this->my_db_rw->quoteIdentifier(array_keys($this->table_def)));
950
951         $key_list = array();
952         $set_list = array();
953         $prop_arg_list = $this->prop;
954
955         Ethna_AppSQL::escapeSQL($prop_arg_list, $this->my_db_type);
956         foreach ($this->prop_def as $k => $v) {
957             if (isset($prop_arg_list[$k]) == false) {
958                 continue;
959             }
960             $key_list[] = $this->my_db_rw->quoteIdentifier($k);
961             $set_list[] = $prop_arg_list[$k];
962         }
963
964         $key_list = implode(', ', $key_list);
965         $set_list = implode(', ', $set_list);
966         $sql = "INSERT INTO $tables ($key_list) VALUES ($set_list)";
967
968         return $sql;
969     }
970     // }}}
971
972     // {{{ _getSQL_Update
973     /**
974      *  オブジェクトプロパティを更新するSQL文を構築する
975      *
976      *  @access private
977      *  @return オブジェクトプロパティを更新するためのUPDATE文
978      */
979     function _getSQL_Update()
980     {
981         $tables = implode(',',
982             $this->my_db_rw->quoteIdentifier(array_keys($this->table_def)));
983
984         // SET句構築
985         $set_list = "";
986         $prop_arg_list = $this->prop;
987         Ethna_AppSQL::escapeSQL($prop_arg_list, $this->my_db_type);
988         foreach ($this->prop_def as $k => $v) {
989             if ($set_list != "") {
990                 $set_list .= ",";
991             }
992             $set_list .= sprintf("%s=%s",
993                                  $this->my_db_rw->quoteIdentifier($k),
994                                  $prop_arg_list[$k]);
995         }
996
997         // 検索条件(primary key)
998         $condition = null;
999         foreach (to_array($this->id_def) as $k) {
1000             if (is_null($condition)) {
1001                 $condition = "WHERE ";
1002             } else {
1003                 $condition .= " AND ";
1004             }
1005             $v = $this->prop_backup[$k];    // equals to $this->id
1006             Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
1007             $condition .= Ethna_AppSQL::getCondition(
1008                 $this->my_db_rw->quoteIdentifier($k), $v);
1009         }
1010
1011         $sql = "UPDATE $tables SET $set_list $condition";
1012
1013         return $sql;
1014     }
1015     // }}}
1016
1017     // {{{ _getSQL_Remove
1018     /**
1019      *  オブジェクトを削除するSQL文を構築する
1020      *
1021      *  @access private
1022      *  @return string  オブジェクトを削除するためのDELETE文
1023      */
1024     function _getSQL_Remove()
1025     {
1026         $tables = implode(',',
1027             $this->my_db_rw->quoteIdentifier(array_keys($this->table_def)));
1028
1029         // 検索条件(primary key)
1030         $condition = null;
1031         foreach (to_array($this->id_def) as $k) {
1032             if (is_null($condition)) {
1033                 $condition = "WHERE ";
1034             } else {
1035                 $condition .= " AND ";
1036             }
1037             $v = $this->prop_backup[$k];    // equals to $this->id
1038             Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
1039             $condition .= Ethna_AppSQL::getCondition(
1040                 $this->my_db_rw->quoteIdentifier($k), $v);
1041         }
1042         if (is_null($condition)) {
1043             trigger_error("DELETE with no conditon", E_USER_ERROR);
1044             return null;
1045         }
1046
1047         $sql = "DELETE FROM $tables $condition";
1048
1049         return $sql;
1050     }
1051     // }}}
1052
1053     // {{{ _getSQL_Duplicate
1054     /**
1055      *  オブジェクトプロパティのユニークチェックを行うSQL文を構築する
1056      *
1057      *  @access private
1058      *  @param  mixed   $key    ユニークチェックを行うプロパティ名
1059      *  @return string  ユニークチェックを行うためのSELECT文
1060      */
1061     function _getSQL_Duplicate($key)
1062     {
1063         $tables = implode(',',
1064             $this->my_db_ro->quoteIdentifier(array_keys($this->table_def)));
1065         $columns = implode(',',
1066             $this->my_db_ro->quoteIdentifier(array_keys($this->prop_def)));
1067
1068         $condition = null;
1069         // 検索条件(現在設定されているプライマリキーは検索対象から除く)
1070         if (is_null($this->id) == false) {
1071             $primary_value = to_array($this->getId());
1072             $n = 0;
1073             foreach (to_array($this->id_def) as $k) {
1074                 if (is_null($condition)) {
1075                     $condition = "WHERE ";
1076                 } else {
1077                     $condition .= " AND ";
1078                 }
1079                 $value = $primary_value[$n];
1080                 Ethna_AppSQL::escapeSQL($value, $this->my_db_type);
1081                 $condition .= Ethna_AppSQL::getCondition(
1082                     $this->my_db_ro->quoteIdentifier($k), $value, OBJECT_CONDITION_NE);
1083                 $n++;
1084             }
1085         }
1086
1087         foreach (to_array($key) as $k) {
1088             if (is_null($condition)) {
1089                 $condition = "WHERE ";
1090             } else {
1091                 $condition .= " AND ";
1092             }
1093             $v = $this->prop[$k];
1094             Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
1095             $condition .= Ethna_AppSQL::getCondition(
1096                 $this->my_db_ro->quoteIdentifier($k), $v);
1097         }
1098
1099         $sql = "SELECT $columns FROM $tables $condition";
1100
1101         return $sql;
1102     }
1103     // }}}
1104
1105     // {{{ _getSQL_SearchLength
1106     /**
1107      *  オブジェクト検索総数(offset, count除外)を取得するSQL文を構築する
1108      *
1109      *  @access private
1110      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
1111      *  @return string  検索総数を取得するためのSELECT文
1112      *  @todo   my_db_typeの参照を廃止
1113      */
1114     function _getSQL_SearchLength($filter)
1115     {
1116         // テーブル名をクォートした上で連結。
1117         $tables = implode(',',
1118             $this->my_db_ro->quoteIdentifier(array_keys($this->table_def)));
1119
1120         // プライマリーキー以外の検索条件が含まれていた
1121         // 場合は、追加テーブルがあるとみなし、
1122         // その解釈は _SQLPlugin_SearchTable に任せる
1123         if ($this->_isAdditionalField($filter)) {
1124             $tables .= " " . $this->_SQLPlugin_SearchTable();
1125         }
1126
1127         $id_def = to_array($this->id_def);
1128
1129         //  テーブル名.プライマリーキー名
1130         //  複数あった場合ははじめのものを使う
1131         $column_id = $this->my_db_ro->quoteIdentifier($this->_getPrimaryTable())
1132              . "." . $this->my_db_ro->quoteIdentifier($id_def[0]);
1133         $id_count = $this->my_db_ro->quoteIdentifier('id_count');
1134         $condition = $this->_getSQL_SearchCondition($filter);
1135
1136         if ($this->my_db_type === 'sqlite') {
1137             $sql = "SELECT COUNT(*) AS $id_count FROM "
1138                 . " (SELECT DISTINCT $column_id FROM $tables $condition)";
1139         } else {
1140             $sql = "SELECT COUNT(DISTINCT $column_id) AS $id_count "
1141                 . "FROM $tables $condition";
1142         }
1143
1144         return $sql;
1145     }
1146     // }}}
1147
1148     // {{{ _getSQL_SearchId
1149     /**
1150      *  オブジェクトID(プライマリーキー)検索を行うSQL文を構築する
1151      *
1152      *  @access private
1153      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
1154      *  @param  array   $order      検索結果ソート条件
1155      *                              (カラム名をキー。値には、昇順の場合は OBJECT_SORT_ASC, 降順の場合は OBJECT_SORT_DESC)
1156      *  @param  int     $offset     検索結果取得オフセット
1157      *  @param  int     $count      検索結果取得数
1158      *  @return string  オブジェクト検索を行うSELECT文
1159      */
1160     function _getSQL_SearchId($filter, $order, $offset, $count)
1161     {
1162         // テーブル
1163         $tables = implode(',',
1164             $this->my_db_ro->quoteIdentifier(array_keys($this->table_def)));
1165         if ($this->_isAdditionalField($filter)
1166             || $this->_isAdditionalField($order)) {
1167             $tables .= " " . $this->_SQLPlugin_SearchTable();
1168         }
1169
1170         $column_id = "";
1171         foreach (to_array($this->id_def) as $id) {
1172             if ($column_id != "") {
1173                 $column_id .= ",";
1174             }
1175             $column_id .= $this->my_db_ro->quoteIdentifier($this->_getPrimaryTable())
1176                 . "." . $this->my_db_ro->quoteIdentifier($id);
1177         }
1178         $condition = $this->_getSQL_SearchCondition($filter);
1179
1180         $sort = "";
1181         if (is_array($order)) {
1182             foreach ($order as $k => $v) {
1183                 if ($sort == "") {
1184                     $sort = "ORDER BY ";
1185                 } else {
1186                     $sort .= ", ";
1187                 }
1188                 $sort .= sprintf("%s %s", $this->my_db_ro->quoteIdentifier($k),
1189                                  $v == OBJECT_SORT_ASC ? "ASC" : "DESC");
1190             }
1191         }
1192
1193         $limit = "";
1194         if (is_null($count) == false) {
1195             $limit = sprintf("LIMIT %d", $count);
1196             if (is_null($offset) == false) {
1197                 $limit .= sprintf(" OFFSET %d", $offset);
1198             }
1199         }
1200
1201         $sql = "SELECT DISTINCT $column_id FROM $tables $condition $sort $limit";
1202
1203         return $sql;
1204     }
1205     // }}}
1206
1207     // {{{ _getSQL_SearchProp
1208     /**
1209      *  オブジェクトプロパティ検索を行うSQL文を構築する
1210      *
1211      *  @access private
1212      *  @param  array   $keys       取得プロパティ(カラム名)一覧
1213      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
1214      *  @param  array   $order      検索結果ソート条件
1215      *                              (カラム名をキー。値には、昇順の場合は OBJECT_SORT_ASC, 降順の場合は OBJECT_SORT_DESC)
1216      *  @param  int     $offset     検索結果取得オフセット
1217      *  @param  int     $count      検索結果取得数
1218      *  @return string  オブジェクト検索を行うSELECT文
1219      */
1220     function _getSQL_SearchProp($keys, $filter, $order, $offset, $count)
1221     {
1222         // テーブル
1223         $tables = implode(',',
1224             $this->my_db_ro->quoteIdentifier(array_keys($this->table_def)));
1225         if ($this->_isAdditionalField($filter)
1226             || $this->_isAdditionalField($order)) {
1227             $tables .= " " . $this->_SQLPlugin_SearchTable();
1228         }
1229         $p_table = $this->_getPrimaryTable();
1230
1231         //  検索用追加プロパティ
1232         //  プライマリーキー以外の検索キーが含まれていた
1233         //  場合は、その解釈を _SQLPlugin_SearchPropDef に任せる
1234         //
1235         //  これによって、複数のテーブルの条件を指定することが
1236         //  できる(一応. ダサいけど)
1237         if ($this->_isAdditionalField($filter)
1238             || $this->_isAdditionalField($order)) {
1239             $search_prop_def = $this->_SQLPlugin_SearchPropDef();
1240         } else {
1241             $search_prop_def = array();
1242         }
1243         $def = array_merge($this->getDef(), $search_prop_def);
1244
1245         // カラム
1246         $column = "";
1247         $keys = $keys === null ? array_keys($def) : to_array($keys);
1248         foreach ($keys as $key) {
1249             if ($column != "") {
1250                 $column .= ", ";
1251             }
1252             $t = isset($def[$key]['table']) ? $def[$key]['table'] : $p_table;
1253             //   テーブル名.カラム名
1254             $column .= sprintf("%s.%s",
1255                                $this->my_db_ro->quoteIdentifier($t),
1256                                $this->my_db_ro->quoteIdentifier($key));
1257         }
1258
1259         // WHERE の条件
1260         $condition = $this->_getSQL_SearchCondition($filter);
1261
1262         // ORDER BY
1263         $sort = "";
1264         if (is_array($order)) {
1265             foreach ($order as $k => $v) {
1266                 if ($sort == "") {
1267                     $sort = "ORDER BY ";
1268                 } else {
1269                     $sort .= ", ";
1270                 }
1271                 $sort .= sprintf("%s %s",
1272                                  $this->my_db_ro->quoteIdentifier($k),
1273                                  $v == OBJECT_SORT_ASC ? "ASC" : "DESC");
1274             }
1275         }
1276
1277         // LIMIT, OFFSET
1278         $limit = "";
1279         if (is_null($count) == false) {
1280             $limit = sprintf("LIMIT %d", $count);
1281             if (is_null($offset) == false) {
1282                 $limit .= sprintf(" OFFSET %d", $offset);
1283             }
1284         }
1285
1286         $sql = "SELECT $column FROM $tables $condition $sort $limit";
1287
1288         return $sql;
1289     }
1290     // }}}
1291
1292     // {{{ _getSQL_SearchCondition
1293     /**
1294      *  オブジェクト検索SQLの条件文を構築する
1295      *
1296      *  @access private
1297      *  @param  array   $filter     WHERE検索条件(カラム名をキー、値には実際の条件値か、Ethna_AppSearchObjectを指定)
1298      *  @return string  オブジェクト検索の条件文(エラーならnull)
1299      */
1300     function _getSQL_SearchCondition($filter)
1301     {
1302         if (is_array($filter) == false) {
1303             return "";
1304         }
1305
1306         $p_table = $this->_getPrimaryTable();
1307
1308         //  検索用追加プロパティ
1309         //  プライマリーキー以外の検索キーが含まれていた
1310         //  場合は、その解釈を _SQLPlugin_SearchPropDef に任せる
1311         //
1312         //  これによって、複数のテーブルの条件を指定することが
1313         //  できる(一応. ダサいけど)
1314         if ($this->_isAdditionalField($filter)) {
1315             $search_prop_def = $this->_SQLPlugin_SearchPropDef();
1316         } else {
1317             $search_prop_def = array();
1318         }
1319         $prop_def = array_merge($this->prop_def, $search_prop_def);
1320
1321         $condition = null;
1322         foreach ($filter as $k => $v) {
1323             if (isset($prop_def[$k]) == false) {
1324                 trigger_error(sprintf("Unknown property [%s]", $k), E_USER_ERROR);
1325                 return null;
1326             }
1327
1328             if (is_null($condition)) {
1329                 $condition = "WHERE ";
1330             } else {
1331                 $condition .= " AND ";
1332             }
1333
1334             $t = isset($prop_def[$k]['table']) ? $prop_def[$k]['table'] : $p_table;
1335
1336             // 細かい条件を指定するには、Ethna_AppSearchObject
1337             // を使う必要がある  文字列の場合は LIKE, 数値の場合
1338             // は = 条件しか指定できないからである。
1339             if (is_object($v)) {
1340                 // Ethna_AppSearchObjectが指定されている場合
1341                 $condition .= $v->toString(
1342                     $this->my_db_ro->quoteIdentifier($t)
1343                     .'.'. $this->my_db_ro->quoteIdentifier($k));
1344             } else if (is_array($v) && count($v) > 0 && is_object($v[0])) {
1345                 // Ethna_AppSearchObjectが配列で指定されている場合
1346                 $n = 0;
1347                 foreach ($v as $so) {
1348                     if ($n > 0) {
1349                         $condition .= " AND ";
1350                     }
1351                     $condition .= $so->toString(
1352                         $this->my_db_ro->quoteIdentifier($t)
1353                         .'.'. $this->my_db_ro->quoteIdentifier($k));
1354                     $n++;
1355                 }
1356             } else if ($prop_def[$k]['type'] == VAR_TYPE_STRING) {
1357                 // 省略形(文字列)
1358                 Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
1359                 $condition .= Ethna_AppSQL::getCondition(
1360                     $this->my_db_ro->quoteIdentifier($t)
1361                     .'.'. $this->my_db_ro->quoteIdentifier($k),
1362                     $v, OBJECT_CONDITION_LIKE);
1363             } else {
1364                 // 省略形(数値)
1365                 Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
1366                 $condition .= Ethna_AppSQL::getCondition(
1367                     $this->my_db_ro->quoteIdentifier($t)
1368                     .'.'. $this->my_db_ro->quoteIdentifier($k),
1369                     $v, OBJECT_CONDITION_EQ);
1370             }
1371         }
1372
1373         return $condition;
1374     }
1375     // }}}
1376
1377     // {{{ _SQLPlugin_SearchTable
1378     /**
1379      *  オブジェクト検索SQLプラグイン(追加テーブル)
1380      *
1381      *  sample:
1382      *  <code>
1383      *  return " LEFT JOIN bar_tbl ON foo_tbl.user_id=bar_tbl.user_id";
1384      *  </code>
1385      *
1386      *  @access protected
1387      *  @return string  テーブルJOINのSQL文
1388      */
1389     function _SQLPlugin_SearchTable()
1390     {
1391         return "";
1392     }
1393     // }}}
1394
1395     // {{{ _SQLPlugin_SearchPropDef
1396     /**
1397      *  オブジェクト検索SQLプラグイン(追加条件定義)
1398      *
1399      *  sample:
1400      *  <code>
1401      *  $search_prop_def = array(
1402      *    'group_id' => array(
1403      *      'primary' => true, 'key' => true, 'type' => VAR_TYPE_INT,
1404      *      'form_name' => 'group_id', 'table' => 'group_user_tbl',
1405      *    ),
1406      *  );
1407      *  return $search_prop_def;
1408      *  </code>
1409      *
1410      *  @access protected
1411      *  @return array   追加条件定義
1412      */
1413     function _SQLPlugin_SearchPropDef()
1414     {
1415         return array();
1416     }
1417     // }}}
1418
1419     // {{{ _dump_csv
1420     /**
1421      *  オブジェクトプロパティをCSV形式でダンプする
1422      *
1423      *  @access protected
1424      *  @return string  ダンプ結果
1425      */
1426     function _dump_csv()
1427     {
1428         $dump = "";
1429
1430         $n = 0;
1431         foreach ($this->getDef() as $k => $def) {
1432             if ($n > 0) {
1433                 $dump .= ",";
1434             }
1435             $dump .= Ethna_Util::escapeCSV($this->getName($k));
1436             $n++;
1437         }
1438
1439         return $dump;
1440     }
1441     // }}}
1442
1443     // {{{ _isAdditionalField
1444     /**
1445      *  (検索条件|ソート条件)フィールドにプライマリーキー以外
1446      *  の追加フィールドが含まれるかどうかを返す
1447      *
1448      *  @access private
1449      *  @param  array   $field  (検索条件|ソート条件)定義
1450      *  @return bool    true:含まれる false:含まれない
1451      */
1452     function _isAdditionalField($field)
1453     {
1454         if (is_array($field) == false) {
1455             return false;
1456         }
1457
1458         $def = $this->getDef();
1459         foreach ($field as $key => $value) {
1460             if (array_key_exists($key, $def) == false) {
1461                 return true;
1462             }
1463             if (is_object($value)) {
1464                 // Ethna_AppSearchObject
1465                 if ($value->isTarget($key)) {
1466                     return true;
1467                 }
1468             }
1469         }
1470         return false;
1471     }
1472     // }}}
1473
1474     // {{{ _clearPropCache
1475     /**
1476      *  キャッシュデータを削除する
1477      *
1478      *  @access private
1479      */
1480     function _clearPropCache()
1481     {
1482         $class_name = strtolower(get_class($this));
1483         foreach (array('_ETHNA_APP_OBJECT_CACHE',
1484                        '_ETHNA_APP_MANAGER_OL_CACHE',
1485                        '_ETHNA_APP_MANAGER_OPL_CACHE',
1486                        '_ETHNA_APP_MANAGER_OP_CACHE') as $key) {
1487             if (array_key_exists($key, $GLOBALS)
1488                 && array_key_exists($class_name, $GLOBALS[$key])) {
1489                 unset($GLOBALS[$key][$class_name]);
1490             }
1491         }
1492     }
1493     // }}}
1494
1495     // {{{ _getDBList
1496     /**
1497      *  DBオブジェクト(read only/read-write)を取得する
1498      *
1499      *  @access protected
1500      *  @return array   array('ro' => {read only db object}, 'rw' => {read-write db object})
1501      */
1502     function _getDBList()
1503     {
1504         $r = array('ro' => null, 'rw' => null);
1505
1506         $db_list = $this->backend->getDBList();
1507         if (Ethna::isError($db_list)) {
1508             return $r;
1509         }
1510         foreach ($db_list as $elt) {
1511             if ($this->db_prefix) {
1512                 // 特定のプレフィクスが指定されたDB接続を利用
1513                 // (テーブルごとにDBが異なる場合など)
1514                 if (strncmp($this->db_prefix,
1515                             $elt['key'],
1516                             strlen($this->db_prefix)) != 0) {
1517                     continue;
1518                 }
1519             }
1520
1521             $varname = $elt['varname'];
1522
1523             // for B.C.
1524             $this->$varname =& $elt['db'];
1525
1526             if ($elt['type'] == DB_TYPE_RW) {
1527                 $r['rw'] =& $elt['db'];
1528             } else if ($elt['type'] == DB_TYPE_RO) {
1529                 $r['ro'] =& $elt['db'];
1530             }
1531         }
1532         if ($r['ro'] == null && $r['rw'] != null) {
1533             $r['ro'] =& $r['rw'];
1534         }
1535
1536         return $r;
1537     }
1538     // }}}
1539
1540     // {{{ _getTableDef
1541     /**
1542      *  テーブル定義を取得する
1543      *
1544      *  (クラス名→テーブル名のルールを変えたい場合は
1545      *  このメソッドをオーバーライドします)
1546      *
1547      *  @access protected
1548      *  @return array   テーブル定義
1549      */
1550     function _getTableDef()
1551     {
1552         $class_name = get_class($this);
1553         if (preg_match('/(\w+)_(.*)/', $class_name, $match) == 0) {
1554             return null;
1555         }
1556         $table = $match[2];
1557
1558         // PHP 4は常に小文字を返す...のでPHP 5専用
1559         $table = preg_replace('/^([A-Z])/e', "strtolower('\$1')", $table);
1560         $table = preg_replace('/([A-Z])/e', "'_' . strtolower('\$1')", $table);
1561
1562         //   JOIN には対応していないので、記述可能なテーブルは
1563         //   常に一つ、かつ primary は trueになる
1564         return array($table => array('primary' => true));
1565     }
1566     // }}}
1567
1568     // {{{ _getPropDef
1569     /**
1570      *  プロパティ定義を取得します。キャッシュされている場合は、
1571      *  そこから取得します。
1572      *
1573      *  @access protected
1574      *  @return array   プロパティ定義
1575      */
1576     function _getPropDef()
1577     {
1578         if (is_null($this->table_def)) {
1579             return null;
1580         }
1581         foreach ($this->table_def as $table_name => $table_attr) {
1582             // use 1st one
1583             break;
1584         }
1585
1586         $cache_manager =& Ethna_CacheManager::getInstance('localfile');
1587         $cache_manager->setNamespace('ethna_app_object');
1588         $cache_key = md5($this->my_db_ro->getDSN() . '-' . $table_name);
1589
1590         if ($cache_manager->isCached($cache_key, $this->prop_def_cache_lifetime)) {
1591             $prop_def = $cache_manager->get($cache_key,
1592                                             $this->prop_def_cache_lifetime);
1593             if (Ethna::isError($prop_def) == false) {
1594                 return $prop_def;
1595             }
1596         }
1597
1598         $r = $this->my_db_ro->getMetaData($table_name);
1599         if(Ethna::isError($r)){
1600             return null;
1601         }
1602
1603         $prop_def = array();
1604         foreach ($r as $i => $field_def) {
1605             $primary  = in_array('primary_key', $field_def['flags']);
1606             $seq      = in_array('sequence',    $field_def['flags']);
1607             $required = in_array('not_null',    $field_def['flags']);
1608             $key      = in_array('primary_key', $field_def['flags'])
1609                         || in_array('multiple_key', $field_def['flags'])
1610                         || in_array('unique_key', $field_def['flags']);
1611
1612             switch ($field_def['type']) {
1613             case 'int':
1614                 $type = VAR_TYPE_INT;
1615                 break;
1616             case 'boolean':
1617                 $type = VAR_TYPE_BOOLEAN;
1618                 break;
1619             case 'datetime':
1620                 $type = VAR_TYPE_DATETIME;
1621                 break;
1622             default:
1623                 $type = VAR_TYPE_STRING;
1624                 break;
1625             }
1626
1627             $prop_def[$field_def['name']] = array(
1628                 'primary'   => $primary,
1629                 'seq'       => $seq,
1630                 'key'       => $key,
1631                 'type'      => $type,
1632                 'required'  => $required,
1633                 'length'    => $field_def['len'],
1634                 'form_name' => $this->_fieldNameToFormName($field_def),
1635                 'table'     => $table_name,
1636             );
1637         }
1638         
1639         $cache_manager->set($cache_key, $prop_def);
1640
1641         return $prop_def;
1642     }
1643     // }}}
1644
1645     // {{{ _fieldNameToFormName
1646     /**
1647      *  データベースフィールド名に対応するフォーム名を取得する
1648      *
1649      *  @access protected
1650      */
1651     function _fieldNameToFormName($field_def)
1652     {
1653         return $field_def['name'];
1654     }
1655     // }}}
1656 }
1657 // }}}
1658 ?>