views:

72

answers:

1

I've run into a bit of a snag in my application where a relationship defined as a one-to-many relationship returns a model object (instance of Doctrine_Record) instead of a Doctrine_Collection when I try to access it as $model->RelatedComponent[] = $child1. This, of course, yields an exception like so:

Doctrine_Exception: Add is not supported for AuditLogProperty

#0 path\library\Doctrine\Access.php(131): Doctrine_Access->add(Object(AuditLogProperty))

#1 path\application\models\Article.php(58): Doctrine_Access->offsetSet(NULL, Object(AuditLogProperty))

#2 path\library\Doctrine\Record.php(354): Article->postInsert(Object(Doctrine_Event))

#3 path\library\Doctrine\Connection\UnitOfWork.php(576): Doctrine_Record->invokeSaveHooks('post', 'insert', Object(Doctrine_Event))

#4 path\library\Doctrine\Connection\UnitOfWork.php(81): Doctrine_Connection_UnitOfWork->insert(Object(Article))

#5 path\library\Doctrine\Record.php(1718): Doctrine_Connection_UnitOfWork->saveGraph(Object(Article))

#6 path\application\modules\my-page\controllers\ArticleController.php(26): Doctrine_Record->save()

#7 path\library\Zend\Controller\Action.php(513): MyPage_ArticleController->createAction()

#8 path\library\Zend\Controller\Dispatcher\Standard.php(289): Zend_Controller_Action->dispatch('createAction')

#9 path\library\Zend\Controller\Front.php(946): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))

#10 path\library\Zend\Application\Bootstrap\Bootstrap.php(77): Zend_Controller_Front->dispatch()

#11 path\library\Zend\Application.php(358): Zend_Application_Bootstrap_Bootstrap->run()

#12 path\public\index.php(11): Zend_Application->run()

#13 {main}

This is what my yaml-schema looks like (excerpt):

AuditLogEntry:
  tableName: audit_log_entries
  actAs:
    Timestampable:
      updated: {disabled: true}
  columns:
    user_id: {type: integer(8), unsigned: true, primary: true}
    id: {type: integer(8), unsigned: true, primary: true, autoincrement: true}
    type: {type: string(255), notnull: true}
    mode: {type: string(16)}
    article_id: {type: integer(8), unsigned: true}
    comment_id: {type: integer(8), unsigned: true}
    question_id: {type: integer(8), unsigned: true}
    answer_id: {type: integer(8), unsigned: true}
    message_id: {type: integer(8), unsigned: true}
  indexes:
#   Must index autoincrementing id-column since it's a compound primary key and 
#   the auto-incrementing column is not the first column and we use InnoDB.
    id: {fields: [id]}
    type: {fields: [type, mode]}
  relations:
    User:
      local: user_id
      foreign: user_id
      foreignAlias: AuditLogs
      type: one
      onDelete: CASCADE
      onUpdate: CASCADE

And then we have the related model:

AuditLogProperty:
  tableName: audit_log_properties
  columns:
    auditlog_id: {type: integer(8), unsigned: true, primary: true}
    prop_id: {type: integer(2), unsigned: true, primary: true, default: 1}
    name: {type: string(255), notnull: true}
    value: {type: string(1024)}
  relations:
    AuditLogEntry:
      local: auditlog_id
      foreign: id
      type: one
      foreignType: many
      foreignAlias: Properties
      onDelete: CASCADE
      onUpdate: CASCADE

Now, if we look at the generated class-files, it looks fine:

/**
 * @property integer $user_id
 * @property integer $id
 * @property string $type
 * @property string $mode
 * @property integer $article_id
 * @property integer $comment_id
 * @property integer $question_id
 * @property integer $answer_id
 * @property integer $message_id
 * @property integer $news_comment_id
 * @property User $User
 * @property Doctrine_Collection $Properties
 * @property Doctrine_Collection $Notifications
 */
abstract class BaseAuditLogEntry extends Doctrine_Record

/**
 * @property integer $auditlog_id
 * @property integer $prop_id
 * @property string $name
 * @property string $value
 * @property AuditLogEntry $AuditLogEntry
 */
abstract class BaseAuditLogProperty extends Doctrine_Record

However, when I later try to add properties I get the exception posted in the beginning of the question:

$auditLog = new AuditLogEntry();
$prop1 = new AuditLogProperty();
$prop1->name = 'title';
$prop1->value = $this->Content->title;
$prop2 = new AuditLogProperty();
$prop2->name = 'length';
$prop2->value = count($this->Content->plainText);
$auditLog->Properties[] = $prop1;
$auditLog->Properties[] = $prop2;
$auditLog->save();

If I do the following:

var_dump(get_class($auditLog->Properties));

I get that Properties is of type AuditLogProperty, instead of Doctrine_Collection.

I use version 1.2.3 of Doctrine.

  • Can anyone spot what is wrong?
  • Is the problem that I use compound primary keys in a way Doctrine doesn't approve of?
  • Any ideas of how to solve it?
+1  A: 

This is not a bug. :)

The problem here lies in the auditlog_id field in the AuditLogProperty model.

  1. This field is a primary key
  2. This field is also a foreign key
  3. Condition 1 and 2 can not apply at the same time.

EDIT: This varies per database type, but Doctrine does not (seem to) allow it.

As soon as you remove the 'primary: true' from the auditlog_id field in your AuditLogProperty model and recreate the database, you will see that your problem disappears.

I don't know exactly why this is impossible, but I could never recommend to make a foreign key primary as well (unless it happens inside a reference table in a many-to-many relation). If you need a FK to be unique, use 'unique:true' instead.

I do understand though that you want to have one combination of prop_id and auditlog_id only once. As far as I know, the only way to achieve this in Doctrine is by using business rules in your model. You can for instance implement public function preInsert($event) inside your PHP class that makes the check and throws an exception (Doctrine would do the same) when something is wrong.

Pelle ten Cate
They can apply at the same time, it just depends on the order that you insert into the database. I order it this way because I want the following properties to hold: 1. AuditLog is dependent on an existing user. Unless the user exists in the database he won't do anything in the system. 2. AuditLogProperty depends on an AuditLog record. It shouldn't be possible to insert a property that doesn't refer to an AuditLog record. 3. I want AuditLog and AuditLogProperty to be clustered in an efficient way.. SO, since it is possible to have this schema without Docrine, it's a limitation
PatrikAkerstrand
This might be a limitation that is introduced by a database-agnostic paradigm. As far as I know, MySQL does support FK constraints on a PK field, but not since a very long time. I think because Doctrine wants to be database-agnostic, they still support databases that can't have FK constraints on PK fields.
Pelle ten Cate