summaryrefslogtreecommitdiff
path: root/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
blob: 50558a2b04dff43645e3ab0d87a7f768fd35d662 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
<?php
/**
 * TActiveRecordHasManyAssociation class file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2007 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Data.ActiveRecord.Relations
 */

/**
 * Loads base active record relations class.
 */
Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');

/**
 * Implements the M-N (many to many) relationship via association table.
 * Consider the <b>entity</b> relationship between Articles and Categories
 * via the association table <tt>Article_Category</tt>.
 * <code>
 * +---------+            +------------------+            +----------+
 * | Article | * -----> * | Article_Category | * <----- * | Category |
 * +---------+            +------------------+            +----------+
 * </code>
 * Where one article may have 0 or more categories and each category may have 0
 * or more articles. We may model Article-Category <b>object</b> relationship
 * as active record as follows.
 * <code>
 * class ArticleRecord
 * {
 *     const TABLE='Article';
 *     public $article_id;
 *
 *     public $Categories=array(); //foreign object collection.
 *
 *     protected static $RELATIONS = array
 *     (
 *         'Categories' => array(self::HAS_MANY, 'CategoryRecord', 'Article_Category')
 *     );
 *
 *     public static function finder($className=__CLASS__)
 *     {
 *         return parent::finder($className);
 *     }
 * }
 * class CategoryRecord
 * {
 *     const TABLE='Category';
 *     public $category_id;
 *
 *     public $Articles=array();
 *
 *     protected static $RELATIONS = array
 *     (
 *         'Articles' => array(self::HAS_MANY, 'ArticleRecord', 'Article_Category')
 *     );
 *
 *     public static function finder($className=__CLASS__)
 *     {
 *         return parent::finder($className);
 *     }
 * }
 * </code>
 *
 * The static <tt>$RELATIONS</tt> property of ArticleRecord defines that the
 * property <tt>$Categories</tt> has many <tt>CategoryRecord</tt>s. Similar, the
 * static <tt>$RELATIONS</tt> property of CategoryRecord defines many ArticleRecords.
 *
 * The articles with categories list may be fetched as follows.
 * <code>
 * $articles = TeamRecord::finder()->withCategories()->findAll();
 * </code>
 * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
 * name, in this case, <tt>Categories</tt>) fetchs the corresponding CategoryRecords using
 * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
 * arguments as other finder methods of TActiveRecord.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.ActiveRecord.Relations
 * @since 3.1
 */
class TActiveRecordHasManyAssociation extends TActiveRecordRelation
{
	private $_association;
	private $_sourceTable;
	private $_foreignTable;

	/**
	* Get the foreign key index values from the results and make calls to the
	* database to find the corresponding foreign objects using association table.
	* @param array original results.
	*/
	protected function collectForeignObjects(&$results)
	{
		$association = $this->getAssociationTable();
		$sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord());

		$properties = array_values($sourceKeys);

		$indexValues = $this->getIndexValues($properties, $results);

		$fkObject = $this->getContext()->getForeignRecordFinder();
		$foreignKeys = $this->findForeignKeys($association, $fkObject);

		$this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys);
	}

	/**
	 * @return TDbTableInfo association table information.
	 */
	protected function getAssociationTable()
	{
		if($this->_association===null)
		{
			$gateway = $this->getSourceRecord()->getRecordGateway();
			$conn = $this->getSourceRecord()->getDbConnection();
			$table = $this->getContext()->getAssociationTable();
			$this->_association = $gateway->getTableInfo($conn, $table);
		}
		return $this->_association;
	}

	/**
	 * @return TDbTableInfo source table information.
	 */
	protected function getSourceTable()
	{
		if($this->_sourceTable===null)
		{
			$gateway = $this->getSourceRecord()->getRecordGateway();
			$this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord());
		}
		return $this->_sourceTable;
	}

	/**
	 * @return TDbTableInfo foreign table information.
	 */
	protected function getForeignTable()
	{
		if($this->_foreignTable===null)
		{
			$gateway = $this->getSourceRecord()->getRecordGateway();
			$fkObject = $this->getContext()->getForeignRecordFinder();
			$this->_foreignTable = $gateway->getRecordTableInfo($fkObject);
		}
		return $this->_foreignTable;
	}

	/**
	 * @return TDbCommandBuilder
	 */
	protected function getCommandBuilder()
	{
		return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord());
	}

	/**
	 * Fetches the foreign objects using TActiveRecord::findAllByIndex()
	 * @param array field names
	 * @param array foreign key index values.
	 */
	protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys)
	{
		$criteria = $this->getContext()->getCriteria();
		$finder = $this->getContext()->getForeignRecordFinder();
		$registry = $finder->getRecordManager()->getObjectStateRegistry();
		$type = get_class($finder);
		$command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys);
		$srcProps = array_keys($sourceKeys);
		$collections=array();
		foreach($command->query() as $row)
		{
			$hash = $this->getObjectHash($row, $srcProps);
			foreach($srcProps as $column)
				unset($row[$column]);
			$obj = new $type($row);
			$collections[$hash][] = $obj;
			$registry->registerClean($obj);
		}

		$this->setResultCollection($results, $collections, array_values($sourceKeys));
	}

	/**
	 * @param TSqlCriteria
	 * @param TTableInfo association table info
	 * @param array field names
	 * @param array field values
	 */
	public function createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys)
	{
		$innerJoin = $this->getAssociationJoin($foreignKeys,$indexValues,$sourceKeys);
		$fkTable = $this->getForeignTable()->getTableFullName();
		$srcColumns = $this->getSourceColumns($sourceKeys);
		if(($where=$criteria->getCondition())===null)
			$where='1=1';
		$sql = "SELECT {$fkTable}.*, {$srcColumns} FROM {$fkTable} {$innerJoin} WHERE {$where}";

		$parameters = $criteria->getParameters()->toArray();
		$ordering = $criteria->getOrdersBy();
		$limit = $criteria->getLimit();
		$offset = $criteria->getOffset();

		$builder = $this->getCommandBuilder()->getBuilder();
		$command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset);
		$this->getCommandBuilder()->onCreateCommand($command, $criteria);
		return $command;
	}

	/**
	 * @param array source table column names.
	 * @return string comma separated source column names.
	 */
	protected function getSourceColumns($sourceKeys)
	{
		$columns=array();
		$table = $this->getAssociationTable();
		$tableName = $table->getTableFullName();
		foreach($sourceKeys as $name=>$fkName)
			$columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName();
		return implode(', ', $columns);
	}

	/**
	 * SQL inner join for M-N relationship via association table.
	 * @param array foreign table column key names.
	 * @param array source table index values.
	 * @param array source table column names.
	 * @return string inner join condition for M-N relationship via association table.
	 */
	protected function getAssociationJoin($foreignKeys,$indexValues,$sourceKeys)
	{
		$refInfo= $this->getAssociationTable();
		$fkInfo = $this->getForeignTable();

		$refTable = $refInfo->getTableFullName();
		$fkTable = $fkInfo->getTableFullName();

		$joins = array();
		foreach($foreignKeys as $ref=>$fk)
		{
			$refField = $refInfo->getColumn($ref)->getColumnName();
			$fkField = $fkInfo->getColumn($fk)->getColumnName();
			$joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}";
		}
		$joinCondition = implode(' AND ', $joins);
		$index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo,array_keys($sourceKeys), $indexValues);
		return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}";
	}
}
?>