533 lines
17 KiB
Plaintext
533 lines
17 KiB
Plaintext
|
++ Introduction
|
||
|
|
||
|
In Doctrine all record relations are being set with {{hasMany}}, {{hasOne}} methods. Doctrine supports almost any kind of database relation from simple one-to-one foreign key relations to join table self-referencing relations.
|
||
|
|
||
|
|
||
|
++ Relation aliases
|
||
|
|
||
|
Doctrine supports relation aliases through {{as}} keyword.
|
||
|
|
||
|
<code type="php">
|
||
|
class Forum_Board extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 100);
|
||
|
$this->hasColumn('description', 'string', 5000);
|
||
|
}
|
||
|
public function setUp()
|
||
|
{
|
||
|
// notice the 'as' keyword here
|
||
|
$this->hasMany('Forum_Thread as Threads', array('local' => 'id',
|
||
|
'foreign' => 'board_id');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Forum_Thread extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('board_id', 'integer', 10);
|
||
|
$this->hasColumn('updated', 'integer', 10);
|
||
|
$this->hasColumn('closed', 'integer', 1);
|
||
|
}
|
||
|
public function setUp()
|
||
|
{
|
||
|
// notice the 'as' keyword here
|
||
|
$this->hasOne('Forum_Board as Board', array('local' => 'board_id',
|
||
|
'foreign' => 'id');
|
||
|
}
|
||
|
}
|
||
|
$board = new Board();
|
||
|
$board->Threads[0]->updated = time();
|
||
|
</code>
|
||
|
|
||
|
|
||
|
++ Foreign key associations
|
||
|
+++ One-To-One
|
||
|
|
||
|
Binding One-To-One foreign key associations is done with {{Doctrine_Record::ownsOne()}} and {{Doctrine_Record::hasOne()}} methods. In the following example user owns one email and has one address. So the relationship between user and email is one-to-one composite. The relationship between user and address is one-to-one aggregate.
|
||
|
|
||
|
The {{Email}} component here is mapped to {{User}} component's column {{email_id}} hence their relation is called LOCALKEY relation. On the other hand the {{Address}} component is mapped to {{User}} by it's {{user_id}} column hence the relation between {{User}} and {{Address}} is called FOREIGNKEY relation.
|
||
|
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasOne('Address', array('local' => 'id', 'foreign' => 'user_id'));
|
||
|
$this->hasOne('Email', array('local' => 'email_id', 'foreign' => 'id'));
|
||
|
$this->hasMany('Phonenumber', array('local' => 'id', 'foreign' => 'user_id'));
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string',50);
|
||
|
$this->hasColumn('loginname', 'string',20);
|
||
|
$this->hasColumn('password', 'string',16);
|
||
|
|
||
|
// foreign key column for email id
|
||
|
$this->hasColumn('email_id', 'integer');
|
||
|
}
|
||
|
}
|
||
|
class Email extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('address', 'string', 150);
|
||
|
}
|
||
|
}
|
||
|
class Address extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('street', 'string', 50);
|
||
|
$this->hasColumn('user_id', 'integer');
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
|
||
|
+++ One-to-Many, Many-to-One
|
||
|
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasMany('Phonenumber', array('local' => 'id', 'foreign' => 'user_id'));
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 50);
|
||
|
$this->hasColumn('loginname', 'string', 20);
|
||
|
$this->hasColumn('password', 'string', 16);
|
||
|
}
|
||
|
}
|
||
|
class Phonenumber extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('phonenumber', 'string', 50);
|
||
|
$this->hasColumn('user_id', 'integer');
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
|
||
|
+++ Tree structure
|
||
|
|
||
|
<code type="php">
|
||
|
class Task extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasOne('Task as Parent', array('local' => 'parent_id', 'foreign' => 'id'));
|
||
|
$this->hasMany('Task as Subtask', array('local' => 'id', 'foreign' => 'parent_id'));
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 100);
|
||
|
$this->hasColumn('parent_id', 'integer');
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
++ Join table associations
|
||
|
|
||
|
+++ Many-to-Many
|
||
|
|
||
|
If you are coming from relational database background it may be familiar to you how many-to-many associations are handled: an additional association table is needed.
|
||
|
|
||
|
In many-to-many relations the relation between the two components is always an aggregate relation and the association table is owned by both ends. For example in the case of users and groups when user is being deleted the groups it belongs to are not being deleted and the associations between this user and the groups it belongs to are being deleted.
|
||
|
|
||
|
Sometimes you may not want that association table rows are being deleted when user / group is being deleted. You can override this behoviour by setting the relations to association component (in this case {{Groupuser}}) explicitly.
|
||
|
|
||
|
In the following example we have Groups and Users of which relation is defined as many-to-many. In this case we also need to define an additional class called {{Groupuser}}.
|
||
|
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasMany('Group', array('local' => 'user_id',
|
||
|
'foreign' => 'group_id',
|
||
|
// the following line is needed in many-to-many relations!
|
||
|
'refClass' => 'GroupUser'));
|
||
|
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Group extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp() {
|
||
|
$this->hasMany('User', array('local' => 'group_id',
|
||
|
'foreign' => 'user_id',
|
||
|
// the following line is needed in many-to-many relations!
|
||
|
'refClass' => 'GroupUser'));
|
||
|
}
|
||
|
public function setTableDefinition() {
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class GroupUser extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('user_id', 'integer', null, array('primary' => true));
|
||
|
$this->hasColumn('group_id', 'integer', null, array('primary' => true));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
$user = new User();
|
||
|
|
||
|
// add two groups
|
||
|
$user->Group[0]->name = 'First Group';
|
||
|
|
||
|
$user->Group[1]->name = 'Second Group';
|
||
|
|
||
|
// save changes into database
|
||
|
$user->save();
|
||
|
|
||
|
// deleting the associations between user and groups it belongs to
|
||
|
|
||
|
$user->Groupuser->delete();
|
||
|
|
||
|
$groups = new Doctrine_Collection($conn->getTable('Group'));
|
||
|
|
||
|
$groups[0]->name = 'Third Group';
|
||
|
|
||
|
$groups[1]->name = 'Fourth Group';
|
||
|
|
||
|
$user->Group[2] = $groups[0];
|
||
|
// $user will now have 3 groups
|
||
|
|
||
|
$user->Group = $groups;
|
||
|
// $user will now have two groups 'Third Group' and 'Fourth Group'
|
||
|
|
||
|
</code>
|
||
|
|
||
|
|
||
|
+++ Self-referencing (Nest relations)
|
||
|
++++ Non-equal nest relations
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasMany('User as Parents', array('local' => 'parent_id',
|
||
|
'foreign' => 'child_id',
|
||
|
'refClass' => 'UserReference'
|
||
|
);
|
||
|
|
||
|
$this->hasMany('User as Children', array('local' => 'child_id',
|
||
|
'foreign' => 'parent_id',
|
||
|
'refClass' => 'UserReference'
|
||
|
);
|
||
|
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
}
|
||
|
}
|
||
|
class UserReference extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition() {
|
||
|
$this->hasColumn('parent_id', 'integer', null, array('primary' => true));
|
||
|
$this->hasColumn('child_id', 'integer', null, array('primary' => true));
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
++++ Equal nest relations
|
||
|
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasMany('User as Friend', array('local' => 'user1',
|
||
|
'foreign' => 'user2',
|
||
|
'refClass' => 'UserReference'
|
||
|
'equal' => true,
|
||
|
);
|
||
|
}
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
}
|
||
|
}
|
||
|
class UserReference extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition() {
|
||
|
$this->hasColumn('user1', 'integer', null, array('primary' => true));
|
||
|
$this->hasColumn('user2', 'integer', null, array('primary' => true));
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
++ Inheritance
|
||
|
+++ One table, many classes
|
||
|
|
||
|
When it comes to handling inheritance Doctrine is very smart. In the following example we have one database table called {{entity}}. Users and groups are both entities and they share the same database table. The only thing we have to make is 3 records ({{Entity}}, {{Group}} and {{User}}).
|
||
|
|
||
|
Doctrine is smart enough to know that the inheritance type here is one-table-many-classes.
|
||
|
|
||
|
<code type="php">
|
||
|
class Entity extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
$this->hasColumn('username', 'string', 20);
|
||
|
$this->hasColumn('password', 'string', 16);
|
||
|
$this->hasColumn('created', 'integer', 11);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class User extends Entity
|
||
|
{ }
|
||
|
|
||
|
class Group extends Entity
|
||
|
{ }
|
||
|
</code>
|
||
|
|
||
|
|
||
|
+++ One table, one class
|
||
|
|
||
|
One-table-one-class inheritance is the only inheritance type that allows additional fields for inherited classes. As shown in the example above adding additional columns is very easy:
|
||
|
|
||
|
<code type="php">
|
||
|
class TextItem extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('topic', 'string', 100);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Comment extends TextItem
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
parent::setTableDefinition();
|
||
|
|
||
|
$this->hasColumn('content', 'string', 300);
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
|
||
|
In one-table-one-class inheritance you don't necessarily have to define additional columns, but in order to make Doctrine create separate tables for each class you'll have to make iterative setTableDefinition() calls.
|
||
|
|
||
|
In the following example we have three database tables called {{entity}}, {{user}} and {{group}}. Users and groups are both entities. The only thing we have to do is write 3 classes ({{Entity}}, {{Group}} and {{User}}) and make iterative {{setTableDefinition}} method calls.
|
||
|
|
||
|
<code type="php">
|
||
|
class Entity extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
$this->hasColumn('username', 'string', 20);
|
||
|
$this->hasColumn('password', 'string', 16);
|
||
|
$this->hasColumn('created', 'integer', 11);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class User extends Entity
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
// the following method call is needed in
|
||
|
// one-table-one-class inheritance
|
||
|
parent::setTableDefinition();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Group extends Entity
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
// the following method call is needed in
|
||
|
// one-table-one-class inheritance
|
||
|
parent::setTableDefinition();
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
|
||
|
+++ Column aggregation
|
||
|
|
||
|
In the following example we have one database table called {{entity}}. Users and groups are both entities and they share the same database table.
|
||
|
|
||
|
The entity table has a column called {{type}} which tells whether an entity is a group or a user. Then we decide that users are type 1 and groups type 2.
|
||
|
|
||
|
The only thing we have to do is to create 3 records (the same as before) and add
|
||
|
call the {{Doctrine_Table::setSubclasses()}} method from the parent class.
|
||
|
|
||
|
<code type="php">
|
||
|
class Entity extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 30);
|
||
|
$this->hasColumn('username', 'string', 20);
|
||
|
$this->hasColumn('password', 'string', 16);
|
||
|
$this->hasColumn('created', 'integer', 11);
|
||
|
|
||
|
// this column is used for column
|
||
|
// aggregation inheritance
|
||
|
$this->hasColumn('type', 'integer', 11);
|
||
|
$this->setSubclasses(array(
|
||
|
"User" => array("type" => 1),
|
||
|
"Group" => array("type" => 2)
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class User extends Entity {}
|
||
|
class Group extends Entity {}
|
||
|
|
||
|
</code>
|
||
|
|
||
|
This feature also enable us to query the {{Entity}} table and get a {{User}} or
|
||
|
{{Group}} object back if the returned object matches the constraints set in the
|
||
|
parent class. See the code example below for an example of this.
|
||
|
|
||
|
<code type="php">
|
||
|
$user = new User();
|
||
|
$user->name = 'Bjarte S. Karlsen';
|
||
|
$user->username = 'meus';
|
||
|
$user->password = 'rat';
|
||
|
$user->save();
|
||
|
|
||
|
$group = new Group();
|
||
|
$group->name = 'Users';
|
||
|
$group->username = 'users';
|
||
|
$group->password = 'password';
|
||
|
$group->save();
|
||
|
|
||
|
$q = new Doctrine_Query();
|
||
|
$user = $q->from('Entity')->where('id=?')->execute(array($user->id))->getFirst();
|
||
|
assert($user instanceOf User);
|
||
|
|
||
|
$q = new Doctrine_Query();
|
||
|
$group = $q->from('Entity')->where('id=?')->execute(array($group->id))->getFirst();
|
||
|
assert($group instanceOf Group);
|
||
|
</code>
|
||
|
|
||
|
++ Foreign key constraints
|
||
|
+++ Introduction
|
||
|
|
||
|
A foreign key constraint specifies that the values in a column (or a group of columns) must match the values appearing in some row of another table. In other words foreign key constraints maintain the referential integrity between two related tables.
|
||
|
|
||
|
Say you have the product table with the following definition:
|
||
|
|
||
|
<code type="php">
|
||
|
class Product extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('id', 'integer', null, 'primary');
|
||
|
$this->hasColumn('name', 'string');
|
||
|
$this->hasColumn('price', 'decimal', 18);
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
Let's also assume you have a table storing orders of those products. We want to ensure that the order table only contains orders of products that actually exist. So we define a foreign key constraint in the orders table that references the products table:
|
||
|
|
||
|
<code type="php">
|
||
|
class Order extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('order_id', 'integer', null, 'primary');
|
||
|
$this->hasColumn('product_id', 'integer');
|
||
|
$this->hasColumn('quantity', 'integer');
|
||
|
}
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->hasOne('Product', array('local' => 'product_id', 'foreign' => 'id'));
|
||
|
|
||
|
// foreign key columns should *always* have indexes
|
||
|
|
||
|
$this->index('product_id', array('fields' => 'product_id'));
|
||
|
}
|
||
|
}
|
||
|
</code>
|
||
|
|
||
|
When exported the class {{Order}} would execute the following SQL:
|
||
|
|
||
|
<code type="sql">
|
||
|
CREATE TABLE orders (
|
||
|
order_id integer PRIMARY KEY,
|
||
|
product_id integer REFERENCES products (id),
|
||
|
quantity integer,
|
||
|
INDEX product_id_idx (product_id)
|
||
|
)
|
||
|
</code>
|
||
|
|
||
|
Now it is impossible to create orders with product_no entries that do not appear in the products table.
|
||
|
|
||
|
We say that in this situation the orders table is the referencing table and the products table is the referenced table. Similarly, there are referencing and referenced columns.
|
||
|
|
||
|
|
||
|
+++ Integrity actions
|
||
|
|
||
|
//CASCADE//:
|
||
|
Delete or update the row from the parent table and automatically delete or update the matching rows in the child table. Both ON DELETE CASCADE and ON UPDATE CASCADE are supported. Between two tables, you should not define several ON UPDATE CASCADE clauses that act on the same column in the parent table or in the child table.
|
||
|
|
||
|
//SET NULL// :
|
||
|
Delete or update the row from the parent table and set the foreign key column or columns in the child table to NULL. This is valid only if the foreign key columns do not have the NOT NULL qualifier specified. Both ON DELETE SET NULL and ON UPDATE SET NULL clauses are supported.
|
||
|
|
||
|
//NO ACTION// :
|
||
|
In standard SQL, NO ACTION means no action in the sense that an attempt to delete or update a primary key value is not allowed to proceed if there is a related foreign key value in the referenced table.
|
||
|
|
||
|
//RESTRICT// :
|
||
|
Rejects the delete or update operation for the parent table. NO ACTION and RESTRICT are the same as omitting the ON DELETE or ON UPDATE clause.
|
||
|
|
||
|
//SET DEFAULT// :
|
||
|
|
||
|
In the following example we define two classes, User and Phonenumber with their relation being one-to-many. We also add a foreign key constraint with onDelete cascade action. This means that everytime a users is being deleted its associated phonenumbers will also be deleted.
|
||
|
|
||
|
<code type="php">
|
||
|
class User extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('name', 'string', 50);
|
||
|
$this->hasColumn('loginname', 'string', 20);
|
||
|
$this->hasColumn('password', 'string', 16);
|
||
|
}
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->index('id', array('fields' => 'id'));
|
||
|
|
||
|
$this->hasMany('Phonenumber', array('local' => 'id',
|
||
|
'foreign' => 'user_id'));
|
||
|
}
|
||
|
}
|
||
|
class Phonenumber extends Doctrine_Record
|
||
|
{
|
||
|
public function setTableDefinition()
|
||
|
{
|
||
|
$this->hasColumn('phonenumber', 'string', 50);
|
||
|
$this->hasColumn('user_id', 'integer');
|
||
|
}
|
||
|
public function setUp()
|
||
|
{
|
||
|
$this->index('product_id', array('fields' => 'user_id'));
|
||
|
|
||
|
$this->hasMany('User', array('local' => 'user_id',
|
||
|
'foreign' => 'id',
|
||
|
'onDelete' => 'CASCADE'));
|
||
|
}
|
||
|
}
|
||
|
</code>
|