My multi table inheritance approach

Here are the tables:


create table Super (

	id			int unsigned auto_increment primary key,

	superAttr	char(10) not null default ''

);


create table Sub (

	id		int unsigned primary key,

	subAttr	char(10) not null default ''

);

column id in table Sub is foreign key to Super.id,but I removed the foreign key as it’s not necessary.

And classes,normal functions such as model(),tableName(),rules(),etc didn’t list below for focusing.





class Sub extends Super

{

	/**

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

	 * @var integer $id

	 * @var string $subAttr

	 */

	public $superAttr;


	/**

	 * I didn't use AR relation in my approach

	 * @return array relational rules.

	 */

	public function relations()

	{

		return array(

		);

	}


	public static function getParentDao()

	{

		return parent::model();

	}


	public function populateRecord($attributes,$callAfterFind=true)

	{

		return $this->populateRecordWithParent($attributes,$callAfterFind);

	}

}


class Super extends CAgsAr

{

	/**

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

	 * @var integer $id

	 * @var string $superAttr

	 */


	/**

	 * I didn't use AR relation in my approach

	 * @return array relational rules.

	 */

	public function relations()

	{

		return array(

		);

	}

}


/**

 * AegeanSiren ActiveRecord

 * A patch of CActiveRecord

 */

abstract class CAgsAr extends CActiveRecord {

	

	public static function model($className=__CLASS__)

	{

		if (__CLASS__ === $className)

		{

			/**

			 * prevent class that directly inherited from CAgsAr

			 * to get a model of CAgsAr

			 */

			return false;

		}

		else{

			return parent::model($className);

		}

	}

	/**

	 * populate model with both child and parent table contents

	 */

	public function populateRecordWithParent($attributes,$callAfterFind=true)

	{

		//call CActiveRecord::populateRecord() to do the normal job

		$record = parent::populateRecord($attributes,$callAfterFind);


		//then our showtime

		if($attributes!==false)

		{

			$parentDao = call_user_func(array($this,'getParentDao'));

			if (false === $parentDao)

			{

				throw new CException(get_class($this).' is not defined correctly to use populate with parent.');

			}

			else

			{

				//get the related record of parent table,and then merge it into the model.

				$parentRecord = $parentDao->findByPk($record->primaryKey);


				foreach($parentRecord->getMetaData()->columns as $name=>$column)

				{

					$record->$name=$parentRecord->$name;

				}


				return $record;

			}

		}

		else

			return null;

	}

}



So far,we could load from both table.But inserting and updating is not done.

Still I’m double if my approach is good and if there are better ways.This is why I post there young codes here - Any ideas?Even “fool,all the world use a better approach that…” is welcome.

I’m too sleepy to keep on working for this moment.

Later I will post about progress.

Here are the improved solution.




/**

 * This class is completely as normal as any other AR class

 */

class Super extends CActiveRecord

{

	/**

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

	 * @var integer $id

	 * @var string $superAttr

	 */	

}

/**

 * this is the one

 */

abstract class SubSupportor extends CActiveRecord

{

	protected $_super;

	protected $_superAttrList = array('superAttr');


	public function __get($name)

	{

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

		{

			/**

			 * call '->super' instead of '->_super'

			 * to trigger init in getSuper()

			 */

			return $this->super->$name;

		}

		else

		{

			return parent::__get($name);

		}

	}


	public function __set($name,$value)

	{

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

		{

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

		}

		else

		{

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

		}

	}


	public function getSuper()

	{

		if (null === $this->_super)

		{

			$this->_super = $this->isNewRecord?new Super:Super::model()->findByAttributes(array('id'=>$this->id));

			$this->_super->scenario = $this->scenario;

			if (null === $this->_super)

			{

				throw new CException('Cant find _super for '.get_class($this).'#'.$this->id);

			}

		}

		return $this->_super;

	}


	public function validate($attributes=null)

