Status update: ActiveRecord

@qiang:

In Yii1 I found it hard (= impossible) to completely implement things like a soft delete, optimistic locking and create/update timestamps (like CTimestampBehavior) through a behavior. This was because the onBeforeSave event was not fired in a method like CActiveRecord::saveAttributes. A similar thing happened with the onBeforeDelete event (not fired by, for example, CActiveRecord::deleteAll).

Is it possible in Yii2 to have these events (or a similar one) fire at all times when some kind of save or delete is done?

Of course the same goes for the onBeforeFind event, but that one was already complete.

I agree to Onman. In one of our applications we built our own ActiveRecord class (extended from CActiveRecord), which provides additional new events: onBeforeUpdateByPk, onAfterUpdateByPk, onBeforeDeleteByPk and onAfterDeleteByPk. It would be useful to have them available in the core. It allows to write an extended version of CTimestampBehavior which also works for saveAttributes() or updateByPk().

Some details on our implementation:

  • The above update events are fired from both: saveAttributes() and updateByPk()

  • If triggered from saveAttributes() the updated record is available in $event->params[‘record’].

  • In the other case there is no record object so $event->params[‘record’] is not set

OHHHHH :lol: Love it!!

Ok I know im being a bit picky but wouldnt it be nicer


$customers = Customer::find()->where(array('name'=>'customer1'))->all(); 

changed to


$customers = Customer::find()->all()->where(array('name'=>'customer1'))

I dont know it just makes extra sense to be. esp when im stuck with some error late at night and im reading the syntax out loud to myself ;D

iScotts

Can’t be done:




$customers = Customer::find()->where(array('name'=>'customer1'))->andWhere(array('status'=>Customer::STATUS_APPROVED))->all();



I like it very much.

One question: “@.” and “?.” are not mnemonic. I know I’ll have a hard time remembering which is which. Maybe you could consider something a little more descriptive. e.g. “@this.” and “@that.”

You can remember them with associations. "@" is "at" and could be treated as "self", "me" or "this" model/table. "?" is question and could be treated as "other", "else" or "another" model/table.

Just like "users" in "actionRules" in 1.*. :slight_smile:

Do you mean like the weird symbols for users in CAccessControlFilter::$rules? I never managed to learn those and switched to using roles instead.

It’s so odd. Yii in general uses very descriptive terminology, which is great because it saves me a lot of time. Why in these corner cases are cryptic symbols used? I get the need for something that’s unlikely to collide with a table name but there are other ways to do that.

Overall, I like most of the changes! Most of the query stuff is nice too, I think it might be easier for newcomers to understand it as well.

There are however a couple of things in this that I dislike to the point that I feel I must say so.

1) The @ and ? for the self and foreign table

First of all I think these are non-intuitive. The closest thing to intuition I can find with them is that @ can be thought of as “home” which can then be like “self”. But other than that, I can’t see how anyone thinks of “self” when they see an @, and even less how they can think of “foreign”/“the other” when they see a ?.

To compare with the Yii 1.1 way, I think “t” for the current table is much better, and I wouldn’t mind using “f” for the foreign table. This also makes one less change, why move to @ when we’re already using “t”.

Another downside to @ and ? is on a note similar to how PHP seems to be running out of special characters for their language syntax. I really hate their choice of \ as the namespace separator, I think it is insanely stupid (in the context of language syntax and as a user). I don’t know if the main reason for that choice was that it was the least bad of the few special chars they had left to choose from, but it’s something I’ve heard mentioning more than once.

In any case, instead of hogging two new special chars for the self and foreign functionality we need here, why not settle on one special char and use that as a signifier for more than one special functionality. For example; Use "@self.foo" instead of "@.foo" and "@foreign.foo" instead of "?.foo". This will let us use other @<whatever> in the future as well, if we need to, without having to figure out yet a new special char for our new purpose.

2) The new relation key syntax, "orders:Order[]"

While I can see that there is a certain value in how we as humans and used-to-PHP-syntax programmers can read that and immediately see that "orders" is a relation to multiple Order models, I think that is not worth the fact that we are taking perfectly well structured meta data/definitions from an array and squeezing it into a simple string. What I am talking about is the change from for example:




'orders'=>array(self::HAS_MANY, 'Order', 'customer_id'),



to:




'orders:Order[]'=>array(

    'link'=>array('customer_id'=>'id'),

),



Sure, it is in one way more readable, but do we really want to end up with code/definitions in strings instead of in your nicely structured PHP code?

Also, we still need to use an array (because the ‘link’ option is required), so it’s not like we do this as part of being able to specify a nice array of values only, like array(‘orders:Order[]’, ‘primaryContact:Contact’). We still need the array, and all we’re doing aside from getting a little readability is lowering the amount of structured code in our application. We are practically taking some of our PHP code and putting it in a string. I just don’t think that’s a pretty thing to do.

Thanks!

Question about relational saving

What is the state of this? In short, one of the most common questions we have in #yii@freenode is how to work with and save related models. The currently most common response we give them is that they do one of the following:

  1. Do it manually by creating a model class for the join table (in the case of MANY_MANY) or simply setting the foreign key attribute on the relevant models and saving it. Naturally one also want to use transactions.

  2. Use CSaveRelationsBehavior - this has the upside that you can give it primary keys which it will save, and that it generally works really nice and isn’t badly written - it’s downside is that it uses INSERT IGNORE INTO, hence it is MySQL specific.

  3. Use with-related-behavior - this has the upside that it is written by Yii core developers and we have been hoping that it or some form of it is a candidate for inclusion in Yii2, and it isn’t database specific - a downside it has is that it doesn’t let you give it primary keys which it then saves, you need to give it models.

