Eloquent delete and MySQL foreign key cascade - mysql

I have these tables in MySQL database:
users['id', 'name'],
roles['id', 'title'] and
user_role ['user_id', 'role_id'] where both are foreign keys, CASCADE.
When it catches an exception the user remains in the table as wanted, while the row from the relation table is deleted.
try{
$user->delete();
}
catch (\Exception $e){
throw new \Dingo\Api\Exception\DeleteResourceFailedException('Error.');
}
Is this eloquent's mistake?
Now, I figured out a way to fix this but I'm not sure that it's the best practise. Is there a better way to do it?
try{
$roleId = $user->roles[0]->id;
$user->delete();
}
catch (\Exception $e){
$user->roles()->attach($roleId);
throw new \Dingo\Api\Exception\DeleteResourceFailedException('Error.');
}

If i understood your question, transactions are what you need.
Database Transactions
DB::transaction(function () {
$user->delete();
});
and in case you face a deadlock use this one
DB::transaction(function () {
$user->delete();
},5);

Related

MySQL deadlock on account registration

I'm trying to implement a feature that detects whether or not an account has already signed up. Now when doing parallel requests I'm getting a deadlock. I think I understand why it is happening but I'm unsure on how to solve it.
Here's a simplified version of what I'm doing;
START TRANSACTION;
-- check if user has already signed up (returned rows > 0, throw error if so)
SELECT * FROM users WHERE email = 'example#site.com' FOR UPDATE;
-- user has not signed up yet.. create the account.
INSERT INTO users SET ...;
COMMIT;
Now this in itself works fine. However when two parallel request happen, a deadlock is made because the transaction will both create a FOR UPDATE lock, which is allowed because initially when there is no account signed up yet there are no rows to lock. Atleast, that's what I think is happening.. correct me if I'm wrong.
I'm curious on how I were to fix this, I still want to check whether not an account has registered already so I can show the user a message. Of course the email has a unique constraint but I do not want to rely on that because the auto increment index will increment, even when it violates the constraint.
Also I'm using typeorm, a sample of my code;
public async registerUser(email: string, password: string, displayName?: string) {
const connection = await getConnection();
connection.transaction(async (manager) => {
// First we need to make sure that this email isn't already registered. If
// it has been registered we can throw a simple UserError which will be
// caught by our error handler.
const hasAlreadyRegistered = await this.findUser(email, manager);
if (hasAlreadyRegistered) throw new UserError('Email has already been registered.');
// At last we can create the user, linking him to the previously created
// authentication strategy.
const user = new User();
user.email = email;
user.displayName = displayName || randomBytes(8).toString('hex');
user.strategies = [authentication];
await manager.save(user);
logger.silly('> Created user row.');
return user;
});
}
I have solved this by just checking for the constraint error in the end (per suggestion of #Shadow). It saves me a lot of hassle.
Code
try {
await manager.save(user);
} catch (err: any) {
// Check whether or not this entry violates an unique constraint.
if (err.code === 'ER_DUP_ENTRY') {
throw new UserError('Email has already been registered.');
} else throw err;
}

LOCK TABLES inside TRANSACTION does not ROLLBACK

I have a problem and it is that I need to lock tables within a transaction to prevent data from being inserted into it, but when I do this when I have an error the transaction rollback does not work.
I read in mysql articles that table locks break a transaction, but is there any other way to lock tables to prevent data from being inserted into it temporarily?
https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
try {
DB::beginTransaction();
DB::unprepared('LOCK TABLES table_name WRITE');
//
DB::unprepared('UNLOCK TABLES');
DB::commit();
} catch (\Exception $e){
DB::rollBack();
}
You can lock the tables in the outer scope, outside of the transaction.
Then, your example code would look like:
try
{
DB::unprepared('LOCK TABLES table_name WRITE');
try
{
DB::beginTransaction();
//...
DB::commit();
}
catch (\Exception $e)
{
DB::rollBack();
}
}
finally
{
DB::unprepared('UNLOCK TABLES');
}

How to handle Integrity constraint violation on delete?

I have rows, which some rows can't be delete because it referenced to other table, and the other can be delete.
What I want is delete rows which can be delete and leave the other rows which can't be delete
so far my code is
$tkota = TbKota::find()->all();
foreach($tkota as $kota){
if($kota->delete()){
echo "del success<br/>";
}else{
echo "fail ".$kota['api_id']."<br/>";
}
}
my above code produce this error
SQLSTATE[23503]: Foreign key violation: 7 ERROR: update or delete on table "tb_kota" violates foreign key constraint "fk_tb_produ_reference_tb_kota" on table "tb_produk_ekspedisi_by_vendor"
DETAIL: Key (kota_id)=(1771) is still referenced from table "tb_produk_ekspedisi_by_vendor".
The SQL being executed was: DELETE FROM "tb_kota" WHERE "kota_id"=1771
instead of show success when record deleted and show fail if record can't be delete.
what's wrong with my code?
thanks in advance.
This one will be better
use yii\db\IntegrityException;
use yii\web\NotFoundHttpException;
foreach($tkota as $kota){
$connection = \Yii::$app->db;
$transaction = $connection->beginTransaction();
try {
$kota->delete();
$transaction->commit();
return $this->redirect(['user/view', 'id' => $model->id]);
}catch (IntegrityException $e) {
$transaction->rollBack();
throw new \yii\web\HttpException(500,"YOUR MESSAGE.", 405);
}catch (\Exception $e) {
$transaction->rollBack();
throw new \yii\web\HttpException(500,"YOUR MESSAGE", 405);
}
}
foreach($tkota as $kota){
try {
if($kota->delete()){
echo "del success<br/>";
}
} catch (\Exception $e) {
echo "fail ".$kota['api_id']."<br/>";
}
}

Check if foreign keys is integrity before insert

I have two tables that follow and profile, and follow in the table insert id of the profile below (foreign keys)
How can I be sure before inserting into my table foreign keys that match a profile id of the table?
var o_segui = {
seguace_id: profilo,
seguito_id: data.profilo_da_seguire,
created: new Date()
};
connection.query('INSERT INTO follow SET ?', o_segui, function(error, rows) {
if (error) {
var err = "Error on 'seguiAction': " + error;
console.error(err);
throw err;
}
now i check that now exists with a query before insert like:
connection.query('SELECT count(*) as n FROM profile WHERE id =' + connection.escape(profilo_da_seguire), function(error, rows) {
if (error) {
var err = "Error on 'verifico Profilo': " + error;
console.error(err);
throw err;
}
console.log(rows);
if (rows.shift().n > 0) { then OK
UPDATE in my Insert i do throw err...if i comment throw err server node dosen't go down...i have to comment this line and close connection ?
Check-before-set is usually not a good approach while using database. Mostly because you will have to take into account possible concurrent access to your tables.
Anyway, concerning foreign key constraints, if you use InnoDB, the engine will check for you at insert-time.
So, I think it is better to be optimistic by trying to insert. But be prepared to handle gracefully a possible failure.
EDIT: I build an example in order to "prove" that integrity constraints are enforced by the DB engine InnoDB: http://sqlfiddle.com/#!2/f372d/1
As you will see, faulty data are not inserted in the DB. In your program you will have to deal with the corresponding exception/error.

Yii integrity constraint exception handling with user friendly message

I am using this model code to delete a record.
public function actionDelete($id)
{
$this->loadModel($id)->delete();
// if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser
if(!isset($_GET['ajax']))
$this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
}
The table containing this record has one to many relationship with other table with on delete restrict constraint.
So when deleting a record which has related records in child table it throws exception like
CDbCommand failed to execute the SQL statement: SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`bzuexamsystem`.`campus`, CONSTRAINT `fk_Campus_City` FOREIGN KEY (`CityID`) REFERENCES `city` (`CityID`) ON UPDATE CASCADE). The SQL statement executed was: DELETE FROM `city` WHERE `city`.`CityID`=1
Is there someway to show user friendly error message. Thanks
You need to catch exception. Something like
try{
$this->loadModel($id)->delete();
if(!isset($_GET['ajax']))
$this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
}catch (CDbException $e){
if($e->getCode()===23000){
//You can have nother error handling
header("HTTP/1.0 400 Relation Restriction");
}else{
throw $e;
}
}
If you also use CGrigView in your view file, you should pass "ajaxUpdateError" function to it.
Example:
$this->widget('zii.widgets.grid.CGridView',
array(
'id' => 'model-grid',
'dataProvider' => $model->search(),
'filter' => $model,
'ajaxUpdateError' => <<<JS
function(xhr, ts, et, err){
if(xhr.statusText==="Relation Restriction"){
$("#errorDiv").text("That model is used by something!");
}
else{
alert(err);
}
}
JS
,
'columns' =>
array(
'model_id',
'name'
)
)
);
I guess, $this->loadModel() returns a CActiveRecord object...
First at all you need to make sure the record you want to delete is really loaded. Second, use # at the start of the statement do disallow errors. Then if the CActiveRecord->delete() returns false, it means, that the record wasn't deleted:
public function actionDelete($id) {
$record = $this->loadModel($id);
if ($record !== null) {
if (#$record->delete()) {
// code when successfully deleted
} else {
// code when delete fails
}
} else {
// optional code to handle "record isn't found" case
}
}
You cannot delete rows that have restrict on the foreign key, change that to set to null, or no action depending on your requirements
so your key would be set to null on delete
and cascade on update