	{

		if (parent::validate($attributes))

		{

			if ($this->super->validate($attributes))

			{

				return true;

			}

			else

			{

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

				return false;

			}

		}

		else

		{

			return false;

		}

	}


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

	{

		$valid = $runValidation?$this->validate($attributes):true;


		if ($valid)

		{

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

			{

				if ($this->isNewRecord)

				{

					$this->id = $this->super->id;

				}


				return parent::save(false,$attributes);

			}

		}


		return false;

	}


	public function delete()

	{

		parent::delete();

		$this->super->delete();

	}

}

/**

 * This class's only different

 * is to extend from SubSupportor

 */

class Sub extends SubSupportor

{

	/**

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

	 * @var integer $id

	 * @var string $subAttr

	 *

	 * And,parent attributes which defined in $_superAttrList

	 * could access in Sub instance

	 * like $subInstance->superAttr

	 */

}



I’ve been experimenting with your code for the last couple of days - thank you. Although I really like the look of your approach I just couldn’t get it working. I’ve gone back to the code that I’ve used before - it makes use of Yii’s AR so that the super model can be populated through eager loading.

Users view a stream of items on my site - items are wire news, news items, adverts, blog posts etc. There is a core Item model, extended by ItemWire, ItemNews etc.

The core Item class is a standard model.




class Item extends CActiveRecord

{

	/**

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

	 * @var integer $itemId

	 * @var integer $userId

	 * @var string $summary

	 * @var string $created

	 * @var string $modified

	 * @var integer $entityId

	 */


	/**

	 * Returns the static model of the specified AR class.

	 * @return CActiveRecord 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 'Item';

	}

}



There is then a base model that all the child models extend from:




class ItemBase extends CActiveRecord

{

	// These attributes come from the Item parent

	public $summary;

	public $created;

	public $modified;

	public $entityId;


	// List of attributes stored in parent table

	public $_itemColumns = array('summary','created','modified','entityId');





	/**

	 * All items belong to item

	 * @return <type>

	 */

	public function relations()

	{

		return array(

			'item'=>array(self::BELONGS_TO,'Item','itemId'),

		);

	}


	/**

	 * Passes parent table properties up to relation and saves them

	 * @return bool calls parent::beforeSave()

	 */

	public function beforeSave()

	{

		if ($this->isNewRecord)

		{

			$item = new Item();

			$item->itemClass = $this->itemClass();

			foreach ($this->_itemColumns as $columnName)

				$item->$columnName = $this->$columnName;

			$item->save();


			$this->itemId = $item->itemId;

		} else

		{

			$this->item->summary = $this->summary;

			$this->item->modified = $this->modified;

			$this->item->save();

		}

		return parent::beforeSave();

	}


	/**

	 * Sets the properties of this model to be those of the parent table 

	 * @return bool calls parent::afterFind()

	 */

	public function afterFind()

	{

		foreach ($this->_itemColumns as $columnName)

				$this->$columnName = $this->item->$columnName;

		return parent::afterFind();

	}

}



Then each child model extends this ItemBase class:




class ItemNews extends ItemBase

{

	// ... Contains standard model methods

	

	// Provides the name of this itemClass

	public function itemClass()

	{

		return 'News';

	}


	public function rules()

	{

		return array( // Contains validation rules for parent model fields in addition to those of child table);

	}

}



The major advantage of this is that models can be populated with a simple join - ItemWire::model()->with(‘item’)->findAll().

It’s far from perfect though, and I’d certainly welcome your thoughts.

I came here looking for a solution to this exact problem with a very similar setup. This should save me hours (days) of work. Thanks!

Alex,

  1. You setup seems to handle create/update operations. How do you handle deletion of a sub item (e.g. NewsItem)?

  2. Do you also extend child controllers from a base ItemController?

