++ Dealing with relations
++ Many-to-Many relations
+++ Creating a new link
Lets say we have two classes User and Group which are linked trhough a GroupUser association class. When working with transient (new) records the fastest way for adding a User and couple of Groups for it is:
$user = new User();
$user->name = 'Some User';
$user->Group[0]->name = 'Some Group';
$user->Group[1]->name = 'Some Other Group';
$user->save();
However in real world scenarious you often already have existing groups, where you want to add a given user. The most efficient way of doing this is:
$gu = new GroupUser();
$gu->user_id = $userId;
$gu->group_id = $groupId;
$gu->save();
+++ Deleting a link
The right way to delete links between many-to-many associated records is by using the DQL DELETE statement. Convenient and recommended way of using DQL DELETE is trhough the Query API.
$deleted = Doctrine_Query::create()
->delete()
->from('GroupUser')
->addWhere('user_id = 5')
->whereIn('group_id', $groupIds);
->execute();
// print out the deleted links
print $deleted;
Another way to {{unlink}} the relationships between related objects is through the {{Doctrine_Record::unlink}} method. However, you should avoid using this method unless you already have the parent model, since it involves querying the database first.
$user = $conn->getTable('User')->find(5);
$user->unlink('Group', array(0, 1));
$user->save();
// you can also unlink ALL relationships to Group
$user->unlink('Group');
While the obvious and convinient way of deleting a link between User and Group would be the following, you still should *NOT* do this:
$user = $conn->getTable('User')->find(5);
$user->GroupUser
->remove(0)
->remove(1);
$user->save();
This is due to a fact that $user->GroupUser loads all group links for given user. This can time-consuming task if user belongs to many groups. Even if the user belongs to few groups this will still execute an unnecessary SELECT statement.
++ Fetching objects
Normally when you fetch data from database the following phases are executed:
1. Sending the query to database
2. Retrieve the returned data from the database
In terms of object fetching we call these two phases the 'fetching' phase. Doctrine also has another phase called hydration phase. The hydration phase takes place whenever you are fecthing structured arrays / objects. Unless explicitly specified everything in Doctrine gets hydrated.
Lets consider we have users and phonenumbers with their relation being one-to-many. Now consider the following plain sql query:
$dbh->fetchAll('SELECT u.id, u.name, p.phonenumber FROM user u LEFT JOIN phonenumber p ON u.id = p.user_id');
If you are familiar with these kind of one-to-many joins it may be familiar to you how the basic result set is constructed. Whenever the user has more than one phonenumbers there will be duplicated data in the result set. The result set might look something like:
index | u.id | u.name | p.phonenumber |
0 | 1 | Jack Daniels | 123 123 |
1 | 1 | Jack Daniels | 456 456 |
2 | 2 | John Beer | 111 111 |
3 | 3 | John Smith | 222 222 |
4 | 3 | John Smith | 333 333 |
5 | 3 | John Smith | 444 444 |
Here Jack Daniels has 2 phonenumbers, John Beer has one whereas John Smith has 3 phonenumbers. You may notice how clumsy this result set is. Its hard to iterate over it as you would need some duplicate data checkings here and there.
Doctrine hydration removes all duplicated data. It also performs many other things such as:
# Custom indexing of result set elements
# Value casting and preparation
# Value assignment listening
# Makes multi-dimensional array out of the two-dimensional result set array, the number of dimensions is equal to the number of nested joins
Now consider the DQL equivalent of the SQL query we used:
$array = $conn->query('SELECT u.id, u.name, p.phonenumber FROM User u LEFT JOIN u.Phonenumber p',
array(), Doctrine::HYDRATE_ARRAY);
The structure of this hydrated array would look like:
array(0 => array('id' => 1,
'name' => 'Jack Daniels',
'Phonenumber' =>
array(0 => array('phonenumber' => '123 123'),
1 => array('phonenumber' => '456 456'))),
1 => array('id' => 2,
'name' => 'John Beer',
'Phonenumber' =>
array(0 => array('phonenumber' => '111 111'))),
2 => array('id' => 3,
'name' => 'John Smith',
'Phonenumber' =>
array(0 => array('phonenumber' => '111 111')),
2 => array('phonenumber' => '222 222'),
3 => array('phonenumber' => '333 333'))));
This structure also applies to the hydration of objects(records) which is the default hydration mode of Doctrine. The only differences are that the individual elements are represented as Doctrine_Record objects and the arrays converted into Doctrine_Collection objects. Whether dealing with arrays or objects you can:
# Iterate over the results using //foreach//
# Access individual elements using array access brackets
# Get the number of elements using //count()// function
# Check if given element exists using //isset()//
# Unset given element using //unset()//
You should always use array hydration when you only need to data for access-only purposes, whereas you should use the record hydration when you need to change the fetched data.
The constant O(n) performance of the hydration algorithm is ensured by a smart identifier caching solution.
+++ Field lazy-loading
Whenever you fetch an object that has not all of its fields loaded from database then the state of this object is called proxy. Proxy objects can load the unloaded fields lazily.
Lets say we have a User class with the following definition:
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 20);
$this->hasColumn('password', 'string', 16);
$this->hasColumn('description', 'string');
}
}
In the following example we fetch all the Users with the fields name and password loaded directly. Then we lazy-load a huge field called description for one user.
$users = Doctrine_Query::create()->select('u.name, u.password')->from('User u')->execute();
// the following lazy-loads the description fields and executes one additional database query
$users[0]->description;
Doctrine does the proxy evaluation based on loaded field count. It does not evaluate which fields are loaded on field-by-field basis. The reason for this is simple: performance. Field lazy-loading is very rarely needed in PHP world, hence introducing some kind of variable to check which fields are loaded would introduce unnecessary overhead to basic fetching.