When and where to use DAO vs Active Record

I am still in the early stages of learning Yii (an MVC, OOP, etc) and I hoping someone can take a little time to help me figure out when and where Active Record should be used vs DAO. I have spent the past few days reading nearly every wiki entry, forum post, and Yii document I could find regarding the topic but have not been able to find a definitive answer. While I understand there is no "right" answer, there still must be a best practice according to Yii experts or authors and that is what I am looking for.

Here is the simplified version of the tables I have and their relations:

"Film" table ~400 records (contains the name of the film, the running time, rating, production year, etc.)

"Name" table ~18,000 (contains names of actors, actresses, directors, and anyone else credited in the films)

"Role" table ~10,000(contains the roles that can be assigned to any credited individuals such as director, character name, etc.)

"Credits" table ~38,000 (contains the combination of the three above tables as well as a flag for cast/crew such as film_id=145, name_id=20786, role_id=48, and is_cast=0 which tells me that Christopher Nolan is the Director of Batman Begins and is a crew member, not a cast member)

I have models, controllers, and views for all of these with basic CRUD functionality. My understanding is that this is what Active Record does best. Right now I have the film information displaying in the CDetailView widget as delivered in the view.php file on the film view.

What I would like is to add a list of cast (is_cast=1) and a list of crew (is_cast=0) beneath the CDetailView widget. As I mentioned earlier, I have read a ton of documents/wikis/forums and cannot figure out the best way to do this. My first attempt was based on some examples in the Agile book and was to modify FilmController.php controller file and change the actionView function to the following:




public function actionView($id)

{

	$creditsCrewDataProvider=new CActiveDataProvider('Credits', array(

		'criteria'=>array(

			'condition'=>'film_id=:filmId AND is_cast=0',

			'params'=>array(':filmId'=>$id),

		),

	));

	$creditsCastDataProvider=new CActiveDataProvider('Credits', array(

		'criteria'=>array(

			'condition'=>'film_id=:filmId AND is_cast=1',

			'params'=>array(':filmId'=>$id),

		),

	));

		

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

		'model'=>$this->loadModel($id),

		'creditsCrewDataProvider'=>$creditsCrewDataProvider,

		'creditsCastDataProvider'=>$creditsCastDataProvider,

	));

}



and modify the Credits.php model file to include




public function getNameOptions()

{

	$nameArray = CHtml::listData(Name::model()->findAll(), 'id', 'name');

	return $nameArray;

}

public function getNameText()

{

	$nameOptions=$this->nameOptions;

	return isset($nameOptions[$this->name_id]) ? $nameOptions[$this->name_id] : "unknown name ({$this->name_id})";

}

etc...



and add this to my view




<h2>Credits</h2>

<?php $this->widget('zii.widgets.CListView', array(

	'dataProvider'=>$creditsCrewDataProvider,

	'viewData' => array( 'model' => 'film' ),

	'itemView'=>'/credit/_view',

)); ?>

etc...



Doing the above, viewing a single film took approximately 25 seconds. Should that take that long? Doing a similar query directly against the database in phpMyAdmin returns results instantly. So, I thought that maybe this is where Active Record should be left behind, and DAO should take over.

Reading hundreds of pages about DAO, I have not found a best practice on how to implement such a requirement. So, combining what I read from multiple sources and playing around until something worked, this is what I have now:

In the Film.php model




public function queryCredits($id,$is_cast)

{

	$cmd=Yii::app()->db->createCommand("SELECT mov_name_id, name, role FROM {{mov_film_credit}} mfc LEFT JOIN {{mov_name}} mn ON mfc.mov_name_id = mn.id LEFT JOIN {{mov_role}} mr ON mfc.mov_role_id = mr.id WHERE mov_film_id=:id AND is_cast=:is_cast ORDER BY mfc.order");

	$cmd->bindValue(':id',$id);

	$cmd->bindValue(':is_cast',$is_cast);

	return $cmd->query();

}



and in the view.php view file for film




<h2>Cast</h2>

<?php

$cast=Film::model()->queryCredits($model->id,'1');

foreach($cast as $credit)

