I have a custom Drupal 7 module with an 'edit' page.
The form fields reference a couple of database tables, so to process the form, we attempt to update the first table, and we try to set a '$error' to 'true' and check against $error before we attempt to update the next table. For example:
HTML:
<input name="field1" />
<input name="field2" />
PHP:
$error = false;
$update_table_1 = db_update('table1')
->fields(array(
'field1' => $_POST['field1'],
))
->condition('id', $id)
->execute();
if(!update_table_1) {
$error = true;
}
if(!$error) {
$update_table_2 = db_update('table2')
->fields(array(
'field2' => $_POST['field2'],
))
->condition('id', $id)
->execute();
if(!$update_table_2) {
$error = true;
}
}
Problem: If only updating something in table 2, it will throw an error before it event gets to update table 2 because db_query says that it is not true since the field was the same as what was in the database (no change). Really, I only want to stop it if there was a database / code error.
Does the Drupal 7 db_update API have some kind of error reporting function like mysql_error()? Other suggestions?
The safest way you can do it is with a transaction and proper PHP error checking:
$transaction = db_transaction();
try {
// Query 1
db_update(...);
// Query 2
db_update(...);
}
catch (Exception $e) {
// Rollback the transaction
$transaction->rollback();
// Do something with the exception (inform user, etc)
}
I should mention the transaction is only necessary if you don't want the changes from the first query to persist if the second query fails. It's quite a common requirement but might not fit your use case.
Related
I have a Symfony app which exposes a collection of JSON web services used by a mobile app.
On the last few days we are having many concurrent users using the app (~5000 accesses per day) and a Doctrine error started to "randomly" appear in my logs. It appears about 2-3 times per day and this is the error:
Uncaught PHP Exception Doctrine\DBAL\Exception\DriverException: "An exception occurred while executing 'UPDATE fos_user_user SET current_crystals = ?, max_crystals = ?, updated_at = ? WHERE id = ?' with params [31, 34, "2017-12-19 09:31:18", 807]:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction" at /var/www/html/rollinz_cms/releases/98/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 115
It seems it cannot get the lock while updating the users table. The controller code is the following:
/**
* #Rest\Post("/api/badges/{id}/achieve", name="api_achieve_badge")
*/
public function achieveAction(Badge $badge = null)
{
if (!$badge) {
throw new NotFoundHttpException('Badge not found.');
}
$user = $this->getUser();
$em = $this->getDoctrine()->getManager();
$userBadge = $em->getRepository('AppBundle:UserBadge')->findBy(array(
'user' => $user,
'badge' => $badge,
));
if ($userBadge) {
throw new BadRequestHttpException('Badge already achieved.');
}
$userBadge = new UserBadge();
$userBadge
->setUser($user)
->setBadge($badge)
->setAchievedAt(new \DateTime())
;
$em->persist($userBadge);
// sets the rewards
$user->addCrystals($badge->getCrystals());
$em->flush();
return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
'current_crystals' => $user->getCurrentCrystals(),
'max_crystals' => $user->getMaxCrystals(),
));
}
I looked into MySQL and Doctrine documentation but I couldn't find a reliable solution. Doctrine suggests retrying the transaction but it doesn't show an actual example:
https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-example.html
try {
// process stuff
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// retry the processing
}
This posts suggests retrying the transaction. How can I do it?
Could it be a server problem (too many accesses) and I must boost the server or the code is wrong and I must explicitly handle the deadlock in my code?
This is a MySQL issue. Multiple simultaneous transactions blocking the same resources.
Check if you have cronjobs that may block the records for long times.
Otherwise is just concurrent requests updating the same data, you may have better knowledge where this data gets updated.
Dirty attempt for a retry in php:
$retry=0;
while (true) {
try {
// some more code
$em->flush();
return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
'current_crystals' => $user->getCurrentCrystals(),
'max_crystals' => $user->getMaxCrystals(),
));
} catch (DriverException $e) {
$retry++;
if($retry>3) { throw $e; }
sleep(1); //optional
}
}
Albert's solution is the right one but you also must recreate a new EntityManager in the catch clause using resetManager() of your ManagerRegistry. You'll get exceptions if you continue to use the old EntityManager and its behavior will be unpredictable. Beware of the references to the old EntityManager too.
This issue will be hopefully corrected in Doctrine 3: See issue
Until then, here is my suggestion to handle the problem nicely: Custom EntityManager
I'm currently trying the following: In my db, I have a column called templateURL, which is a unique key, so it may only exist once. Then if something tries to submit a form, where this is the same as already exist an error is thrown, saying
Integrity constraint violation: 1062 Duplicate entry for key 'templateURL'.
Of course it does, that's why I made the column unique. But: What I want is not the default Laravel Error, but getting back to the form with the values still entered and a message (alert from bootstrap for example or just a div next to the input field) saying that this already exist and asking to chooose another one. So I need to catch this exception and do something custom instead. How can I achieve what I want?
You've to wrap your database operation into a try-catch block and catch the error and do something else when you get a error, maybe redirect with old input data with a error message.
The error code for duplicate entry is 1062. So if you get 1062 as error code that means this data is a duplicate entry. Here is the code look like for catching this exception.
try {
$data = Model::create(array(
'templateURL' => 'some value ',
));
} catch (Illuminate\Database\QueryException $e) {
$errorCode = $e->errorInfo[1];
if($errorCode == 1062){
// we have a duplicate entry problem
}
}
Or if you wish not to handle the exception yourself, you can take help of Laravel Validator. Like following in your controller
// validation rules
$rules = array(
'templateURL' => 'unique:YourTableNameHere'
);
$validator = Validator::make(Input::all(), $rules);
// check if the validation failed
if ($validator->fails()) {
// get the error messages from the validator
$messages = $validator->messages();
// redirect user back to the form with the errors from the validator
return Redirect::to('form')
->withErrors($validator);
} else {
// validation successful
$data = Model::create(array(
'templateURL' => 'some value ',
));
}
Then in your view template you can access the error messages via $errors variable.
Learn more about validation there https://laravel.com/docs/5.3/validation
I'm trying to validating my insert data in codeigniter
The problem is the code returning me a duplicate entry's page error. And i want to throw that failure to home page with error message.
Here is my code:
$data = array(
'heheId' => $this->input->post('heheId'),
'userId' => $this->input->post('userId')
);
$this->db->insert('tentarasaya',$data);
if ($this->db->affected_rows() > 0){
$this->session->set_flashdata('info', "Hore sukses");
} else {
$this->session->set_flashdata('danger', "Fail insert");
}
redirect('my_home');
Any answer?
Update:
Duplicate entry like this
Try this
$data = array(
'heheId' => $this->input->post('heheId'),
'userId' => $this->input->post('userId')
);
if (!$this->db->insert('tentarasaya',$data)) { # add if here
# Unsuccessful
$this->session->set_flashdata('danger', "Fail insert");
}
else{
# Success
$this->session->set_flashdata('info', "Hore sukses");
}
redirect('my_home');
make sure that auto increment option is selected in your database. I am assuming that user_id here is a primary key, no need to insert that into a database yourself.
data = array(
'heheId' => $this->input->post('heheId'),
);
$this->db->insert('tentarasaya',$data);
Otherwise if you wish to insert user_ids yourself you can 1) define a custom call back function to check to see if the specified user id already exists in the database, 2) use the is_unique form validation rule that comes bundled with codeigniter
I have project with 3 application's sharing one codeigniter installation. It was working excellent until I changed name of mysql table, and changed query to reflect new changes to the database. Now it looks like my insert query is cached, and trying to insert value into old table (which doesn't exist anymore). This is what I've tried so far:
CI:
$this->db->cache_delete_all()
mysql:
RESET QUERY CACHE;
In config/database.php:
$db['default']['db_debug'] = TRUE;
$db['default']['cache_on'] = FALSE;
When db_debug configuration is set to TRUE it gives 500 error, when FALSE, I was able to catch error and it turn's out it's trying to insert into old table that doesn't exist anymore. I don't get how is this possible, I've even searched my complete project and can't find old table name anywhere in my project, also CI cache folders are empty. Any help would be appreciated.
EDIT: My insert code:
if($this->db->insert('booking_calendar', $data))
{
$booking_id = $this->db->insert_id();
$this->load->helper('string');
$order_id = strtoupper(random_string('alnum', 6)) . $booking_id;
$data = array(
'id' => $order_id,
'booking_calendar_id' => $booking_id,
'status' => 'PAYMENT_PENDING'
);
if($this->db->insert('bookings', $data))
{
$this->notifications->add($MYSELF['id'], 'Court successfully booked.');
return $order_id;
}
$this->notifications->add($MYSELF['id'], 'Court booking failed.', 'warning');
return false;
}
else
{
log_message('ERROR', 'Insert booking_calendar failed. ' . print_r($this->db->_error_message(), true));
}
And this is the output in my log:
ERROR - 2013-09-23 20:05:13 --> Insert booking_calendar failed. Table 'tennis.courts_calendar' doesn't exist
Notifications is custom class which just insert one row in "notifications" table.
I found answer to my problem, I had ON BEFORE INSERT/UPDATE triggers on my table, and when I changed it's name it turns out that inserting on the same table with new name was still triggering old table name instead of new one. So I just updated triggers and now it's working. Hope this will save time for someone else, because it took me a while even if it's looking trivial from this point of view now.
i'm using these two lines to update my table using codeigninter active records
$this->db->where('reference_number', $reference);
$this->db->update('patient', $data);
what i want to de is to check weather it successfully updates the table and according to that i want to give a notification to the user, how can i check the successful update is happened with this lines? there is a no clue in the user guide telling that by putting the line
if($this->db->update('patient', $data));
will give us a true or false value, can we do like that? or is there any other solution to this problem?
regards,
Rangana
You can put a code like this in your model...
function func() {
$this->db->where('reference_number', $reference);
$this->db->update('patient', $data);
$report = array();
$report['error'] = $this->db->_error_number();
$report['message'] = $this->db->_error_message();
return $report;
}
_error_number and _error_message use the mysql_errno and mysql_error functions of php.
Then inside your controller, you can check for the error like this...
$this->load->model("Model_name");
$report = $this->Model_name->func();
if (!$report['error']) {
// update successful
} else {
// update failed
}
In addition, you can also use $this->db->affected_rows() to check if something was actually updated.
Can I just follow on from #ShiVik - if you use the _error_number() or _error_message() functions, you will need to disable automatic database error reporting.
You can do this in /config/database.php. Set db_debug = FALSE.
The way I handle this functionality in my system is just to test the result returned by the ActiveRecord class using something like
if($this->db->update('patient', $data) === TRUE) {
$flash_data = array('type' => 'success', 'message'
=> 'Update successful!');
$this->session->set_flashdata('flash', $flash_data);
}
Although usage of the flash session data system is up to you :) You'd need to add in the relevant front-end to this in your view files, and tweak to your liking etc.
use this
return ($this->db->affected_rows() > 0) ? TRUE : FALSE;