Class table inheritance

Does Yii support class table inheritance?

I can’t find it in the documents so I guess this is not supported.

I’ve been searching on the forum as well and I couldn’t find a good solution to implement this.

Any suggestions?

What do you mean with class table inheritance?

Something like user that is inherited buy StandardUser and Administrator?

There is not specific support, I usally create a foreign key in the child table (StandardUser and Administrator) and this is working quite good for me.

Did you mean Single Table Inheritance? Look at this topic: http://www.yiiframework.com/forum/index.php?/topic/11131-inheritance-extending-a-model

I guess you mean this:

http://www.martinfowler.com/eaaCatalog/classTableInheritance.html

I try to work out a generic solution in RAR for this, too.

BTW I found a nice roundup of the different object relational mapping patterns (and more) on Martin Fowlers page here:

http://www.martinfowler.com/eaaCatalog/index.html

Interesting overwiev that filled some gaps for me.

That’s what I mean.

Found also another topic about this:

http://www.yiiframework.com/forum/index.php?/topic/5803-my-multi-table-inheritance-approach/

I will try to work out a solution for this.

Will post some code later on.

I wrote some code based on this thread:

http://www.yiiframework.com/forum/index.php?/topic/5803-my-multi-table-inheritance-approach/page__view__findpost__p__34765

This is my base model:




<?php


/**

 * This is the model class for table "catalog_product".

 *

 * The followings are the available columns in table 'catalog_product':

 * @property string $id

 * @property string $type_id

 * @property string $sku

 * @property string $created

 * @property string $updated

 * @property string $subtype

 *

 * The followings are the available model relations:

 * @property CatalogProductShirt $catalogProductShirt

 */

class CatalogProduct extends CActiveRecord

{

	/**

	 * Returns the static model of the specified AR class.

	 * @return CatalogProduct the static model class

	 */

	public static function model($className=__CLASS__)

	{

		return parent::model($className);

	}


	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'catalog_product';

	}


        // REST OF CODE




Abstract class:




<?php

abstract class BaseActiveRecord extends CActiveRecord

{


  protected $_baseModel;				// holds the base model name

  protected $_relatedField;			        // holds the related field name

  protected $_baseRecord;				// holds the base record

  protected $_baseRecordAttributeNames=array();		// holds the base record attribute names




  /**

  * Returns the base model.

  * You may override this method.

  * @return string the associated database base table name

  */

  public function baseModel()

  {

    return $this->_baseModel;

  }


  /**

  * Returns the related field name.

  * You may override this method.

  * @return string the related field name

  */

  public function relatedField()

  {

    return $this->_relatedField;

  }

  

  /**

  * Sets the base model name.

  * @param string the base model name

  */

  public function setBaseModel($name)

  {

    $this->_baseModel=$name;

  }


  /**

  * Sets the related field name

  * @param string the related field name

  */

  public function setRelatedField($name)

  {

    $this->_relatedField=$name;

  }


  /**

  * Returns the column name of the base model to store the subtype in.

  * You may override this method.

  * @return string 

  */

  public function subtypeColumn()

  {

    return 'subtype';

  }

  

  /**

  * This method is invoked after a record instance is created by new operator.

  */

  protected function afterConstruct()

  {

  $this->initRelation();

  eval('$this->_baseRecord = new '.$this->baseModel().';');

  $this->initBaseRecordAttributes();

    

  if($this->baserecord->hasAttribute($this->subtypeColumn()))

    $this->baserecord->{$this->subtypeColumn()}=get_class($this);

	    

  parent::afterConstruct();

  }


  /**

  * This method is invoked after each record is instantiated by a find method.

  */

  public function afterFind()

  {

    $this->initRelation();

    eval('$this->_baseRecord='.$this->baseModel().'::model()->findByPk('.$this->{$this->relatedField()}.');');

		$this->initBaseRecordAttributes();

    return parent::afterFind();

  }


  protected function initRelation()

  {

    foreach($this->relations() as $key => $value)

    {

      // $key = Name of the Relation

      // $value[0] = Type of the Relation

      // $value[1] = Related Model

      // $value[2] = Related Field

      switch($value[0]) 

      {

        case 'CBelongsToRelation':

          $this->setBaseModel($value[1]);

          $this->setRelatedField($value[2]);

          break;

      }

    }

  }

  