Regarding my talk about being able to give primary keys instead of model instances to the extensions in #2 and #3 above, well that is simply important because you often get primary keys from a form, for example if you created a form where you select one or more related models that you want to bind to the current/main model. You then use for example checkboxes with values representing the primary keys, and then when saving you want to simply hand those primary keys to the saving stuff, instead of first having to look the related models up to be able to give them to the saving, which I think you currently need to to when using with-related-behavior. You don’t want those extra queries.

In summary, I think that support for saving/adding/removing related models for all types of relations is something that has a great value and should be on the roadmap for Yii 2.0 release. A great part of this opinion is based on what I feel that newcomers ask for.

I agree with rAWTAZ, especially in point 2.

I’d like to see less strings and more arrays.

Agreed again. This is a problem I see a lot among Yii newbies.

I even wrote giix with this in mind.

rAWTAZ, There’s no MANY_MANY in Yii2 ;)

Roger that :slight_smile:

MANY_MANY was just mentioned to give an example of what we answer when users ask how to save and update related models. They ask for HAS_MANY as well (though MANY_MANY are indeed the most common one).

I think people would love being able to do $model->relationFoo=arrayOfPksOrModelInstances; $model->save(); or something similar. Whether the amount of work involved in making this happen is reasonable for this or not, given that there are no MANY_MANY in Yii2, I don’t know. But maybe most of this is already done in with-related-behavior, and if you can just make it accept primary keys as well as models, it might be almost done? :)

Considering that Yii2 AR internal design is different we can only look at its syntax and ideas. Anyway, it would be a great feature and we’re definitely interested in it.

I personally consider this as must rather than a feature. Feature would be the ability of turning this off/on on dynamic on occasions. I really insist that this is a must. There are also 1 or 2 more things that would be nice.

One of them is “dirty attributes” (I don’t know if this was mentioned before so excuse me If I bring old news ).

So, it would be convenient to know which attributes have changed since my model was fetched and act accordingly before/after save ( even CActiveRecord::save() function could only save the really changed values rather all the model attributes as it does now ).

This is something missing and I am pretty sure it’s easy to implement by a user of Yii or a behavior somewhere exists for sure. Ror does already and once it seems that the yii ar wants to share the same “philosophy” this could be a nice build in feature.

For the relations string now. The only opinion I can give is I don’t like the mix of key-value (…‘condition’=>’…’), with plain values (…self::HAS_MANY, ‘ModelName’ ) in the Yii 1.1.0 relations array. It’s to so structured after all. Simple example of failure




<?php

class Foo extends CActiveRecord

{

    public function relations()

    {

        return array(

            'user' => array(self::HAS_ONE, 'User'),

        );

    }

}

class Bar extends Foo

{

    public function relations()

    {

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

            'user' => array(self::HAS_ONE, 'User', 'condition'=>'1=1'),

        );

    }

}



I know it looks rare, or there should like be written differently but it happened. :)

Someone could say the following is “too much” typing or its not “convention over configuration” or whatever, bottom line is that’s straight forward what it does. I don’t have opinion what is the best approach on this really.




      public function relations()

      {

           return array(

               'users'=>array(

                   'relation'=>self::HAS_ONE,

                   'class'   =>'User',

               )

           );

      }



This is in AR in 2.0 as I understood it, see Qiang’s initial post in this topic.

Good catch, this might be something people run into. I’m not sure it’s a practical problem though, because what you would do is use array_merge() instead of CMap::mergeArray(). At least if you are fine with the second relation overwriting the first one. Maybe you are not, I just don’t see an obvious use case where you are defining a relation in both a parent and child class. Maybe there is one :) Another way to solve it could be with a foreach(), but I agree it might not be perfect for the eye.

Irrelevant to the topic. The issue occurred when a colleague introduced the relation in the Foo after it was declared in Bar and did not picked it up from there (bad practice of course).

Problem with array_merge is that you lose the recursive. Example:




Class Foo extends CActiveRecord {

    public function relations() {

        return array('users'=>array(self::HAS_MANY,'User','scopes'=>array('test1')));

    }

}

class Bar extends Foo {

    public function relations() {

        return array_merge(parent::relations(),array(

            'users' => array(self::HAS_MANY, 'User', 'scopes'=>array('test2'))

        );

    }

}



It could be probably expected that both scopes would apply but this will not happen. And even if we have "resolved" the issue with a syntax like:




Class Foo extends CActiveRecord {

    public function relations() {

        return array('users'=>array('relation'=>self::HAS_MANY,'model'=>'User','scopes'=>array('test1')));

    }

}

class Bar extends Foo {

    public function relations() {

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

            'users' => array('relation'=>self::HAS_MANY, 'model'=>'User', 'scopes'=>array('test1','test2'))

        );

    }

}



What would happen with the scope ‘test1’? Wouldn’t this apply more than 1 time in the query? ( I have not tested but I am guessing it will happen (correct my if wrong).

Something closely related: I’ve been experimenting with dirty attributes in ActiveRecords lately. The idea is to only save attributes to the DB that actually changed. This is most benefitial for indexed db-fields, but tends to relieve the database from some unnecessary traffic. Is there any chance something comparable will make it into Yii 2?

See http://www.yiiframew…post__p__145514

Oh, heh. How could I’ve missed that :lol: