Sorry...My previous posting had an error in the subject line.
Unfortunately I discovered that Doctrine does not handle one-to-zero-
or-one relationships well. In fact Doctrine assumes that one-to-one
relationships cannot be one-to-zero-or-one but Doctrine's
documentation does not explicitly say so (e.g. in chapter "4.2.1 One-
To-One" or in chapter "7.1 Dealing with relations").
Programmers who expect Doctrine to treat one-to-zero-or-one
relationships correctly are bound to discover unexpected behavior.
Before I go any further let me premise this posting by saying that
overall I am very happy with Doctrine. Our company chose it as the
default ORM solution for our Enterprise-Wide applications platform.
Here is an illustration of the problem:
Imagine an object that represents a car Insurance Policy with its own
ID, date, status, etc. This Policy object has one-to-one (1-1)
"Applicant" relationship with a Person object. (Person object has its
own ID, FirstName, LastName, etc.)
A Policy object also has a one-to-zero-or-one (1-0..1) "Co-applicant"
relationship with another Person object.
class OrmPolicies extends BaseOrmPolicies
{
public function setUp()
{
// notice the 'as' keyword here
$this->hasOne('OrmPersons as Applicant', array( 'local' =>
'applicant_id',
'foreign' =>
'id'));
$this->hasOne('OrmPersons as CoApplicant', array('local' =>
'coapplicant_id',
'foreign' =>
'id'));
}
}
Now consider a script that displays a policy and then updates its
status:
line 01: $id = 15; //just some ID in Policy table
line 02: $policyObj = $conn->getTable('OrmPolicies ')->find($id);
line 03: $applicantObj = $policyObj->Applicant;
line 04: $coapplicantObj = $policyObj->CoApplicant;
line 05:
line 06: //Print out information about Applicant and coapplicant
line 07: echo "Applicant: ".$applicantObj->FirstName." ".$applicantObj-
>LastName."\n";
line 08: if ($coapplicantObj->exists()){
line 09: echo "Co-Applicant: ".$coapplicantObj ->FirstName." ".
$coapplicantObj ->LastName."\n";
line 10: }
line 11:
line 12: //Set status to a new value.
line 13: $policyObj->status = "Closed";
line 14: $policyObj->save();
If at the beginning of the script a policy did not have a coapplicant
defined (coapplicant_id was NULL in Policy table) then at the end of
the script Doctrine will add a new empty applicant to the policy
number!!!
It is NOT OBVIOUS at all to the programmer that this script might have
such consequences.
In line 04 Doctrine will create a new Person object with all fields
set to default values and linked to the $policyObject. In line 14
Doctrine will then insert the new Person object into the database and
update Policy's coapplicant_id foreign key to point to the new Person
object.
Unfortunately I could not come up with any elegant workarounds for
this problem. I had to remove CoApplicant relationship from Policy's
setUp function and manually resolve applicant_ids everywhere (see
lines 04 and 09 in the code below)
class OrmPolicies extends BaseOrmPolicies
{
public function setUp()
{
// notice the 'as' keyword here
$this->hasOne('OrmPersons as Applicant', array( 'local' =>
'applicant_id',
'foreign' =>
'id'));
}
line 01: $id = 15; //just some ID in Policy table
line 02: $policyObj = $conn->getTable('OrmPolicy')->find($id);
line 03: $applicantObj = $policyObj->Applicant;
line 04: $coapplicant_id = $policyObj->coapplicant_id
line 05:
line 06: //Print out information about Applicant and coapplicant
line 07: echo "Applicant: ".$applicantObj->FirstName." ".$applicantObj-
>LastName."\n";
line 08: if (coapplicant_id<>NULL){
line 09: $coapplicantObj = $conn->getTable('OrmPerson')-
>find(coapplicant_id);
line 10: echo "Co-Applicant: ".$coapplicantObj ->FirstName." ".
$coapplicantObj ->LastName."\n";
line 11: }
line 12:
line 13: //Set status to a new value.
line 14: $policyObj->status = "Closed";
line 15: $policyObj->save();
The culprit of this behavior is the assumption that is made in
Doctrine_Relation_ForeignKey::fetchRelatedFor() function. (line 58 in
Relation/ForeignKey.php file in Doctrine-0.10.4 release). The critical
code look as follows
...
if ($this->isOneToOne()) {
if ( ! $record->exists() || empty($id) ||
! $this->definition['table']-
>getAttribute(Doctrine::ATTR_LOAD_REFERENCES)) {
$related = $this->getTable()->create();
} else {
$dql = 'FROM ' . $this->getTable()-
>getComponentName()
. ' WHERE ' . $this->getCondition();
$coll = $this->getTable()->getConnection()-
>query($dql, $id);
$related = $coll[0];
}
$related->set($related->getTable()->getFieldName($this-
>definition['foreign']), $record, false);
} else {...
In the fifth line of this code excerpt you see: $related = $this-
>getTable()->create(); which later followed by
$related->set($related->getTable()->getFieldName($this-
>definition['foreign']), $record, false);
In order to handle one-to-zero-or-one relationship I would expect that
no new object be created (or Doctrine_Null object should be created
instead).
I would appreciate any comments/suggestions about more elegant
workarounds and any possible solutions to this problem in the future.
Best Regards,
Max Zalota