  protected function initBaseRecordAttributes()

  {

    $this->_baseRecordAttributeNames = $this->baserecord->attributeNames();

  }

  

  public function getBaseRecord()

  {

    return $this->_baseRecord;

  }


  public function getBaseRecordAttributeNames()

  {

    return $this->_baseRecordAttributeNames;

  }

  

  public function __get($name)

  {

    if (in_array($name,$this->getBaseRecordAttributeNames()))

    {

      return $this->baserecord->$name;

    }

    else

    {

      return parent::__get($name);

    }

  }

  

  public function __set($name,$value)

  {

    if (in_array($name,$this->getBaseRecordAttributeNames()))

    {

      return $this->baserecord->$name = $value;

    }

    else

    {

      return parent::__set($name,$value);

    }

  }


  public function isAttributeRequired($attribute)

  {

    if (in_array($attribute,$this->getBaseRecordAttributeNames()))

    {

      return $this->baserecord->isAttributeRequired($attribute);

    }

    else

    {

      return parent::isAttributeRequired($attribute);

    }

  }


	public function getAttributeLabel($attribute)

	{

    if (in_array($attribute,$this->getBaseRecordAttributeNames()))

    {

      return $this->baserecord->getAttributeLabel($attribute);

    }

    else

    {

      return parent::getAttributeLabel($attribute);

    }

  } 


  public function setAttributes($attributes,$safeOnly=true)

  {

    parent::setAttributes($attributes,$safeOnly);

    $this->baserecord->attributes = $attributes;

  }


  public function validate($attributes=null)

  {

    $v1 = parent::validate($attributes);

    $v2 = $this->baserecord->validate($attributes);

    if ($v1 && $v2)

      return true;

    else

    {

      $this->addErrors($this->baserecord->getErrors());

      return false;

    }  

  }

	

  public function save($runValidation=true,$attributes=null)

  {

  if(!$runValidation || $this->validate($attributes))

    {

      if($this->baserecord->save(false,$attributes))

      {

        if($this->getIsNewRecord())

          $this->{$this->relatedField()}=$this->baserecord->primaryKey;


        if(parent::save(false,$attributes))

          return true;

        else

        {

          $this->baserecord->delete();

          return false;

        }

      }

    }

		else

			return false;

  }


  public function delete()

  {

    if(parent::delete())

      return $this->baserecord->delete();

  }        




}



Model that inherits from base model:




<?php


/**

 * This is the model class for table "catalog_product_shirt".

 *

 * The followings are the available columns in table 'catalog_product_shirt':

 * @property string $product_id

 * @property string $size

 * @property string $color

 *

 * The followings are the available model relations:

 * @property CatalogProduct $product

 */

class CatalogProductShirt extends BaseActiveRecordCActiveRecord

{

	/**

	 * Returns the static model of the specified AR class.

	 * @return CatalogProductShirt the static model class

	 */

	public static function model($className=__CLASS__)

	{

		return parent::model($className);

	}


	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'catalog_product_shirt';

	}


	/**

	 * @return array validation rules for model attributes.

	 */

	public function rules()

	{

		// NOTE: you should only define rules for those attributes that

		// will receive user inputs.

		return array(

			array('size', 'required'),

			array('product_id', 'length', 'max'=>10),

			array('size, color', 'length', 'max'=>128),

			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('product_id, size, color', 'safe', 'on'=>'search'),

		);

	}


	/**

	 * @return array relational rules.

	 */

	public function relations()

	{

		// NOTE: you may need to adjust the relation name and the related

		// class name for the relations automatically generated below.

		return array(

			'product' => array(self::BELONGS_TO, 'CatalogProduct', 'product_id'),

		);

	}


        // REST OF CODE



@heyhoo, remember, eval is evil

Couldn’t that line just as easily become something like below?




  $_baseModel = $this->baseModel();

  $this->_baseRecord = new $_baseModel;



Hi,

I am following all the discussions on single table and class table inheritance in here for a while but I am still searching for a "best practices" implementation.