{

	echo CHtml::link(CHTML::encode($credit[name]), array('/movies/name/view', 'id'=>$credit[mov_name_id]));

	echo " ($credit[role])<br />";

}

?>

<h2>Crew</h2>

<?php

$crew=Film::model()->queryCredits($model->id,'0');

foreach($crew as $credit)

{

	echo CHtml::link(CHTML::encode($credit[name]), array('/movies/name/view', 'id'=>$credit[mov_name_id])) . " ($credit[role])<br />";

}

?>



This solution returns the results instantly like I would expect. So, I’m happy that it works, but, it doesn’t seem right. I merely kept playing with the code until something worked (my newbie showing through). So I have a few questions about the first example using Active Record and this second example using DAO.

[list=1]

[*]Which approach is preferred for this type of scenario? If Active Record, why is it taking so long and how do I fix the speed issue?

[*]Assuming the second approach is necessary/desired for this scenario, what is the "correct" way to code this? Should that much logic be placed into the view file? Is the Film model (or any model in general) the best place for the select statement? Is the controller just not used in this situation? Should there be a separate "dao" directory where I just place all of these "queryCredits" style functions that I use throughout the site.

[/list]

I’m sure I will have more questions for the amazingly helpful individual that responds to this long post. All I ask is that people not merely reply with a post to another forum topic that I’ve probably already read. Here is a bonus question for anyone that wants to answer–which of the two options below is better?




$cmd=Yii::app()->db->createCommand("SELECT mov_name_id, name, role FROM {{mov_film_credit}} mfc LEFT JOIN {{mov_name}} mn ON mfc.mov_name_id = mn.id LEFT JOIN {{mov_role}} mr ON mfc.mov_role_id = mr.id WHERE mov_film_id=:id AND is_cast=:is_cast ORDER BY mfc.order");

$cmd->bindValue(':id',$id);

$cmd->bindValue(':is_cast',$is_cast);

return $cmd->query();






$credits=Yii::app()->db->createCommand()

	->select('mov_name_id, name, role')

	->from('{{mov_film_credit}} mfc')

	->join('{{mov_name}} mn', 'mfc.mov_name_id = mn.id')

	->join('{{mov_role}} mr', 'mfc.mov_role_id = mr.id')

	->where('mov_film_id=:id AND is_cast=:is_cast', array(':id'=>$id, ':is_cast'=>$is_cast))

	->order('mfc.order')

	->limit('25')

	->query();

return $credits;



To whomever decides to answer this post, thank you so much. If I could, I’d buy you a beer (or other beverage of choice)

hi justin.

I’ll try to answer your questions one by one.

1-Which approach is preferred for this type of scenario? If Active Record, why is it taking so long and how do I fix the speed issue?

it’s totally depends on your project specially your concurrent visitor. if you want to start up a new web site I advise you to develop your code as simple as possible and obviously active record is the simplest way to fetch your information from the DB.

but I’m agree with you 25 sec is too long for such a task. so let’s see what’s the main bottleneck in your code. profiling is your friend! please use some elegant extension available for you like yii-debug-toolbar please get it from github because it has a bug and I don’t know the new version updated in extension page or not https://github.com/m...archives/master

ok it give you so many helpful information about the execution time of each query and running time.

the next step is using Query Caching to speed up your queries http://www.yiiframew…a#query-caching

you can find out how to cache a query in active record in "Using Query Caching with ActiveRecord"

now you have passed the main bottleneck of your code according what you guess!

now assume that you want use DAO,

2-Assuming the second approach is necessary/desired for this scenario, what is the "correct" way to code this?

I explain you… continue :)

Should that much logic be placed into the view file?

no it’s better to reduce your logic but how?

here is a sample code of my recent project to get offers according category Id




$offers = Offer::getOffersByCategoryId($categoryId);

        $dataProviderOffer = new CArrayDataProvider($offers, array(

            'sort'=>array(

                'attributes'=>array(

                    'register_date', 'title',

                ),

            ),

            'pagination'=>array(

                'pageSize'=>20,

            ),

        ));



