Active Record

In my opinion, the entire DB package is a good candidate for a package - the ability to use alternative storage solutions is becoming an increasingly relevant topic every day.

I think I would argue that an entire feature, such as Yii’s database abstraction layer (of which AR is a subset) is a great candidate for something optional.

I agree, and I would not argue for distributing Yii without the DB/AR package.

What I would argue though, is that if it came in the distribution in the form of a package, that makes it more obvious to everyone that I have a choice - so I can feel confident hitting the delete key and pulling in an entirely different storage layer. A lot of professionals will prefer Doctrine, for example - and in the future, increasing number of developers will probably prefer a graph/document-database.

I think it’s important to show that we’ve thought of this, and to clarify that nobody is forced to accept every aspect of Yii for every aspect of development.

I don’t expect to see Yii’s storage layer become something you can “replace” from underneath an existing application. But it would be nice if there were no direct ties between a basic web-application and the built-in database abstraction. It’s a matter of choice.

I already use a graph database in my project (Neo4j) because it is the only sane way I know to model complex relationships. I created my own ActiveRecord like class to talk to the database and it was a real pain to do that as AR is so deeply rooted within the SQL world. So thumbs up for an abstraction layer to allow different storage types.

Anyways, I hope it’s not too off topic but another AR-like feature that I personally think is very interesting is the ActiveRESOURCE approach Rails provides (http://api.rubyonrails.org/classes/ActiveResource/Base.html). This could be interesting in terms of SOA as different Yii applications would be able to communicate via a standardized REST layer.

+1 for ability to remove DB access layer completely and replace by Doctrine.

Yii AR is small and efficient, but there are intranet projects with other requirements.

I miss collections that support and optimize multiple rows and relations.

$PostCollection = $new PostCollection;

foreach ($data as $row) $PostCollection->addRow($row);

$PostCollection->save();

$PostCollection->Users->update(…)

I like this API, but in some cases (like receiving data from a form), you will need to delete-save (or other better approach).

I would like to see support for automatically validating & updating related Models.

A example:





class Post extends ActiveRecord {

 function relations() {

  return array('comments' => self::HAS_MANY, 'Comment', 'post_id');

}

}


$model = Post::model()->findByPk(5);

$comment = new Comment;

$comment->author = Yii::app()->user->id;

$comment->title = $title;

$model->comments[] = $comment;

$model->save();



This would save the Post model, and as well will save the

comment and attaches it to the post, by setting the post_id to

the primary key of the post model.

Multiple assignments would also be possible, either by passing

object instances or just pk’s of the foreign objects.

Validation could also be done something like this:


function rules() {

 return array('comments', 'min' => 3, 'error' => 'In order to save this post, it needs at least 3 comments');

}

There are three implementations for this, from which

i recommend the first two (i never looked at the third):

http://www.yiiframework.com/extension/cadvancedarbehavior

http://www.yiiframework.com/extension/save-relations-ar-behavior

http://www.yiiframework.com/extension/saverbehavior

What do you think about this idea?

I think it would be difficult to implement at framework-level in a way that satisfies everyone’s needs.

It’s pretty clear in your example that you want to add the new comment to existing ones, but what do you think about this:




$post->tags = array($tag1, $tag2);

$post->save();



Should the new tags be added to existing ones or should they replace the old ones?

I’d prefer to handle it at application level - with the help of available extensions.

EDIT: Sorry, it’s already solved by samdark’s proposal :




$post->tags->add($tag); // add new tag

$post->tags->set(array($tag1, $tag2)); // (or similar syntax) replace tags



Let me add my 3 cents:-)

  • current Yii’s AR is very well implemented. It has good API and excellent performance. One of the reasons I opted for Yii. It should continue this way - being lightweight cross storage compatible layer with caching options.

  • replacing with doctrine - no way ! - huge performance degradation.

  • AR should be part of the core. It takes 400 - 500 kB unzipped code, which is irrelevant.

  • API should stay backward compatible as much as possible. It is intuitive and it would ease migrations.

What can be improved regarding AR:

=================================

  • abstracting the storage NOT to be bound only to PDO. SamDark’s points 1 + 2.

  • fixing problem with column quoting. Especially the Criteria::where, AR::where conditions - this is currently one of the biggest flaws in version 1.1.X.

  • cross portability should be tested before each release by running standardized CRUD unit tests for EACH supported database. I know it’s a pain to set up environment, but would undisclose great part of bugs reported currently for Yii issues regarding AR. If it would not be possible to test all supported storages then it would be good to distribute AR unit tests with core so that users can test it easily.

Lubos

In my opinion Yii AR works very well. I’ts well balanced between not trying to accomplish too much while still dealing with most everyday problems in a nice way.

Yii AR should be a part of the core. As qiang stated, neither space nor IDE performance should be a problem to anyone. Having optional packages is just going to confuse new users more than necessary.

