Data concurrency management in CakePHP - Behavior / Controller / Model?

453 Views Asked by At

In order to manage concurrency - that is ensuring that data being saved to the database is not stale or already edited by some other user - in my CakePHP application I am using the modified attribute in my edit functions. Below is a snippet of the code that is in my controller.

$this->MyModel->recursive = -1;
$event = $this->MyModel->findById($id);
$requestTimeStamp = new DateTime($this->request->data['MyModel']['modified']);
$dbTimeStamp = new DateTime($event['MyModel']['modified']);
if ($requestTimeStamp < $dbTimeStamp) {
    $response = array(
         'success' => false, 
         'id' => $id, 
         'message' => 'A concurrency error occurred while trying to save. Please try again');
    echo json_encode($response);
    exit;
} else {
   //... continue processing
}

This code works fine - but as I try to optimize it across my application I am trying to figure out where best to place it. Is it best placed in my AppModel class or is it better to create a Behavior for the same or is it just best left in the controller? I suppose that an ideal option would consider performance and minimize the amount of class loading overhead as well as database access overhead.

Has anyone come across / solved this problem before? Thoughts / suggestions appreciated.

1

There are 1 best solutions below

0
On BEST ANSWER

So I solved this by making concurrency check a part of my AppModel->beforeSave() method. Below is the code for reference of others

/*
 * Incorporated concurrency check in the beforeSave callback method to ensure that data is not stale before user saves.
 * The function checks if the model has a `modified` field, before it proceeds. If the model does not have such a method
 * then concurrency does not apply to this data structure. Upon proceeding, the method checks to see if the value of modified
 * data is the same in the database as well as the request that invokes this method. If they are not same then the save is 
 * aborted
 * This method requires the view or controller to pass a variable called data[ModelName][modified]. 
 * This variable must contain the value of the modified field when the record was read and it must be passed back as such. 
 * I usually set a hidden form field in my view like below - 
 * <input type="hidden" name="data[Model][modified]" value="<?php echo $model['modifed']; ?>" />
 */
    public function beforeSave($options = array()) {
        if ($this->hasField('modified') && isset($this->data[$this->name]['id']) && isset($this->data[$this->name]['modified'])) {
            CakeLog::debug('AppModel: beforeSave - inside concurrency check');
            CakeLog::debug($this->data);
            $this->recursive = -1;

            // run a select statement to ensure the modified date in the database has not changed. If it has changed then 
            // the below find query will return 0 rows
            $row = $this->find('first', array(
                'fields' => array(
                    'id', 'modified'
                ),
                'conditions' => array(
                    'id' => $this->data[$this->name]['id'],
                    'modified' => $this->data[$this->name]['modified']
                )
            ));

            // if no row is returned then error out and return - basically a concurrency error has occurred
            if (!$row) {
                CakeLog::error($this->name.':Concurrency error - [row-id:'.$this->data[$this->name]['id'].']');
                return false;
            }

            // if a row was retrned then there is no concurrency error, so proceed but change the modified date
            // to current timestamp to reflect accuracy
            $this->data[$this->name]['modified'] = date('Y-m-d H:i:s');
            return true;
        }
    }