  3. Also, could you provide a short example of a rules method that combines fields from the Item model and ItemNews model?

thanks,

Muhammad

I do deletions through the parent model - Item::model()->findByPk(1)->delete() - I guess this might not be the most logical thing, but it seems to work. Have a look at the Item model code below for how that works.

No, I’ve never extended a controller - I’d be interested to see an implementation of this.

In my ItemNews model I have the following:




public function rules()

{

	return array(

		array('summary,sourceUrl,sourceName','required'),

		array('itemId', 'numerical', 'integerOnly'=>true),

	);

}



summary is a column in the Item table, and sourceUrl and sourceName are in the ItemNews table.

There is another side to the structure that I use - I’d really like everyone else’s ideas because it seems quite convoluted.

In my case most of my relations are held by the Item table. For example an Item (of whatever type) is added by (belongs to) a Business. I therefore do most of my queries like this:




$items = Item::model()->findAll("businessId=1");



I then need a way to access the child model (eg. ItemNews, ItemWire) from the parent model (Item). Each child has a different name so I came up with the following solution:




class Item extends CActiveRecord

{ 

	// ... STANDARD MODEL STUFF ....


	 /**

	 *

	 * @var object Used to store extension object for this item {@link getExtension()}

	 */

	private $_extension;




	public function getExtension ()

	{

		if (isset($this->_extension))

			return $this->_extension;

		else

		{

			$relationName = strtolower($this->itemClass); // itemClass is capitalised (relation is lowercase)

			

			// Check whether there is a relation for the extension model 

			// This enables extension to be eager-loaded

			if (!is_null($this->{$relationName}))

				$this->_extension = $this->{$relationName};

			else

				$this->_extension = CActiveRecord::model('Item'.$this->itemClass)->findByPk($this->itemId);


			return $this->_extension;

		}

	}


	/**

	 * Deletes extension when an item is deleted.

	 * @return bool calls parent::beforeDelete()

	 */

	public function beforeDelete()

	{

		$this->extension->delete();

	}


}



This way I can access child table properties through the parent using $item->extension->sourceUrl

It’s far from ideal though. Ideally I would be able to access the child-table properties using $item->sourceUrl, but I can’t figure out how I would do this.

Alex,

Could you do something that is essentially the reverse of your original setup? In other words…

First declare the array of fields used by the child model.





class NewsItem extends CActiveRecord


  public function getItemFields()

  {

     return array('title', 'content', 'contentParsed');

  }

}



Then in the Item model:




class Item extends CActiveRecord

{


    //These attributes come from news item model

    public $title;

    public $subheading;

    public $byline; 

    public $article;


    //These attributes come from blog item model

    //public $title;  //the blog model uses a title field also, but we dont need to define more than one

    public $content;

    public $contentParsed; 

    


  . . . 

 

  public function afterFind()

  {

    $modelname = $this->itemClass; //itemClass name should confirm to the name of the sub-item model

    $itemFields = $modelname::model()->itemFields; //fields as defined in getItemFields method of relevant model

    foreach ($itemFields as $columnName)

       $this->$columnName = $this->strtolower($modelname)->$columnName; 

    return parent::afterFind();

  }

}



Im not actually sure that $itemFields = $modelname::model()->itemFields; is syntactically correct, in which case you might need to resort to using if/else or a case statement. I havent tried this yet, but dont see any reason why it should not work?

I had a think about that approach - it would work but I think I might end up creating a huge model file. When it came to validation rules and attribute labels I think it might become too confusing, with case or if statements littered throughout. Though I do much prefer the idea of having a single model that deals with both tables.

I frankly don’t know what the solution is - it seems like quite a common data structure but there’s very little written about it. I feel like I’ve tried it from loads of angles and nothing quite fits perfectly.

Incidentally, the technical name for this structure seems to be either class table inheritance or concrete table inheritance.

You can’t call $modelName::model()->findAll(), you have to use CActiveRecord::model($modelName)->findAll(). The result is the same. You can, however, use


new $modelName;