I know that qiang doesn’t want to implement class table inheritance in Yii’s active record but there are cases in which single table inheritance would lead to an unmaintainable “column monster table”. Example: I am dealing with a database of electronic components and an STI approach would lead to 80 columns or more which is far from being perfect. I think that the pro of having better performance of selects without joins would be destroyed by bad indexing this way.

The above code may work but I would really be interested in what the Yii-gurus are suggesting. Maybe we could have a wiki entry for this issue someday.

@Mike: You said you are working on a solution for this. Did you have the chance to do that? I think a really nice way would be to define additional relations like IS_CHILD_OF or IS_PARENT_OF for example

P.S.: Thank you heyhoo for sharing your code => +1 point

I have to implement this at some point in the near future, but it’s a matter of finding the time.

I’d rather implement as a behavior though, instead of a base model (or sets of models), as models must be extended and provides less flexibility for reuse. If I come up with a solution, I’ll post here.

A behaviour would surely be a elegant way to do that, I totally second that.

Your help would be highly appreciated as I am getting more and more frustrated with my attempts to use STI.

That looks like an interesting approach - even though i think an implementation is complicated.

What i have so far is more experimental code rather than something generic. I found it hard to implement a pure CTI approach. So what i did was to create a master model + different "attribute models":

Offer.php -> AR for the parent table

AttributesSomeOfferType.php - > AR for attributes of offer type 1

AttributesSomeOtherOfferType.php -> AR for attributes of other type

The offer table holds a column "type" that defines the type of offer. Breaking clean OO patterns, i added a mapping in my Offer model that allows to fetch the related attributes model for a given type. Because i have a getSubModel() in Offer to access the related attributes model, i can use:


$offer->subModel



to get the related attributes model. This is by no means perfect - i have to deal with 2 models now in my forms. It’s not really hard to do, but doesn’t feel very clean.

Hi guys,

I spent the last couple of days thinking about a solution for this but had to give up after the hacks got nastier and nastier. Here are some of the main issues when trying to implement CTI

  • As far as I know behaviors are not able to override methods, so you won’t be able to override the __get and __set methods of CActiveRecord. You would need to extend CActiveRecord directly.

  • The above examples do not provide multiple table inheritance meaning that you can’t have a “person” that is an “employee” who is an object of the concrete class “manager”: You could fix that by merging the attributes array of the parent-record with the attributes array of its “grandparent”-record, so the __get function would go through all parent classes. I used that approach for some tests and it worked.

  • The examples also do not implement inherited relations: Let’s say a Person has an Address and you want to write code like this for an manager instance: $this->address->streetname wouldn’t work. You would have to extend the above __get examples by adding a function that is checking if there is a relation with the given name in any parent class. Again you should be able to do that with an array that is merging parent and grandparent relations().

  • I also encountered some problems when using the build in search() functionality. As this function is using CDbCriteria which is using direct SQL commands you can’t say $criteria->compare($name,$this->name) as the object’s __get() method isn’t used here. You would get an SQL error because there is no column “name” in the manager table. You would have to use something like this: ClassThatHasAttribute::model()->findbyPk($this->PrimaryKey())->attributename. Not very elegant.

These are some of the problems I encountered so I finally decided to only use Yii’s build in functionalities. I don’t want my code to break at a later stage of a project so I’ll better stick with what is already working. If you have any news regarding this topic please post your solutions. I think it could be interesting for a lot of people.

Not sure if it would help your problem but i opened a feature request for discussion that would provide events that are fired, whenever get/set/… (any magic method) is called on a non existing property:

http://www.yiiframework.com/forum/index.php?/topic/17318-new-model-events-onget-onset-onisset-onunset-oncall/

You could attach handlers from a behavoir and thus create new attributes. If this would help, i’d like to see your comment in the thread above ;)

Mike, couldn’t that possibly result in an infinite loop, as the magic functions are what’s used to implement behaviors themselves?

Where do you think magic method (__get(),__set(), …) are used with behaviors? Even if they where: It would not be affected by my proposal. We just could attach listeners to this interesting moment:

"Someone tried to access a undefined property in a model"

This could be a good step in the right direction because then we would be able to write an ClassTableInheritanceBehavior which could use these events to search for the corresponding attribute/relation/column in its parent object, yes. I like this approach and it would be much cleaner than completely overwriting CActiveRecord which could lead to a lot of complications. I added my thoughts to the thread above.

