04-Relationships.txt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. = Basic Relationships =
  2. [[PageOutline]]
  3. The definition of foreign keys in your schema allows Propel to add smart methods to the generated model and query objects. In practice, these generated methods mean that you will never actually have to deal with primary and foreign keys yourself. It makes the task of dealing with relations extremely straightforward.
  4. == Inserting A Related Row ==
  5. Propel creates setters for related objects that simplify the foreign key handling. You don't actually have to define a foreign key value. Instead, just set a related object, as follows:
  6. {{{
  7. #!php
  8. <?php
  9. $author = new Author();
  10. $author->setFirstName("Leo");
  11. $author->setLastName("Tolstoy");
  12. $author->save();
  13. $book = new Book();
  14. $book->setTitle("War & Peace");
  15. // associate the $author object with the current $book
  16. $book->setAuthor($author);
  17. $book->save();
  18. }}}
  19. Propel generates the `setAuthor()` method based on the `phpName` attribute of the `<foreign-key>` element in the schema. When the attribute is not set, Propel uses the `phpName` of the related table instead.
  20. Internally, the call to `Book::setAuthor($author)` translates into `Book::setAuthorId($author->getId())`. But you don't actually have to save a Propel object before associating it to another. In fact, Propel automatically "cascades" INSERT statements when a new object has other related objects added to it.
  21. For one-to-many relationships - meaning, from the other side of a many-to-one relationship - the process is a little different. In the previous example, one `Book` has one `Author`, but one `Author` has many `Books`. From the `Author` point of view, a one-to-many relationships relates it to `Book`. So Propel doesn't generate an `Author::setBook()`, but rather an `Author::addBook()`:
  22. {{{
  23. #!php
  24. <?php
  25. $book = new Book();
  26. $book->setTitle("War & Peace");
  27. // associate the $author object with the current $book
  28. $book->save();
  29. $author = new Author();
  30. $author->setFirstName("Leo");
  31. $author->setLastName("Tolstoy");
  32. $author->addBook($book);
  33. $author->save();
  34. }}}
  35. The result is the same in the database - the `author_id` column of the `book` row is correctly set to the `id` of the `author` row.
  36. == Save Cascade ==
  37. As a matter of fact, you don't need to `save()` an object before relating it. Propel knows which objects are related to each other, and is capable of saving all the unsaved objects if they are related to each other.
  38. The following example shows how to create new `Author` and `Publisher` objects, which are then added to a new `Book` object; all 3 objects are saved when the `Book::save()` method is eventually invoked.
  39. {{{
  40. #!php
  41. <?php
  42. /* initialize Propel, etc. */
  43. $author = new Author();
  44. $author->setFirstName("Leo");
  45. $author->setLastName("Tolstoy");
  46. // no need to save the author yet
  47. $publisher = new Publisher();
  48. $publisher->setName("Viking Press");
  49. // no need to the publisher yet
  50. $book = new Book();
  51. $book->setTitle("War & Peace");
  52. $book->setIsbn("0140444173");
  53. $book->setPublisher($publisher);
  54. $book->setAuthor($author);
  55. $book->save(); // saves all 3 objects!
  56. }}}
  57. In practice, Propel '''cascades''' the `save()` action to the related objects.
  58. == Reading Related Object Properties ==
  59. Just like the related object setters, Propel generates a getter for every relation:
  60. {{{
  61. #!php
  62. <?php
  63. $book = BookQuery()::create()->findPk(1);
  64. $author = $book->getAuthor();
  65. echo $author->getFirstName(); // 'Leo'
  66. }}}
  67. Since a relationship can also be seen from the other end, Propel allows the foreign table to retrieve the related objects as well:
  68. {{{
  69. #!php
  70. <?php
  71. $author = AuthorQuery::create()->findPk(1);
  72. $books = $author->getBooks();
  73. foreach ($books as $book) {
  74. echo $book->getTitle();
  75. }
  76. }}}
  77. Notice that Propel generated a `getBooks()` method returning an array of `Book` objects, rather than a `getBook()` method. This is because the definition of a foreign key defines a many-to-one relationship, seen from the other end as a one-to-many relationship.
  78. '''Tip''': Propel also generates a `countBooks()` methods to get the number of related objects without hydrating all the `Book` objects. for performance reasons, you should prefer this method to `count($author->getBooks())`.
  79. Getters for one-to-many relationship accept an optional query object. This allows you to hydrate related objects, or retrieve only a subset of the related objects, or to reorder the list of results:
  80. {{{
  81. #!php
  82. <?php
  83. $query = BookQuery::create()
  84. ->orderByTitle()
  85. ->joinWith('Book.Publisher');
  86. $books = $author->getBooks($query);
  87. }}}
  88. == Using Relationships In A Query ==
  89. === Finding Records Related To Another One ===
  90. If you need to find objects related to a model object that you already have, you can take advantage of the generated `filterByXXX()` methods in the query objects, where `XXX` is a relation name:
  91. {{{
  92. #!php
  93. <?php
  94. $author = AuthorQuery::create()->findPk(1);
  95. $books = BookQuery::create()
  96. ->filterByAuthor($author)
  97. ->orderByTitle()
  98. ->find();
  99. }}}
  100. You don't need to specify that the `author_id` column of the `Book` object should match the `id` column of the `Author` object. Since you already defined the foreign key mapping in your schema, Propel knows enough to figure it out.
  101. === Embedding Queries ===
  102. In SQL queries, relationships often translate to a JOIN statement. Propel abstracts this relational logic in the query objects, by allowing you to ''embed'' a related query into another.
  103. In practice, Propel generates one `useXXXQuery()` method for every relation in the Query objects. So the `BookQuery` class offers a `useAuthorQuery()` and a `usePublisherQuery()` method. These methods return a new Query instance of the related query class, that you can eventually merge into the main query by calling `endUse()`.
  104. To illustrate this, let's see how to write the following SQL query with the Propel Query API:
  105. {{{
  106. #!sql
  107. SELECT book.*
  108. FROM book INNER JOIN author ON book.AUTHOR_ID = author.ID
  109. WHERE book.ISBN = '0140444173' AND author.FIRST_NAME = 'Leo'
  110. ORDER BY book.TITLE ASC
  111. LIMIT 10;
  112. }}}
  113. That would simply give:
  114. {{{
  115. #!php
  116. <?php
  117. $books = BookQuery::create()
  118. ->filterByISBN('0140444173')
  119. ->useAuthorQuery() // returns a new AuthorQuery instance
  120. ->filterByFirstName('Leo') // this is an AuthorQuery method
  121. ->endUse() // merges the Authorquery in the main Bookquery and returns the BookQuery
  122. ->orderByTitle()
  123. ->limit(10)
  124. ->find();
  125. }}}
  126. Propel knows the columns to use in the `ON` clause from the definition of foreign keys in the schema. The ability to use methods of a related Query object allows you to keep your model logic where it belongs.
  127. Of course, you can embed several queries to issue a query of any complexity level:
  128. {{{
  129. #!php
  130. <?php
  131. // Find all authors of books published by Viking Press
  132. $authors = AuthorQuery::create()
  133. ->useBookQuery()
  134. ->usePublisherQuery()
  135. ->filterByName('Viking Press')
  136. ->endUse()
  137. ->endUse()
  138. ->find();
  139. }}}
  140. You can see how the indentation of the method calls provide a clear explanation of the embedding logic. That's why it is a good practice to format your Propel queries with a single method call per line, and to add indentation every time a `useXXXQuery()` method is used.
  141. == Many-to-Many Relationships ==
  142. Databases typically use a cross-reference table, or junction table, to materialize the relationship. For instance, if the `user` and `group` tables are related by a many-to-many relationship, this happens through the rows of a `user_group` table. To inform Propel about the many-to-many relationship, set the `isCrossRef` attribute of the cross reference table to true:
  143. {{{
  144. #!xml
  145. <table name="user">
  146. <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  147. <column name="name" type="VARCHAR" size="32"/>
  148. </table>
  149. <table name="group">
  150. <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  151. <column name="name" type="VARCHAR" size="32"/>
  152. </table>
  153. <table name="user_group" isCrossRef="true">
  154. <column name="user_id" type="INTEGER" primaryKey="true"/>
  155. <column name="group_id" type="INTEGER" primaryKey="true"/>
  156. <foreign-key foreignTable="user">
  157. <reference local="user_id" foreign="id"/>
  158. </foreign-key>
  159. <foreign-key foreignTable="group">
  160. <reference local="group_id" foreign="id"/>
  161. </foreign-key>
  162. </table>
  163. }}}
  164. Once you rebuild your model, the relationship is seen as a one-to-many relationship from both the `User` and the `Group` models. That means that you can deal with adding and reading relationships the same way as you usually do:
  165. {{{
  166. #!php
  167. <?php
  168. $user = new User();
  169. $user->setName('John Doe');
  170. $group = new Group();
  171. $group->setName('Anonymous');
  172. // relate $user and $group
  173. $user->addGroup($group);
  174. // save the $user object, the $group object, and a new instance of the UserGroup class
  175. $user->save();
  176. }}}
  177. The same happens for reading related objects ; Both ends see the relationship as a one-to-many relationship:
  178. {{{
  179. #!php
  180. <?php
  181. $users = $group->getUsers();
  182. $nbUsers = $group->countUsers();
  183. $groups = $user->getGroups();
  184. $nbGroups = $user->countGroups();
  185. }}}
  186. Just like regular related object getters, these generated methods accept an optional query object, to further filter the results.
  187. To facilitate queries, Propel also adds new methods to the `UserQuery` and `GroupQuery` classes:
  188. {{{
  189. #!php
  190. <?php
  191. $users = UserQuery::create()
  192. ->filterByGroup($group)
  193. ->find();
  194. $groups = GroupQuery::create()
  195. ->filterByUser($user)
  196. ->find();
  197. }}}
  198. == One-to-One Relationships ==
  199. Propel supports the special case of one-to-one relationships. These relationships are defined when the primary key is also a foreign key. For example :
  200. {{{
  201. #!xml
  202. <table name="bookstore_employee" description="Employees of a bookstore">
  203. <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
  204. <column name="name" type="VARCHAR" size="32"/>
  205. </table>
  206. <table name="bookstore_employee_account" description="Bookstore employees' login credentials">
  207. <column name="employee_id" type="INTEGER" primaryKey="true"/>
  208. <column name="login" type="VARCHAR" size="32"/>
  209. <column name="password" type="VARCHAR" size="100"/>
  210. <foreign-key foreignTable="bookstore_employee">
  211. <reference local="employee_id" foreign="id"/>
  212. </foreign-key>
  213. </table>
  214. }}}
  215. Because the primary key of the `bookstore_employee_account` is also a foreign key to the `bookstore_employee` table, Propel interprets this as a one-to-one relationship and will generate singular methods for both sides of the relationship (`BookstoreEmployee::getBookstoreEmployeeAccount()`, and `BookstoreEmployeeAccount::getBookstoreEmployee()`).
  216. == On-Update and On-Delete Triggers =
  217. Propel also supports the ''ON UPDATE'' and ''ON DELETE'' aspect of foreign keys. These properties can be specified in the `<foreign-key>` tag using the `onUpdate` and `onDelete` attributes. Propel supports values of `CASCADE`, `SETNULL`, and `RESTRICT` for these attributes. For databases that have native foreign key support, these trigger events will be specified at the datbase level when the foreign keys are created. For databases that do not support foreign keys, this functionality will be emulated by Propel.
  218. {{{
  219. #!xml
  220. <table name="review">
  221. <column name="review_id" type="INTEGER" primaryKey="true" required="true"/>
  222. <column name="reviewer" type="VARCHAR" size="50" required="true"/>
  223. <column name="book_id" required="true" type="INTEGER"/>
  224. <foreign-key foreignTable="book" onDelete="CASCADE">
  225. <reference local="book_id" foreign="id"/>
  226. </foreign-key>
  227. </table>
  228. }}}
  229. In the example above, the `review` rows will be automatically removed if the related `book` row is deleted.
  230. == Minimizing Queries ==
  231. Even if you use a foreign query, Propel will issue new queries when you fetch related objects:
  232. {{{
  233. #!php
  234. <?php
  235. $book = BookQuery::create()
  236. ->useAuthorQuery()
  237. ->filterByFirstName('Leo')
  238. ->endUse()
  239. ->findOne();
  240. $author = $book->getAuthor(); // Needs another database query
  241. }}}
  242. Propel allows you to retrieve the main object together with related objects in a single query. You just the `with()` method to specify which objects the main object should be hydrated with.
  243. {{{
  244. #!php
  245. <?php
  246. $book = BookQuery::create()
  247. ->useAuthorQuery()
  248. ->filterByFirstName('Leo')
  249. ->endUse()
  250. ->with('Author')
  251. ->findOne();
  252. $author = $book->getAuthor(); // Same result, with no supplementary query
  253. }}}
  254. Since the call to `with()` adds the columns of the related object to the SELECT part of the query, and uses these columns to populate the related object, that means that a query using `with()` is slower and consumes more memory. So use it only when you actually need the related objects afterwards.
  255. If you don't want to add a filter on a related object but still need to hydrate it, calling `useXXXQuery()`, `endUse()`, and then `with()` can be a little cumbersome. For this case, Propel provides a proxy method called `joinWith()`. It expects a string made of the initial query name and the foreign query name. For instance:
  256. {{{
  257. #!php
  258. <?php
  259. $book = BookQuery::create()
  260. ->joinWith('Book.Author')
  261. ->findOne();
  262. $author = $book->getAuthor(); // Same result, with no supplementary query
  263. }}}
  264. `with()` and `joinWith()` are not limited to immediate relationships. As a matter of fact, just like you can nest `use()` calls, you can call `with()` several times to populate a chain of objects:
  265. {{{
  266. #!php
  267. <?php
  268. $review = ReviewQuery::create()
  269. ->joinWith('Review.Book')
  270. ->joinWith('Book.Author')
  271. ->joinWith('Book.Publisher')
  272. ->findOne();
  273. $book = $review->getBook() // No additional query needed
  274. $author = $book->getAuthor(); // No additional query needed
  275. $publisher = $book->getPublisher(); // No additional query needed
  276. }}}
  277. So `with()` is very useful to minimize the number of database queries. As soon as you see that the number of queries necessary to perform an action is proportional to the number of results, adding a `with()` call is the trick to get down to a more reasonnable query count.
  278. '''Tip''': `with()` also works for left joins on one-to-many relationships, but you musn't use a `limit()` in the query in this case. This is because Propel has no way to determine the actual number of rows of the main object in such a case.
  279. {{{
  280. #!php
  281. <?php
  282. // this works
  283. $authors = AuthorQuery::create()
  284. ->leftJoinWith('Author.Book')
  285. ->find();
  286. // this does not work
  287. $authors = AuthorQuery::create()
  288. ->leftJoinWith('Author.Book')
  289. ->limit(5)
  290. ->find();
  291. }}}
  292. However, it is quite easy to achieve hydration of related objects with only one additional query:
  293. {{{
  294. #!php
  295. <?php
  296. // $authors is a PropelObjectCollection
  297. $authors = AuthorQuery::create()->find();
  298. $authors->populateRelation('Book');
  299. // now you can iterate over each author's book without further queries
  300. foreach ($authors as $author) {
  301. foreach ($authors->getBooks() as $book) { // no database query, the author already has a Books collection
  302. // do stuff with $book and $author
  303. }
  304. }
  305. }}}