[SOLVED] Search for similar record before saving

I need to recognize, on saving a record, if a similar record has been created less than 15 minutes ago. If it has, I don’t need to save the new record, I need instead to load the similar record.

Use Case: A price is calculated (and some price records are inserted) upon the saving of a PriceRequest. A PriceRequest is valid for 15 minutes, subsequent requests should not be saved: the action of saving a new analog request will load the previous still valid request.

I have a working solution for this problem, but it is in the Controller (alas, in multiple controllers…):




//in controller

$request = PriceRequest::model()->findByAttributes(

    [...]

    ,

    'created > DATE_SUB(NOW(), INTERVAL 15 MINUTE)'

);


if (is_null($request))

{

    $request = new PriceRequest();

    [...]

    $request->save();

}



I want to move this logic to the model’s beforeSave, if possible.

Is it possible?

I can do




//in the model

protected function beforeSave()

{

    [...]

    //look for similar request made in the last 15 minutes. 

    $previous = self::model()->findByAttributes(array(

        [...]

        ), 'created > DATE_SUB(NOW(), INTERVAL 15 MINUTE)'

    );


    if (!is_null($request))

    {

        $this = $previous;

        return false;

    }

    [...]

    return true;

}



but obviously the $this = $previous does not work…

Can anyone help me?

you could just reinitialize $this by performig the database request or assigning the values to the attributes. but actually you cannot reset $this "pointer". actually




$this->attributes = $previous->attributes;



could do the trick, but i didn’t try. maybe you neet to set some other properties or maybe there’s a function in the model class to init one object by another but i’m not aware of it.

Omg! So obvious! Thanks!

I just have to do




$this->findByPk($request->id);



Many thanks!

[color="#FF0000"]Edit, 30 minutes later…:[/color]

No, it doesn’t work: the $this->id is not updated, and also the $this->isNewRecord is not updated.

Assigning attributes by $this->attributes = $previous->attributes; requires special care for unsafe attributes and stuff like that… I’m looking in the class reference for another viable solution…

Ok, I solved it (definitely, I hope):

To load the previous record replacing the actual I used this code:




$this->id = $request->id;

$this->setIsNewRecord(false);

$this->refresh();



This effectively change $this and loads into it the record I found. (It did not work with $this = $request, neither with $this->findByPk($request->id), neither with $this->attributes = $request->attributes)

Now, I thougt, the SQL INSERT would have become a UPDATE.

Wrong: Yii tries to INSERT the record, and throws an error because it tryes to insert a duplicate PK.

Turns out, as stated here, that issuing $this->setIsNewRecord(false); inside the beforeSave() is too late: Yii has already decided to do an INSERT.

The solution is moving the logic out of the beforeSave function, and into the afterValidate function.

This is the final code:




    protected function afterValidate()

    {

        parent::afterValidate();

        

        //look for a previous request with same attributes in the last 15 minutes

        $request = Request::model()->findByAttributes(array(

                'offer_id' => $this->offer_id,

                'from_date' => $this->from_date,

                'to_date' => $this->to_date,

                'set_products' => $this->set_products

            ), 'created > DATE_SUB(NOW(), INTERVAL 15 MINUTE)'

        );


        if (!is_null($request))

        {

            Yii::log('found old request still valid, id: ' . $request->id);


            $this->id = $request->id;

            $this->setIsNewRecord(false);

            $this->refresh();

        }

    }



Now, if a valid previous record is found, the record will be swapped for the still valid one, and saving it will result in SQL UPDATE.

In the onAfterSave, I will check if INSERT or UPDATE via $this->isNewRecord and I will pursue the request accordingly, requesting new prices only if the system saved a new request.

Thanks, Yiingeneur, and thanks to all.

But why are you saving again the same record ?

Because what I am trying to avoid is the creation of many similar records.

In the controller a PriceRequest is created and then saved.

Imho, the model should not answer FALSE to $request->save().

In my use case, issuing (saving) a new PriceRequest is resource consuming: it performs a CURL request and saves all the found prices in another table.

Updating a PriceRequest will just write the PriceRequest to the DB (I agree with you, this is unnecessary), but it will NOT ask for new prices to the system.

I differentiate between the two cases by looking at $this->isNewRecord in the afterSave function.

Also, I set the $this->created attribute in the beforeSave function, but only for new records.

I understand all you logic… but I don’t see the point in saving the same data again… this can be too a resource intensive operation (database saving)…

wouldn’t make more sense to have a method in the model like “validRequest($model)”… that would make your check (search for existing request)… and then if it returns true it would mean the request is valid and can be saved… if not nothing is saved…

You are absolutely correct.

So many optimizations, so little time!

The price request has to do with another entity (a REST service in another server) and trying not to hit that service too often is mandatory.

Avoiding the overhead of a UPDATE is an optimization that I’ll leave for later… there’s so many thing that I still have to do, optimizing now would just add layers of difficulty.