I'm using Propel 1.7-dev and am playing around with class delegation i.e. table "inheritance". I've got Propel1 and Phing set up as a Git submodule and each is checked out on their respective masters. My models build and I can get the hierarchy to work, but if I do it as per the docs (arguably more cleanly) then it fails.
Here's the relevant snippet of my models:
<table name="process_step">
<column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
<column name="process_id" type="integer" required="true" />
<column name="ordinal" type="integer" required="true" />
<column name="comment" type="varchar" size="256" />
<column name="type" type="enum" required="true"
valueSet="load, submit_form, check_page, get_links"
/>
<column name="is_enabled" type="boolean" required="true" default="false" />
<foreign-key foreignTable="process">
<reference local="process_id" foreign="id" />
</foreign-key>
</table>
<table name="process_step_load">
<column name="id" type="integer" required="true" primaryKey="true" />
<behavior name="delegate">
<parameter name="to" value="process_step" />
</behavior>
<column name="url" type="varchar" size="1024" />
<column name="method" type="varchar" size="6" required="true" default="get" />
</table>
There are in practice many more child models, but I've removed them for brevity.
The first thing to notice is that the docs show parent and child class definitions as all having auto-incremented primary keys. This isn't possible, however: the primary key must be set in the parent, and copied into the child (since the parent will always have a row). I believe this to be a mistake in the docs, but I'll offer a patch for that after this question, in case I have misunderstood something.
So, here's my working code:
$process = new Process();
$processStep = new ProcessStep();
$processStep->setOrdinal(1); // Simplified for brevitiy
$processStep->setType(1); // Ditto
$processStep->setProcess($process);
// This creates a primary key which we use below (i.e. a cascading
// save won't work)
$processStep->save();
$processStepChild->setId($processStep->getId());
$processStepChild->save();
Here's how the docs would have it:
$process = new Process();
$processStep = new ProcessStep();
$processStep->setOrdinal(1); // Simplified for brevitiy
$processStep->setType(1); // Ditto
$processStep->setProcess($process);
$processStepChild->setProcessStep($processStep);
$processStepChild->save();
The result of this is that, where I create four child rows (each of a different type), the primary keys are set fine in the parent (ProcessStep) but are all set as zero in the children (ProcessStepLoad and others).
Even if I add in the intermediate save, this doesn't help. Thus, it seems to me that $processStepChild->setProcessStep($processStep)
is the problem - it should work out that it is on a delegate child, and change the primary key of that child to the key of the supplied parent. It is clearly not doing so.
Some questions that might be relevant:
enum
in the parent manually)Right, I just switched to MySQL's InnoDB engine, so I can make use of constraints, and my new parent table looks like the following. This illustrates where things are going wrong; the relationships on the tables are the wrong way around:
CREATE TABLE `process_step`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`process_id` INTEGER NOT NULL,
`ordinal` INTEGER NOT NULL,
`comment` VARCHAR(256),
`type` TINYINT NOT NULL,
`is_enabled` TINYINT(1) DEFAULT 0 NOT NULL,
PRIMARY KEY (`id`),
INDEX `process_step_FI_1` (`process_id`),
CONSTRAINT `process_step_FK_1`
FOREIGN KEY (`process_id`)
REFERENCES `process` (`id`),
CONSTRAINT `process_step_FK_2`
FOREIGN KEY (`id`)
REFERENCES `process_step_load` (`id`)
ON DELETE CASCADE,
CONSTRAINT `process_step_FK_3`
FOREIGN KEY (`id`)
REFERENCES `process_step_submit_form` (`id`)
ON DELETE CASCADE,
CONSTRAINT `process_step_FK_4`
FOREIGN KEY (`id`)
REFERENCES `process_step_check_page` (`id`)
ON DELETE CASCADE,
CONSTRAINT `process_step_FK_5`
FOREIGN KEY (`id`)
REFERENCES `process_step_get_links` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB;
The last four (to the child tables) of course are wrong: we cannot have a foreign key to each child since that would require every child to extend every parent. It turns out that this happens in the absence of an explicit FK in the child. Thus, I changed the class children in the schema to include an FK to the parent:
<table name="process_step_load">
<column name="id" type="integer" required="true" primaryKey="true" />
<foreign-key foreignTable="process_step">
<reference local="id" foreign="id" />
</foreign-key>
<behavior name="delegate">
<parameter name="to" value="process_step" />
</behavior>
<column name="url" type="varchar" size="1024" />
<column name="method" type="varchar" size="6" required="true" default="get" />
</table>
This creates the constraints correctly across the database:
DROP TABLE IF EXISTS `process_step`;
CREATE TABLE `process_step`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`process_id` INTEGER NOT NULL,
`ordinal` INTEGER NOT NULL,
`comment` VARCHAR(256),
`type` TINYINT NOT NULL,
`is_enabled` TINYINT(1) DEFAULT 0 NOT NULL,
PRIMARY KEY (`id`),
INDEX `process_step_FI_1` (`process_id`),
CONSTRAINT `process_step_FK_1`
FOREIGN KEY (`process_id`)
REFERENCES `process` (`id`)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- process_step_load
-- ---------------------------------------------------------------------
DROP TABLE IF EXISTS `process_step_load`;
CREATE TABLE `process_step_load`
(
`id` INTEGER NOT NULL,
`url` VARCHAR(1024),
`method` VARCHAR(6) DEFAULT 'get' NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `process_step_load_FK_1`
FOREIGN KEY (`id`)
REFERENCES `process_step` (`id`)
) ENGINE=InnoDB;
This has permitted me to use the cascading save as per the docs, yay!
Morals of the story: (a) I should read the docs more carefully, (b) the docs illustrate many:1 inheritance perfectly correctly, (c) with my foreign keys set up, 1:1 inheritance is also possible in Propel.