My proposals for AR:

  • Methods should have precedence over db fields. "getName" should be invoked by "->name", even though "name" is also a field in the db.

  • When attributes are changed, their old value should be saved (for trails or other uses), and there should be kept an index with "dirty" attributes allowing AR to only save "dirty" attributes on save.

  • There should be kept an index of all AR models, so that the same model (same primary key) can’t be represented by to or more instances. E.g. $a = AR::findByPk(1), $b = AR::findByPk(1), $a->field = ‘changed’, $a->field === $b->field.

I agree with that.

Altho, it should abstract some classes so users can have an option to create their own extension when needed

Something like this looks fine to me

CConnection as an abstract class that connects with any storage type, with a couple abstract common methods, like connect, executeCommand, openConnection, closeConnection, etc

CDbConnection extending CConnection having specific pdo/sql methods

CAbstractActiveRecord extending CModel, with a couple abstract common methods, like save, find, delete, insert, findAll

CActiveRecord extending CAbstractActiveRecord, that uses CDbConnection and have specific sql methods like findAllBySql, etc

this way users can having something like

myConnection extening CConnection

myActiveRecord extending CAbstractActiveRecord, that uses myConnection and have their own specific command to find, save, delete, etc

What do you think ?

Personally I think that the KISS principle should be respected as much as possible. It’s too easy to make an abstract on the abstract on the class exteding throught 2 more parent classes.

PHP is not Java - some things need to be simple and straight forward.

Mixing SQL and NOSQL can be done to some degree in Active Record, but reality is - you will gain nothing then. The power of the NOSQL is that there is no structure at all and linking between the entities can be very complex. Not to mention the fetching of the data.

There should be a very careful thinking through process and careful design. And if it doesn’t fit together - screw it - just make 2 sets of models - Active Record for SQL and NOSQL models and make them able to link together via relations.

Some things I’ve done in my custom Yii version:

  • CModel child / parent relationship - CModel has 2 new properties "parentModel" and "parentAttribute" - with this, I can build a tree for saving inner models with a save on the parent.

  • CModelCollection: base class for model collections. It is simply a CModel in what each "property" is a CModel. It has a "save" method that saves all models within it. All relations that returned an array now returns a CModelCollection, and a custom CModelCollection class can be specified on the relation parameters.

  • In CActiveRecord properties defined as "safe" in rules() that are CModel are saved together with the parent model. I implemented some more checkings for saving order (saving HAS_MANY after the main model as to have the base model autoinc ID ready).

It’s a little more complicated than that, but these are the basic ideas.

remember that static methods make unit testing really hard

It would be nice if AR does not need a "model" method to be called:




$something = SomeModel::find(...);

// instead of

$something = SomeModel::model()->find(...);



It could be easily solved with a get_called_class() (PHP 5.3).

I concur.

But if these methods are still required, at least take them out from the gii generated models and put them into the base class.

For example, this is what I use:


    

class ActiveRecord extends CActiveRecord

{    

    /**

     * Returns the static model of the specified AR class

     * @param string $className

     * @return object the static model class

     */

    public static function model($className = "")

    {

        // gets the input class name, or the called class name (aka, the child class)

        return ($className) 

            ? parent::model($className) 

            : parent::model(get_called_class());

    }

    

    /**

     * Gets table name based on class name

     * @return string the associated database table name

     */

    public function tableName()

    {

        // gets the called class name

        $calledClassName = strtolower(get_called_class());

        

        // adds in table prefix if it is set

        return (Yii::app()->db->tablePrefix) ? '{{'.$calledClassName.'}}' : $calledClassName;

    }

}

My biggest gripe with the current implementation, is the fact that persistence management is mixed into the model type itself.

What I mean is, Post::model() actually returns an instance of Post - some kind of reserved singleton instance of the model-type, with special privileges and responsibilities.

It feels wrong.

I would like to see the repository manager untangled from the model entity.

For example, where you have this:




$post = Post::model()->findByPk(1);


$post->title = 'New title';


$post->save();



I would prefer to have something like this:




$post = Yii::app()->posts->findByPk(1);


$post->title = 'New title';


Yii::app()->posts->save($post);



Yii::app()->posts in this case is the dedicated repository manager for the Post type, which is a pure model-type without the storage aspects blended into it.

Basic CRUD repository operations can be standardized: findByPk(), save() and delete().

The rest can be specific to the storage implementation. The PDO/SQL implementation would support all the usual stuff, findAll(), findFirst() etc. with the usual bits and pieces of SQL-expression where needed. A third-party graph or document storage engine would have it’s own dedicated API.

The point is not to try to support multiple engines with one API, except for the basic load/save/delete for individual entities; the idea here, is to separate concerns.