you can use CArrayDataProvider and use DAO. I think some [font=“arial, sans-serif”][size=“2”]lights turned on for you ;) now you can reduce the logic with a combination of two methods you have used to.[/size][/font]

[font="arial, sans-serif"] [/font]

Is the Film model (or any model in general) the best place for the select statement? Is the controller just not used in this situation?

yes the models is the best place to interact with database. make your model FAT and make you controller thin as possible as you can.

Should there be a separate "dao" directory where I just place all of these "queryCredits" style functions that I use throughout the site.

no, it’s recommended for heavy trafic web site to use DAO in fetching data and active record to insert/update. it’s fair enough :)

please feel free to ask any other question!

Simple Answer after using yii for a while.

In SQL you have two main Actions: Select and update/insert

For Insert I made that simple method in my base class:


public function saveNewAttributes($attr){

	$this->setAttributes($attr, false);

	$this->save(false);

}

Thats all. Nobody needs more.

For updates I made this;


public function updateAttibutes($attr){

	$this->setAttributes($attr, false);

	$this->save();

}

Thats all. Nobody needs more.

Selects are often much to complex to construct any stupid method based querys around them.

The only really usefull Tool is the bindParam/bindValue method to bring userinput secure in your Database - after validation of course.

And that I do simply:


public function createCommand($sql, $params = array()){

	$cmd = Yii::app()->db->createCommand($sql);

	foreach($params as $key => $value){

		$cmd->bindValue($key, $value);

	}

	return $cmd;

}

Thats all. Nobody needs more.

Build select querys old style and only format the user- or whatever input with the :name thing to bind it with bindParam or bindValue.

The rest is stuff for the controller except the request validation things and some really complex transactions.

Note that you don’t must use everything that looks modern or is made by a big pro. Keep cool and use that Stuff with sense. Cause there is much overdo in PDO, ORM and all that chit-chat.

Ramin! Thank you so much! Your reply is packed with useful information and it has already helped me greatly.

Here’s a few of the main points I picked up from your reply… Fat model containing the database interaction with a thin controller. For heavy traffic use AR for writing and DAO for reading. For what I am doing, I should start by trying AR for this item and if necessary (because of heavy traffic) switch to DAO.

Now that I’ve learned a few things, here is where I am. I installed the extension and removed some of my code just to test the ability to pull the film information and a list of cast. I found that with the DAO example I had 3 views, 6 queries (in 0.0025 seconds) and page load time of .05 seconds. With AR I had 13 views, 31 queries (in .3 seconds), and page load of 12 seconds. Te additional 10 views look like they are the “_view” file being called for each of the 10 cast records being displayed on the first page. Some of the additional queries look like they are selecting from two tables, each 10 times (again, for the 10 cast records I am seeing).

Now for the fun part. In my credit _view file, I have "$data->getNameText()" which relies on the following functions within my Credit model:




public function getNameOptions()

{

	$nameArray = CHtml::listData(Name::model()->findAll(), 'id', 'name');

	return $nameArray;

}

public function getNameText()

{

	$nameOptions=$this->nameOptions;

	return isset($nameOptions[$this->mov_name_id]) ? $nameOptions[$this->mov_name_id] : "unknown name ({$this->mov_name_id})";

}



If I remove the "$data->getNameText()" in the _view file and replace it with "$data->name_id" my page view displays IDs instead of names but I get the same 13 views, the queries drop from 31 to 7 ()in 0.0018 seconds, and a page load time of 0.05 seconds! So, it appears that the code snippets I included above are causing the slow down. Those pieces of code were borrowed from the Agile book and are meant to return the text value of a give record. For example, name_id 9038 is Christian Bale.

So, now for my question about speeding this up. Obviously, displaying just the ID of a record is pointless, I need the "name" field for that record. There has to be an easier and faster way to return that field for the ID that I have. I know it will obviously increase the number of queries I have again but as long as they are fast does it really matter? How am I supposed to return "name" for this "ID" as quickly and efficiently as possible.

Thanks again Ramin! Your reply has been such a huge help and I’m hoping with just a little more help I can figure out where I am going wrong and how to fix it!

you can do that in a far simple way!