I am still thinking about a solution regarding CDbCriteria. Maybe one could simply merge the criteria provided by the parent with the current object’s criteria.

Maybe i’ve missed it from the discussion above but there’s even more to consider: Methods like rules(), behaviors(), scopes(), … will have to be merged with the return value of their parent::*() counterpart:




public function rules() {

    return CMap::mergeArray(parent::rules(),array(

        // more rules here

    ));

}

Hi, have anybody got a general solution? I’m facing the same problem and trying to code a general solution. It goes quite well, I’ll come with news if you are interested. Bye.

I want to share my examinations of the class table inheritance problem and possible solutions.

I do not know whether it is worth to add this examination as wiki article, but I think it should be added to this thread.

If you do something different from approaches I describe below - please share you solution.

And here it is:

yii - class (or multiple) table inheritance

Class table inheritance seems to have no perfect solution (comparing to very good one for single table inheritance).

Possible solutions I see are:

[list=1]

[*]Add support for class table inheritance to the active record class. There are some implementations of this method (see here and here for examples). But I do not like this approach because it is too complex to implement it properly and to make it work for all possible active record usages.

[*]Use MySQL VIEWs. Mysql allows creating a VIEW which can be updated by INSERT and UPDATE statements. So we can hide two tables (for base and child classes) behind the view and use it is usual single table.

[*]Use single table inheritance and keep extended data in separate tables.

[/list]

Since solution (1) does not look like a good approach (at least for me) I decided to try (2) and (3) in a simple application.

MySQL VIEWs

Using MySQL views we can join two or more tables on the database level and work with join result as with single table.

This approach could be a perfect solution, but unfortunately MySQL has some limitations related to views which join several tables.

I examined these limitations in my test application (“view” module) and found following issues:

  • some additional code needed to make active record work properly (it does not detects primary key automatically and does not set record id after insert). This issue is not critical and can be handled in active record subclass.

  • MySQL does not allow to delete records using view. This issue also can be handled in active record subclass - we can emulate delete method and do actual delete from joined tables.

  • MySQL does not allow to update both joined tables at once. This is very disappointing and breaks the idea of hiding two tables behind the view.

This issue also can be handled, for example in my sample app I show only fields from the base take when creating a record and only fields from the extended table when updating.

The controller code in this case can remain simple and similar to the single table case, but you anyway should remember that you use the view and not a single table and do special handling in the UI on in the controller when you create / update records.

So views can not provide a good abstract level when you want to create and update records.

What is good with views is that you can easily list records - the code will be exactly the same as for the single table.

It is possible to use MySQL views to implement class table inheritance, but I would not recommend this, because of complexities with create / update data code.

Views are convenient when you list records and simplify configuration of CListView and CGRidView widgets.

Single table inheritance and additional data in external tables

This approach is implemented in the “aggregation” module of my test application.

Data structure is following:

[sql]

CREATE TABLE car (

id INT NOT NULL AUTO_INCREMENT ,

name VARCHAR(45) NULL ,

type ENUM(‘Car’,‘SportCar’,‘FamilyCar’) NULL DEFAULT ‘Car’ ,

PRIMARY KEY (id) )

ENGINE = InnoDB;

CREATE TABLE sport_car_data (

car_id INT NOT NULL ,

power INT NOT NULL ,

PRIMARY KEY (car_id) ,

CONSTRAINT fk_sport_car_data_car

FOREIGN KEY (`car_id` )


REFERENCES `car` (`id` ))

ENGINE = InnoDB;

CREATE TABLE family_car_data (

car_id INT NOT NULL ,

seats INT NOT NULL ,

PRIMARY KEY (car_id) ,

CONSTRAINT fk_family_car_data_car

FOREIGN KEY (`car_id` )


REFERENCES `car` (`id` ))

ENGINE = InnoDB;

[/sql]

Here the ‘car’ table is single inheritance table with type field - ENUM with class names.

Base class is ‘Car’:




class Car extends CActiveRecord {

    public function tableName() {

	return 'car';

    }


    protected function instantiate($attributes) {

        $class=$attributes['type'];

        $model=new $class(null);

        return $model;

    }


    public function beforeSave() {

        if ($this->isNewRecord) {

            $this->type = get_class($this);

        }

        return parent::beforeSave();

    }

}