You only run into the validation rules problem if you try to do something like "new Item". My thinking is that referencing the item model directly would only be used for looking up items from the database (e.g. list, show, delete actions), in which case validation is not an issue. Attempting a "new Item" anywhere outside of the ItemBase model would definately open up a huge can of worms. I guess this does make the code somewhat less "robust" but the ability to easily reference any item through the item model is really handy for things like listing all items by a user, deleting any item by id without having to figure out or verify what type it is, etc.

Hi,Alex,may you describe what problem you met with my code here?I will see what could I do.Thank u very much^^

That’s kind of you. I’m afraid I can’t remember what the problem was now and I still can’t get my head around version control so there’s no trace of the old code. I’m fairly happy with my current implementation - it’s scaling up quite well as the project expands. How is yours coming along?

I use this approach in my current project.It works quite well so far.here cames some codes from the project code.

first comes the AR class of parent table


class User extends CActiveRecord

{

	/**

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

	 * @var integer $id

	 * @var string $email

	 * @var string $password

	 * @var string $salt

	 * @var string $name

	 * @var string $subtype

	 */


	/**

	 * Returns the static model of the specified AR class.

	 * @return CActiveRecord 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 'User';

	}


	/**

	 * other codes...

	 */

}

then the support class of child table AR class


abstract class Baseuser extends CActiveRecord

{

	/*

	 * when call $instanceOfBaseuser->baseuser

	 * will trigger getBaseuser() and init $_baseuser.

	 */

	protected $_baseuser

	protected $_baseAttrs = array('email','password','salt','name','subtype');


	public function __get($name)

	{

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

		{

			return $this->baseuser->$name;

		}

		else

		{

			return parent::__get($name);

		}

	}


	public function __set($name,$value)

	{

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

		{

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

		}

		else

		{

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

		}

	}


	public function setScenario($value)

	{

		parent::setScenario($value);


		if (null === $this->_baseuser)

		{

			$this->_baseuser = $this->isNewRecord?new User:User::model()->findByAttributes(array('id'=>$this->id));

		}

		if (null != $this->_baseuser)

		{

			$this->_baseuser->scenario = $value;

		}

	}


	public function isAttributeRequired($attribute)

	{

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

		{

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

		}

		else

		{

			return parent::isAttributeRequired($attribute);

		}

	}


	public function getBaseuser()

	{

		if (null === $this->_baseuser)

		{

			$this->_baseuser = $this->isNewRecord?new User:User::model()->findByAttributes(array('id'=>$this->id));

			if (null === $this->_baseuser)

			{

				throw new CException('Cant find baseuser for '.get_class($this).'#'.$this->id);

			}

		}

		return $this->_baseuser;

	}


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

	{

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


		$this->baseuser->attributes = $attributes;

		$this->baseuser->subtype = strtolower(get_class($this));

	}


	public function validate($attributes=null)

	{

		if (parent::validate($attributes))

		{

			if ($this->baseuser->validate($attributes))

			{

				return true;

			}

			else

			{

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

				return false;

			}

		}

		else

		{

			return false;

		}

	}


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

	{

		$valid = $runValidation?$this->validate($attributes):true;


		if ($valid)

		{

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

			{

				if ($this->isNewRecord)

				{

					$this->id = $this->baseuser->id;

				}


				return parent::save(false,$attributes);

			}

		}


		return false;

	}


	public function delete()

	{

		parent::delete();


		$this->baseuser->delete();

	}

}

last one of the child table AR classes


class Jobseeker extends Baseuser

{

	/**

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

	 * @var integer $id

	 * @var integer $schoolId

	 * @var integer $courseId

	 * @var integer $created

	 * @var integer $updated

	 */

	/**

	 * Returns the static model of the specified AR class.

	 * @return CActiveRecord 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 'Jobseeker';

	}


	/**

	 * other codes...

	 */		

}

Just a note, the above code is a partial solution that would work for most users. Once you start using CActiveRecord’s other methods, such as updateAll, I believe errors would be encountered (or data would simply fail to be saved).

You should also be aware of transaction security when you save or delete.