fetch your information that you need to use in view by an extra join!

do not use "$data->getNameText()" at all.

if you place your code in _view, controller and model you used them for this part in here I can understand more about your situation but according to your information, I don’t know why you did not fetch all the information you need in @dataprovider. anyway executing a query for each render of _view is totally wrong.

Ramin thank you again! I focused on one part of your reply about including the data I needed in a join. With hint, I went back to AR to find a way to get this to work. I’ll include what I did in case anyone else stumbles across this thread and has similar questions.

THIS IS MY OLD APPROACH AND IS WRONG

I started in FilmController.php, reading data about any related credit records …




public function actionView($id)

{

        $creditsDataProvider=new CActiveDataProvider('Credits', array(

                'criteria'=>array(

                        'condition'=>'film_id=:filmId',

                        'params'=>array(':filmId'=>$id),

                ),

        ));


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

                'model'=>$this->loadModel($id),

                'creditsDataProvider'=>$creditsDataProvider,

        ));

}



… then I included some code in the Credits model to return the Actor/Actress name of the credit …




public function getNameOptions()

{

        $nameArray = CHtml::listData(Name::model()->findAll(), 'id', 'name');

        return $nameArray;

}

public function getNameText()

{

        $nameOptions=$this->nameOptions;

        return isset($nameOptions[$this->name_id]) ? $nameOptions[$this->name_id] : "unknown name ({$this->name_id})";

}



… and added to my view file …




<?php $this->widget('zii.widgets.CListView', array(

        'dataProvider'=>$creditsCrewProvider,

        'itemView'=>'/credit/_view',

)); ?>



… and in my _view file called the "getNameText" function to return the name of the actor/actress in the credit …




<?php echo CHTML::encode($data->getNameText()); ?>



With the help of Ramin and the Yii Debug Toolbar, I was able to remove unnecessary code from the model, modify my controller to return extra data, eliminate extra database interactions, and speed up page render/display greatly.

THIS IS MY NEW APPROACH AND IS BETTER

Here’s the new method. In the FilmController, I grab the associated Credits (the table that related films, names, and roles), and the actual name, film title, and role associated with each credit …




$creditDataProvider=new CActiveDataProvider('Credit', array(

	'criteria'=>array(

		'select'=>'name_id, role_id', // selects data from the film_credit table

		'with'=>array(

			'name'=>array('select'=>'full_name'), // "name" is the name of the relation defind in the Credit model. This array works just as the "criteria" of the data provider (see CDbCriteria) and can have its own select, with, condition, etc.

			'role'=>array('select'=>'role_desc'),

			),

		'condition'=>'film_id=:filmId',

		'params'=>array(':filmId'=>$id),

	),

));



… for the given film being requested, this Data Provider will provide the IDs for name and role (used in the view as links to those records) as well as the name of the actor and the role that actor played. Then, I simply changed my view to the following and all of that information is displayed as I need it, directly from this one selected rather than a new select to return the name and role from the database for each actor and role …




<?php

echo CHtml::link(CHTML::encode($data->name->full_name), array('/name/view', 'id'=>$data->name_id));

echo CHtml::link(CHTML::encode($data->role->role_desc), array('/role/view', 'id'=>$data->role_id));

?>



Things to point out: First, I modified the above examples from what I truly have in my environment just for purposes of showing how this works. I may have changed something accidentally or missed changing a name in the above examples so they may not work as expected if you try this code exactly. Second, I am sure this is not perfect and there are areas for improvement. However, this accomplishes all of my needs at this time and making the above changes had the following effect on various pages of my website used to view file and actor information:

When viewing a single film and its actors - Old method 177 queries, New method 13 queries

When viewing a single actor and his films - Old method 61 queries, New method 11 queries

For both, page load times are now down to 0.1 seconds or less.

Thanks again to Ramin for his replies that gave me enough information that I could fix this issue. Hopefully anyone else that is looking for this information will find this helpful.

Many thanks for these great posted questions. Quite valuable, methodically, and why not say it, scientific, a lot of material to study it further when the occasion asks for it.

Best regards and thanks,

D.