class FamilyCar extends Car {

    public function tableName() {

        return 'car';

    }


    public function relations() {

       return array(

            'data' => array(self::HAS_ONE, 'FamilyCarData', 'car_id'),

        );

    }


    function defaultScope() {

        return array(

            'condition'=>"type='FamilyCar'",

        );

    }

}


class SportCar extends Car {

    public function tableName() {

        return 'car';

    }


    public function relations() {

       return array(

            'data' => array(self::HAS_ONE, 'SportCarData', 'car_id'),

        );

    }


    function defaultScope() {

        return array(

            'condition'=>"type='SportCar'",

        );

    }

}



Some notes:

[list=1]

[*]Class name is used as value for “type” field, so there is no need in switch in the Car::instantiante method.

[*]The Car::defaultScope() method is defined in the child classes only. This way we can use base Car class to process all models regardless of type and child classes FamilyCar and SportCar to work with one model type only.

[*]Child classes have ‘data’ relation which points to the model with extended type data. In this case FamilyCarData and SportCarData are such models with extended data.

[/list]

Whith this approach you should handle two models in controllers and views related to supclasses (FamilyCar and SportCar).

For example, create action in the FamilyCar controller:




public function actionCreate() {

    //create base model and model with extended data

    $model=new FamilyCar;

    $model->data=new FamilyCarData;


    if(isset($_POST['FamilyCarData'])) {

        //get properties for both models and save them

        //of cause it is better to use transaction here

        $model->attributes=$_POST['FamilyCar'];

        $model->data->attributes=$_POST['FamilyCarData'];

        if($model->save()) {

            $model->data->car_id = $model->id;

            if ($model->data->save()) {

                $this->redirect(array('view','id'=>$model->id));

            }

        }

    }


    $this->render('create',array(

        'model'=>$model,

    ));

}



And the view code will be like this:




<div class="form">


<?php $form=$this->beginWidget('CActiveForm', array(

	'id'=>'family-car-form',

	'enableAjaxValidation'=>false,

)); ?>


	<p class="note">Fields with <span class="required">*</span> are required.</p>


	<?php echo $form->errorSummary($model); ?>


	<div class="row">

       <?php echo $form->labelEx($model,'name'); ?>

       <?php echo $form->textField($model,'name'); ?>

       <?php echo $form->error($model,'name'); ?>

	</div>

	<div class="row">

       <?php echo $form->labelEx($model->data,'seats'); ?>

       <?php echo $form->textField($model->data,'seats'); ?>

       <?php echo $form->error($model->data,'seats'); ?>

	</div>


	<div class="row buttons">

		<?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?>

	</div>


<?php $this->endWidget(); ?>


</div><!-- form -->



Here we can access extended data through relation: $model->data.

Summary

In my opinion it is better to use the mentioned above solution #3 - single table inheritance with extended data in separate tables.

Yes, you will have to handle two models in your controllers / views, but you have a clean view of what is going on and the fact you actually use two database tables is explicit.

This approach can be combined with solution #2 - MySQL views - which are good for list and grid views (but not for create / update forms).

Links

http://www.martinfowler.com/eaaCatalog/classTableInheritance.html

http://www.yiiframework.com/wiki/198/single-table-inheritance/

http://dev.mysql.com/doc/refman/5.0/en/view-updatability.html

http://www.yiiframework.com/forum/index.php?/topic/12978-class-table-inheritance

http://www.yiiframework.com/forum/index.php?/topic/5803-my-multi-table-inheritance-approach/

http://www.yiiframework.com/forum/index.php?/topic/23405-cactiverecord-inheritance-and-dynamic-attributes/

http://www.yiiframework.com/forum/index.php?/topic/11131-inheritance-extending-a-model/

Hi, yes, I finally developed a solution! From my point of view it is very good because you may have several levels of inheritance (I used recursion for it) and it is provided transparently, that is, you just need to extend the class (InheritableActiveRecord) I’ve developed to get inheritance. It’s easy to implement, just requires extend from InheritableActiveRecord and follow some BBDD design tips. I didn’t publish it yet because I think it needs to be polished up before share. I’ll try to refine it, so it can be shared and maybe others can contribute.