I need your help using transactions in CakePHP.
I have a Product model, with clause hasMany to Price and Property models (key product_id).
In my Product model, I add
function begin() {
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$db->begin($this);
}
function commit() {
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$db->commit($this);
}
function rollback()
{
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$db->rollback($this);
}
And in ProductController I use save() to save my Product, and then my Price and Property. (I use only save(), not saveAll() ).
My code is:
$this->Product->begin();
$error = false;
if($this->Product->save($data)
{
//my functions and calculations
if(!$this->Price->save($data_one)
{
$error = true;
}
//calculations
if(!$this>Property->save($my_data)
{
$error = true;
}
}
if($error) {
$this->Product->rollback();
}
else
{
$this->Product->commit();
}
The problem is that if I have an error inside the save Price or Property row, the Product is still added. I would have thought that when I have any errors, none of my rows would be added (i.e. a rollback would delete it).
I am using CakePHP 2.3.8
Tables must be in InnoDb format. MyISAM format of tables doesn't support transactions.
No need to insert additional code into model.
ProductController:
$datasource = $this->Product->getDataSource();
try {
$datasource->begin();
if(!$this->Product->save($data)
throw new Exception();
if(!$this->Price->save($data_one)
throw new Exception();
if(!$this->Property->save($my_data)
throw new Exception();
$datasource->commit();
} catch(Exception $e) {
$datasource->rollback();
}
Related
I want to delete data through the api but it is showing an error because it is necessary to delete records in the properties table.
SQLSTATE[23000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server] The DELETE statement conflicted with the REFERENCE constraint "FK_Interests_Properties_User". The conflict occurred in database "CADASTRO", table "dbo.Interests_Properties", column 'interests_user_id'. (SQL: delete from [Interests_User] where [user_id] = 1515626)
I created the model that performs actions on the Interests_Properts table, but when I try to delete the data that has the same interests_user_id, errors are occurring.
InterestsUser.php:
use App\Source\InterestsProperties;
class InterestsUser extends Model
{
protected $connection = 'sql_cadastro';
protected $table = 'Interests_User';
protected $primaryKey = 'id';
public $timestamps = false;
public function properties()
{
$this->belongsToMany(InterestsProperties::class, 'foreign_key');
}
public static function lgpdInterestsUser($id_user, $action)
{
if ($action == 'search') {
$data = InterestsUser::where('user_id', $id_user)->get();
if (count($data) > 0) {
return $data;
} else {
return false;
}
} elseif ($action == 'delete') {
$data = InterestsUser::with('properties')->where('user_id', $id_user)->get();
foreach ($data->properties as $p) $p->delete();
if ($data > 0) {
return 'Success Remove.';
} else {
return 'Not Found.';
}
} else {
return "Action Incorrect!";
}
}
}
InterestsProperties.php
class InterestsProperties extends Model
{
protected $table = 'Interests_Properties';
protected $primaryKey = 'id';
public $timestamps = false;
}
The error is occurring when trying to remove with cascade:
Call to undefined method Illuminate\Database\Eloquent\Builder::foreign()
Table Structure
Interests_User
id
interest_id
user_id
created_at
updated_at
Interests_Properties
id
interests_user_id
key
value
created_at
updated_at
At DB level, using onDelete: when migrating your InterestsProperties model you'll have a line like
$table->foreignId('foreign_key')
to that add ->onDelete('cascade')
after that update each time you delete a record from the main table it will do it in this one to.
At PHP level,
$data = InterestsUser::with('properties')->where('user_id', $id_user)->first();
foreach($data->properties as $p) $p->delete();
PD: Those will remove the property record too.
I have to help me convert an entity to array but I have issues resolving associated records, which I need.
However, this gives me an error
The class 'Doctrine\ORM\PersistentCollection' was not found in the
chain configured namespaces ...
The code follows:
public function serialize($entityObject)
{
$data = array();
$className = get_class($entityObject);
$metaData = $this->entityManager->getClassMetadata($className);
foreach ($metaData->fieldMappings as $field => $mapping)
{
$method = "get" . ucfirst($field);
$data[$field] = call_user_func(array($entityObject, $method));
}
foreach ($metaData->associationMappings as $field => $mapping)
{
// Sort of entity object
$object = $metaData->reflFields[$field]->getValue($entityObject);
if ($object instanceof ArrayCollection) {
$object = $object->toArray();
}
else {
$data[$field] = $this->serialize($object);
}
}
return $data;
}
How can I resolve the associated fields into their respective arrays.
I have tried using the built-in, and JMS serialiser, but this gives me issues of nestedness limits, so this is not an option for me.
UPDATE:
I have updated the code to handle instance of ArrayCollection as per #ScayTrase's suggestion. However, the error above is still reported with a one-to-many field map. In debug, the variable $object is of type "Doctrine\ORM\PersistentCollection"
For *toMany association properties implemented with ArrayCollection you should call ArrayCollection::toArray() first. Just check it with instanceof before, like this
if ($object instanceof ArrayCollection) {
$object = $object->toArray();
}
I'm going to create 50 temp/dummy rows inside my model database. My problems is how can I create and save new records in my DB? Assume my model Random containts columns (id,randomValue,...).
public function addNew() {
$this->autoRender = false; // We don't render a view in this example
$moduleSize = 50;
$count = $moduleSize;
while ($count > 0) {
$random_number = rand(1000000000, 9999999999);
//check if duplicate
if ($this->checkIfDuplicateSerial($random_number)) {
//nothing to be done
} else {
$this->Random->create(array('randomValue' => $random_number));
$this->Random->save();
$this->Random->clear(); //to init new one
$count--;
}
}
$this->response->body('Inserted' . $moduleSize . 'Random Numbers');
}
Hi I've done a find() and added a new field to some of the results:
$approved = $this->ExpenseClaim->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
$i = 0;
foreach ($approved as $ap) {
$approved[$i]['ExpenseClaim']['claimTotal'] = $this->ExpenseClaim->expenseClaimTotal($approved[$i]['ExpenseClaim']['id']);
$i++;
}
I now need to pass this to paginate, however I read here that you cannot do this and that I must create another model to use the afterFind() method only on this one particular find.
So I've created the new Model called ExpenseClaimTotal and set the UseTable to
public $useTable = 'expense_claims';
Then in the new models afterFind() method I did a simple debug:
public function afterFind($results, $primary = false) {
debug($results);
//return $results;
}
But when I now try and do a find against this new model in pagesController it fails:
$this->loadModel('ExpenseClaimTotal');
$approved = $this->ExpenseClaimTotal->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
This is the error I get:
Database Error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'ExpenseClaim.claim_status_id' in 'where clause'
SQL Query: SELECT `ExpenseClaimTotal`.`id`, `ExpenseClaimTotal`.`user_id`, `ExpenseClaimTotal`.`claim_status_id`, `ExpenseClaimTotal`.`created`, `ExpenseClaimTotal`.`modified`, `ExpenseClaimTotal`.`approved`, `ExpenseClaimTotal`.`approved_by`, `ExpenseClaimTotal`.`declined_by`, `ExpenseClaimTotal`.`date_submitted` FROM `expenses`.`expense_claims` AS `ExpenseClaimTotal` WHERE `ExpenseClaim`.`claim_status_id` = 3
There doesnt seem to be much in the docs about using 2 models for one table
You don't want to paginate an array
You're already performing a find, it's not sensible to perform a find and then paginate the resultant array.
Simply paginate your model data directly and inject your total values in the process. As such - if you put your original "added a new field to some of the results" logic in the model:
class ExpenseClaim extends AppModel {
public function afterFind($results, $primary = false) {
foreach ($results as &$ap) {
if (isset($ap['ExpenseClaim']['id'])) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
}
return $results;
}
}
Your controller code becomes simply:
public function index() {
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
And the code is simple and "just works".
Enhancements
The above is the simplest way to achieve the desired results, but has some disadvantages - namely it will call the total method on pretty much all finds.
Depending on exactly what you're doing you may wish to for example:
Cache your totals
If appropriate, you can remove problems by simply adding the field "claim_total" to the database, and recalculate whenever it changes. That would mean there is absolutely no extra logic when reading from the expense claim model.
Use a custom find type
If you don't want to recaculate the total on all finds - you can create a custom find type
class ExpenseClaim extends AppModel {
public $findMethods = array('allWithTotals' => true);
protected function _findAllWithTotals($state, $query, $results = array()) {
if ($state === 'before') {
return $query;
}
foreach ($results as &$ap) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
return $results;
}
And then use it in your paginate call:
public function index() {
$this->paginate['findType'] = 'allWithTotals'; # <-
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
In this way, only the index method will trigger the call to add the totals.
This might not be the problem with saveAll, the situation is like this. I have table messages, and table sent_messages, which holds message id and user id. Models are created, where Messaage hasMany SentMessage, and SentMessage belongsTo Message. I need to save just one message in messages table, and save user ids and message id in *sent_messages* table. The problem starts when I try to save that in MessageController. Here is the code:
function custom() {
if (!empty($this->data)) {
$this->loadModel('User');
$userIDs = $this->User->listActiveUsers();
$this->data['Message']['message_type_id'] = 2;
if ($this->Message->save($this->data)) {
array_pop($this->data);
foreach ($userIDs as $key => $value ) {
$this->data['SentMessage'][$key]['user_id'] = $value;
$this->data['SentMessage'][$key]['message_id'] = $this->Message->getLastInsertID();
}
//die($this->Message->SentMessage->saveAll($this->data));
if ($this->Message->SentMessage->saveAll($this->data)) {
$this->Session->setFlash('Data saved', 'success');
}
else {
$this->Session->setFlash('Data not saved', 'error');
}
}
}
}
Any help would be appreciated.
I think your issue come from using "$key" as key for $userIDs array, saveAll take unindexed array for hasMany relations as parameters.
More-over, saveAll is perfect to save two related model in same time. Here something that should work :
function custom() {
if (!empty($this->data)) {
$this->loadModel('User');
$this->data['Message']['message_type_id'] = 2;
$userIDs = $this->User->listActiveUsers();
foreach ($userIDs as $value ) {
$this->data['SentMessage'][]['user_id'] = $value;
}
if ($this->Message->saveAll($this->data)) {
$this->Session->setFlash('Data saved', 'success');
} else {
$this->Session->setFlash('Data not saved', 'error');
}
}
}
Check the doc ! http://book.cakephp.org/view/1031/Saving-Your-Data
About using loadModel, I suppose that your SentMessage model is related to User, so you can use : $this->Message->SentMessage->User. But well no idea of which is the more efficient.
Easiest Way to save all data in cakephp
$this->ModelName->saveAll($data_array);
Note : name of the fields in table and in array should be same.