possibility to update primary key values via active record

See my post in the discussion:

http://www.yiiframework.com/forum/index.php?/topic/4445-set-primay-key/page__p__23700&#entry23700

Currently its not possible to update a primary key via active record.

Because the update function uses this the primary key value directly from the attributes via the function $this->getPrimaryKey(), which aren’t the original from the database if changed via a update form.

the code from the update function:

$this->updateByPk($this->getPrimaryKey(),$this->getAttributes($attributes));

I would use add a private Property to store the primary key after load/find and use this stored value(s) for the update instead of the key values from the attributes.

Then we can easily add the primary key fields to the safe attributes (if wanted) and can edit and change the values.

the way i am currently changing my primary key values is directly in the controller:




public function actionUpdate()

{

	$model=$this->loadProject();

	if(isset($_POST['Project']))

	{

		$key_before=$model->getPrimaryKey();

		$model->attributes=$_POST['Project'];

		$key_after=$model->getPrimaryKey();

		if($key_before!==$key_after)

		{

		    if($model->validate())

		    if($model->updateByPk($key_before,$model->getAttributes()))

		    {

		       $this->redirect(array('show','id'=>$model->ID));

		    }

		}else

		{

		    if($model->save())

		       $this->redirect(array('show','id'=>$model->ID));

		}

	}

	$this->render('update',array('model'=>$model));

}



But this isn’t that “good”, cause I don’t call any function which normally called in save() e.g. after save etc. So if i add something to e.g. after change I also have to remember to call this function in the the code part above.

You should virtually never change the primary key of a row in a database. Why would you want to do this?

Why shouldn’t I?

There is nothing really special about it. I have a varchar(9) primary key which is some kind of project short name e.g. P1204 and some other without any numbers.

The main problem is, this isn’t my database (so I cannot change anything) I am working with and those key’s are used in several relation tables.

I also see nothing really "special" on a unique varchar primary key which is also set to be updated on cascade with an InnoDB database. Not everyone uses a autoincrement key and some varchar key is sometimes better readable when you look directly into the tabledata. I guess the old developer was just too lazy to do some queries when displaying the relation data.

The problem is, a wrong sql update command can be created if I add the primary key to the safe attributes.

As its a “normal column” why shouldn’t I also be able to change it?

I mean cause its a hand inserted "text" there could be any cause for a change of this key. Beginning from an easy mistyped on insert to an change of the project (in my case).

Sadly I just have to do it, my own database schema wouldn’t also use varchars as keys.

I think this is a very special case that should not be handled by AR. As the SQL for updates on tables without Pk depend a lot on the table structure you will always end up with custom SQL. How else would you make sure that the row that you want to update is uniquely identified?

But you still can enhance your ARs with some custom methods. They can use DAO to perform these updates according to your logic. Something like this:


public function updatePk()

{

    $command=$this->db->createCommand('UPDATE bla SET id=:id WHERE colA=:a AND colB=:b');

    $command->bindValue(':a', $this->a);

    $command->bindValue(':b', $this-><img src='http://www.yiiframework.com/forum/public/style_emoticons/default/cool.gif' class='bbc_emoticon' alt='B)' />;

    $command->bindValue(':id', $this->createNewPk());

    $command->execute();

}




public function rules()

	{

         return array(

          array('ID', 'unique'),

       );

}



Solves the problem with uniqueness. On validate() fails if the new ID is already used.

The only thing that has to be changed is store the original key on Load/Find. And use this stored key for the update! And not the one from the attributes. Because you would also have the same problem "wrong sql" when you set the model->primary key attribute by hand and try to save afterwards.

My code from above works, its just not that nice. And I think it should be changed in the yii framework to be able to change the primary key as well.

I mean currently it gets the key from the attributes, so it can also get the keys from some private property which is only filled on load of an entry, same as the direct attribute, only just not accessible and changeable from the outside.

To make sure that you understand where my code from the 1st post is used I added some more code to the 1st post (hopefully right because I just typed it, but its normal crud code)

the only problem is really that i get

UPDATE tbproject SET ID=‘mynewkey’, … other columns… WHERE tbproject.ID=‘mynewkey

instead of

UPDATE tbproject SET ID=‘mynewkey’, … other columns… WHERE tbproject.ID=‘myloadedkey

when I try to edit the ID, which is just added to save attributes when calling $model->save(). If I leave the ID unchanged the save() works as it should, but on change of the ID it will use the new submitted ID for both new key value as well as the key to change, which is wrong!

Sorry, i think i mixed this up a little with the other topic “tables without Pk”. That’s why i talked about uniquely identifiying a table row. :)

Don’t know about your issue though…

I personally like "natural" primary keys much more than those automatically incremented IDs. I feel like introducing such a generated ID is more like a very common and widely accepted workaround.

Granted, it has its positive aspects. It never has to be changed, it is a general approach that fits every table, you don’t need to worry upon updates and foreign keys (if it really won’t be changed), and so on…

But on the other hand, if you do proper analysis of the data you have to store, you will (in most cases) find a real, a natural identifier of your data that makes every sort of generated ID redundant. That real Pk will describe your dataset in a more precise way than an id could do, thus making it easier to “read” tables if you ever need to look at them (and while developing, I do it again and again). You don’t blow up your database with unnecessary integer values (all you need those IDs for is for referencing your data, never for the data itself).

Of course one can argue that it is hard to find “real” identifiers for real world data. Users for example might be identified by their names. But we all know that there are many people that share the same name. So this might not be sufficient. But as soon as you introduce an unique column (maybe because you don’t want users to share the same nick because that could confuse the community - only an example) you found the column that can serve as primary key. There’s no more need for any strange numbers in your tables.

That’s why I highly appreciate this feature request.

totally agree on that, and as I started using yii for the first time I stumbled on this problems, so maybe this feature request should have the highest priority, IMHO of course!

I’ve overcome this issue quite simply by making the following changes:




class MyModel extends CActiveRecord

{

  private $_originalKey;


  public function afterFind()

  {

    $this->_originalKey = parent::getPrimaryKey();

    parent::afterFind();

  }


  public function getPrimaryKey()

  {

    return $this->_originalKey;

  }

}



The CUniqueValidator must also be changed like so:




Index: validators/CUniqueValidator.php

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

--- validators/CUniqueValidator.php	(revision 1457)

+++ validators/CUniqueValidator.php	(working copy)

@@ -82,7 +82,8 @@

 		if($this->criteria!==array())

 			$criteria->mergeWith($this->criteria);

 

-		if($column->isPrimaryKey || $this->className!==null)

+		//if($column->isPrimaryKey || $this->className!==null)

+		if($this->className!==null)

 			$exists=$finder->exists($criteria);

 		else

 		{



I haven’t fully explored the else statement after this, but I’ll post any updates if I run into issues.

A lot of implementation issues around keys can be resolved quite simply by removing the special handling for primary keys, and treating them as any other key or index. The onus is then on the application to ensure that when you’re updating or deleting that you’re doing it to the subset (or individual record) you want.

You also need to add this method to the class to ensure that the original key is changed to the new value when the record is saved:




  public function afterSave()

  {

    $this->_originalKey = parent::getPrimaryKey();

    parent::afterSave();

  }