Think about other potential storage engines, such as a flat-file storage engine for configurations, or a cache-provider for some key/value engine. A cache is just another kind of repository.

On a related note, PHP 5.4 has real support for mix-ins in the form of "traits" - which would make it possible to have the same semantic syntax as before, e.g. $post->save() or $post->delete(), rather than the elaborate Yii::app()->posts->save($post); but with the important difference that the save() and delete() methods are not methods of the Post class, avoiding this ugly static dependency for the storage aspect…

I bugs me a little, too.

This is a problem with Active Record itself and with MVC.

There could exist something like P+MVC (or PMVC), where the persistence should explicitly be separated from the domain model.

The decoupling in this architectural pattern would create a better abstraction and more flexibility.

The downsides are some additional complexity (not much, in my opinion) and a new learning curve (something that may be troublesome for Yii 2.0).

Globally the AR we have in 1.1.x is just fine in it’s implementation, just missing related model management (saving, deleting and so on) and multilingual functionality.

What really bugs me (and at work we have stumbled on this) is that PDO is quite restrictive in it’s feature set. We work with MySQL only (5.5) and we really are considering using a wrapper for the MySQLi extension to replace the PDO because we are in need of features the extension providers (*_ping, *_fetch_array, *_multi_query, even some async queries would be helpful). And our project isn’t something that PHP is unfit to deal with - it does it’s job perfectly and Yii makes that job even pleasurable. Just inability to use full DB functional due to the PDO not providing ability to use DB specific functions and being stuck in it’s development as PHP devs have acknowledged.

Some time without no post here, but I want share some notes.

I like the AR Yii approach, but right now I feel that need some extra stuff.

I has use Doctrine 2. I like it too, but its so complex… well, anyway Doctrine 2 have nice things.

Anyway, following some line like midplay, samdark, Rangel Reale… I think the next AR implementation on Yii must have some little Data-Mapper approach. This for separate the logical from the Model class to a Repository/Store/Storage class.

I see the AR model class/object like a ToyStory alien… but right now we do not have “The Claw”. And the claw must have the function for CRUD operation over the ToyStory aliens. :)

So for the next AR implementation on Yii 2 I want somthing like a Claw :)




$theClaw = Yii::app()->db->storage;


$alien = $theClaw->findByPk('Alien', 1);


$alien->eyes = 3;


$theClaw->save($alien);


//or


$dbCriteria = new dbCriteria(...);

$manyAlliens = $theClaw->find('Alien', $dbCriteria);


//or like repos


class ClawForAliens extend BaseClaw{

  public function findByXYZ(){

    $dcCriteria = ...;

    $moreAliens = self::findByCriteria($dcCriteria, 'array');

    return $moreAliens;

  }

}


$aliensClaw = new ClawForAliens();

$moreAliens = $aliensClaw->findByXYZ();



PD: And will be nice more commands for AR from the yiic cli :)

What I will love to see into Yii 2 AR:

  1. Separate Model and ModelManager (or ModelService or whatever you like to call it)



$post = PostManager::getByPk(1); // get always return signle Post instance

$posts = PostManager::findByPk(1); // find always return Posts collection

$post = PostManager::getByCustomCriteria();



I mean - model should incapsulate directly related to model instance stuff. Manager should care about how to find model(s), or some other domain logic.

  1. Support model collection class



$posts = PostManager::findAll(); // will return PostCollection

$posts->delete(); // delete will perfomed to each posts into collection

$posts->add(new Post());

$posts->someCustomMethod();

$posts->save(); // save all posts into collection

$posts->filterByAttributes(array('type' => Post::PENDING));


$posts->

  each(function($post, $index){

    $post->type = Post::APPROVED;

  })->

  save(); 



  1. Save model with relations



$post = PostManager::findByPk(1);

// append comment ([] alias of CommentCollection::add())

$post->comments[] = new Comment();

// delete all comments

$post->comments->delete();

// add new comment

$post->comments[] = new Comment();

$post->save(); // will save post with comments



  1. Cover all things with events.

For example there is no onUnsafeAttribute event now. But sometimes it could be very usefull into behaviors.

  1. Allow to attach event handlers to models before they were actually created.



PostManager::attachLiveEvent('afterSave', ...);


$post = PostManager::findByPk(1); // will have onAfterSave handler

$post = new Post(); // will have too onAfterSave handler



That will give developers apportunity to create observers which care about attaching to needed events by their own.




class MailObserver extends CComponent {


  public function init() {

    PostManager::attachEvent('afterSave', array($this, 'onPostSave'));

  }


  public function onPostSave($event) {

    // send email to admin

  }

}



This is soooo important. I cannot second or third or even fourth this enough. Please add this!

To clarify: Make setter and getter methods have precedence over both regular attributes and relations.

Shall I say it again? Please add it! :slight_smile: