Save model in Yii twice - getting "Integrity constraint violation: 1062 Duplicate entry" - php

I'm trying to save newly created yii model twice - first to get auto-incremented id. And the second time to save that id-related stuff:
$node = new Node;
$node->attributes = $attrs;
$node->save(); // now I have 'id'
$node->vector = calcVector($node->id); // vector is based on 'id'
$node->save();
The second save (edit: error was thrown elsewhere) throws this error: Integrity constraint violation: 1062 Duplicate entry. The expected behavior is to simply update the already saved model.
What is the right way to save it second time?
(I could do $node = Node::model()->findByPk($node->id);, but that doesn't seem right)

just set
$node->isNewRecord = false;
then
$node->save();
cheers

Uh, so apparently the problem was not in what I describe above.
Saving twice is working as expected - 1st call inserts, 2nd call updates.
The problem was probably that I was saving the model in beforeSave(). I've had a complicated and confusing logic in there, didn't realize what's happening..

I had a somewhat similar situation where I needed to save a model to database multiple times. I accomplished it by simply instantiating a model after saving it:
foreach ($partsIdArray as $id)
{
$model->load(Yii::$app->request->post()); // loading form values
$model->part_id = $id;
$model->save();
$model = new \backend\models\Abc();
}

Related

Laravel Eloquent, how to handle UNIQUE error?

I have a MySQL constraint to ensure unique on a composite key. When inserting a new record in my model Foo I get the expected error:
$foo = new Foo(['foo' => 42, 'bar => 1]);
$foo->save();
Error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '42' for key 'Unique'...
One solution to avoid this error is to query the model before inserting:
if (!Foo::where('foo', 42)->where('bar', 1)->first()) {
$foo = new Foo(['foo' => 42, 'bar => 1]);
$foo->save();
}
Another one would be to catch the exception when preg_match(/UNIQUE/, $e->message) is true.
Is there any better solution?
EDIT
I noticed that in Illuminate\Database\Eloquent\Builder Laravel does the double query anyway which is a bit sad:
public function findOrNew($id, $columns = ['*'])
{
if (! is_null($model = $this->find($id, $columns))) {
return $model;
}
return $this->newModelInstance();
}
In the general case you should be dealing with database errors using the error code and not any regex.
In your particular case pre-querying or using a Laravel method that does that automatically for you, might be preferable if your intention is to overwrite/update existing data.
If you want to generally anticipate an error and handle it you should do something like:
try {
$foo = new Foo(['foo' => 42, 'bar' => 1]);
$foo->save();
} catch (\Exception $e) { // It's actually a QueryException but this works too
if ($e->getCode() == 23000) {
// Deal with duplicate key error
}
}
Refer to https://dev.mysql.com/doc/refman/5.5/en/error-reference.html for an exhaustive list of error codes (but ideally you'd only need to deal with a couple of specific errors and under very specific circumstances.
Lastly the SQL ON DUPLICATE KEY UPDATE might also work for you, however if you are doing this to silently ignore the new values then I suggest you do the error handling instead.
You can use the firstOrCreate method:
$foo = Foo::firstOrCreate(['foo' => 42, 'bar' => 1]);
This will check if the record exists in the database before creating it. If it exists, it will return the record.
For more information: https://laravel.com/docs/5.8/eloquent#inserting-and-updating-models
laravel provides you with the firstOrCreate functions, which first checks if that value exist in the database, then you also have updateOrCreate function to use incase you want to update some value, but most important one, if you want to handle only unique records then check the validation rules, you would do a FormRequest and add unique rule to that field, after that laravel with provide you with error messages, so you handle the error on the client side
The verification need to be done at Controller level (like laravel Validator) and it's better to do a count() instead of the first().
There are different solution depending on what you wanna do when the entity exists in the database. For example you can do updateOrCreate()
$foo = App\Foo::updateOrCreate(['foo' => 42, 'bar' => 1]);
or throw an exception

Laravel duplicate entry after force delete

Working on a Laravel 4.2 app (yes, it's an older app not worth upgrading)
Trying handle the case where a user was soft deleted and wants to create an account again.
Having the issue that registering a new user doesn't work as the 'email' column is unique and the soft deleted record still exists.
I would much rather prefer to create a new user record (with all other fields empty), with same incrementing ID to keep relations with other soft deleted records.
So physically delete the old record and create a new one with the old ID? Right? Not working.
Calling forceDelete() on the record works, it does remove it from the Database, but then trying to create a new record immediately afterward throws a 'duplicate key' exception. Refreshing the page it is no longer a problem, as it doesn't see the old record.
It seems that the error is coming from MYSQL itself, though php should be waiting for a response to the delete query before executing the insert query, so mysql should know at this point that the record has been removed.
Is it possible that this error is actually coming from some sort of memory cache in Laravel?
I am also using the Sentinel authentication package, if that is of relevance.
Here is the basic code:
$found = User::where('email', $email)->first();
if ($found->deleted_at) {
$old_id = $found->id;
$found->forceDelete();
$found->save();
$unique_id = $this->generateUnique(NULL, $first_name . $last_name);
$user = Sentinel::registerAndActivate(array(
'id' => $old_id,
'email' => $email,
'password' => $password,
'first_name' => $first_name,
'last_name' => $last_name,
'unique_id' => $unique_id,
'preferred_lang' => $this->locale,
));
}
This results in a
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'whoever#mail.com' for key 'users_email_unique'
Does anyone know why this is? Is there an easy solution?
From the docs (https://laravel.com/docs/4.2/eloquent):
To restore a soft deleted model into an active state, use the restore method:
$user->restore();
To determine if a given model instance has been soft deleted, you may use the trashed method:
if ($user->trashed())
{
//
}
I actually got my code to work...
I read somewhere that I had to call
$found->save()
to update the deleted record, but that still had not worked for me.
Oddly enough, switching to
$found->update()
worked.
You can restore deleted row like this:
$deletedRow = User::withTrashed()->where('email', $email)->first();
if($deletedRow) {
$deletedRow->restore();
}

Doctrine ODM flush() with unique key

I use doctrine ODM to work with MongoDB. I have documents to save which can duplicate time to time. I need only 1 copy of each event, so I use hashed uniq key to ensure event is only 1.
So I do several ->persist($document);
And when I do ->flush();
I'm getting an exception:
localhost:27017: E11000 duplicate key error index: dbname.event.$eventKey_1 dup key: { : "keyValue" }
And all data never persisted to MongoDB. So question is: is any way to persist uniq data and ignore existing without doing:
try {
->persist();
->flush();
} catch (\Exception $e) {}
for each document?
Thank you.
Updated:
thanks for your answers and your time, but I found exact solution :)
Mongo function insert has an option "ordered: "
https://docs.mongodb.org/manual/reference/method/db.collection.insert/
which allow continue insertion even after errors.
Doctrine use Pecl extension for mongo.
doctrine flush() use this method:
http://www.php.net/manual/en/mongocollection.batchinsert.php
which has option "continueOnError"
So if you do this way:
$documentManager->flush(null, ['continueOnError' => true]);
It will save all documents without errors and skip all with errors. Though it will throw "\MongoDuplicateKeyException". So all you need - catch this exception and handle or simply ignore it (depending on your needs).
Something like this :)
The native Doctrine methods do not support filtering unique values - you need to do this on your own.
To insert those data without any errors you have to do a few things, depending on your entity structure:
Find all existing entities with the unique keys you have
Find unique keys that are duplicated between the entities you are trying to persist
Replace the already existing entities with the entities you found
Persist and flush
There is absolutely no chance to do this without at least one additionally query. If you had the primary key of the existing entities, you could use those to get a reference object. But unfortunately, there is no support for getting references by unique keys according to the doctrine documentation:
http://doctrine-orm.readthedocs.org/en/latest/reference/limitations-and-known-issues.html#join-columns-with-non-primary-keys
It is not possible to use join columns pointing to non-primary keys. Doctrine will think these are the primary keys and create lazy-loading proxies with the data, which can lead to unexpected results. Doctrine can for performance reasons not validate the correctness of this settings at runtime but only through the Validate Schema command.
I was unable to get the ['continueOnError' => true] to work, so solved it differently:
$collection = $documentManager->getDocumentCollection(Content::class);
try {
$collection->insertMany($arrayData, ['ordered' => false]);
} catch (BulkWriteException $exception) {
// ignore duplicate key errors
if (false === strpos($exception->getMessage(), 'E11000 duplicate key error')) {
throw $exception;
}
}
You can just do: Search your existing id edit it and save..
With Doctrine + symfony:
$Document = $EntityManager->getRepository("ExpBundle:Document")->find(123);
$Document->setTitile("Doc");
$EntityManager->flush();
With Doctrine
$Document = $EntityManager->find('Document', 1234);
$Document->setTitle("edited");
$EntityManager->flush();

$model->save() adding 'where id = 1' condition

I'm trying to update a model, I load the model, take all the data from the POST and then save it, easy... But my record was never updating so went to the log and discovered that the update query is adding a weird condition. FYI, MD_ID is my primary key.
So, I load the model, the next line is the SQL produced by Yii:
$model = Ositems::model()->findByPk($id);
SELECT * FROM "MTODETALLADO_INV" "t" WHERE "t"."MD_ID"=249217
If echo the json_encode of the loaded model I get that dictionary in my browser:
echo json_encode($model->getAttributes());
{""MD_BODEGA":"01","MD_PRODUCTO":"0031253","MD_CANTIDAD":"1","MD_PRECIOTOTAL":"1466",,"MD_PORCENTAJEDESCUENTO":"0","MD_IDCABECERA":"97403","MD_ID":"249217","MD_OBSERVACION":null}
At this point everything looks right, now I take the values from post:
$model->attributes = $_POST;
And here if echo the values of the model I get the new values right, now here is the problem: I save the model and this is the SQL Yii runs (I replaced the :yp_ values to make it more readable)
$model->save();
UPDATE "MTODETALLADO_INV" SET
MD_BODEGA"='01'
MD_PRODUCTO"='0020514
MD_CANTIDAD"='10'
MD_PORCENTAJEDESCUENTO"='0
MD_IDCABECERA"=97403
MD_ID"=249218
MD_PRECIOTOTAL"='36210'
MD_OBSERVACION"=''
WHERE "MTODETALLADO_INV"."MD_ID"=1
And there is the problem! WHERE "MTODETALLADO_INV"."MD_ID"=1, Why would it make it 1 if all this time my model id has been 249218 ?
A few considerations:
My model only takes some columns that I need from the actual table, Yii sets the other columns as null and I omitted them in the previous code.
The table is in a foreign db, I use have a custom ActiveRecord which manages the CDbConnection to a database according to the user. (It's a webservice app)
I followed what the function save() did and could finally find the problem was when it tried to get the primary key. I had this method in my model:
public function primaryKey()
{
return array('MS_ID');
}
}
But it had to be:
public function primaryKey()
{
return 'MS_ID';
}
}
Somehow that was causing the problem.

Handling foreign key exceptions in PHP

What is the best way in PHP to handle foreign key exceptions on a mysql database? Is there a mysql class that can be used to simplify any code?
Ideally, what I want to do, as an example, is to try to delete a record where it is the foreign key parent to any number of child tables. The foreign key throws the exception, so then I would like to be able to look at each foreign key table and test it, giving meaningful feedback on the tables and number of records causing the exception. This would then be returned as the error so the end user can reference and delete the offending records.
The way I handle this is to set up my database wrapper class to always throw an exception when you encounter a database error. So, for instance, I might have a class called MySQL with the following functions:
public function query($query_string)
{
$this->queryId = mysql_query($query_string,$this->connectionId);
if (! $this->queryId) {
$this->_throwException($query_string);
}
return $this->queryId;
}
private function _throwException($query = null)
{
$msg = mysql_error().". Query was:\n\n".$query.
"\n\nError number: ".mysql_errno();
throw new Exception($msg,mysql_errno());
}
Any time a query fails, a regular PHP exception is thrown. Note that I would throw these from within other places too, like a connect() function or a selectDb() function, depending on whether the operation succeeded or not.
With that set up, you're good to go. Any place you expect that you might need to be handling a database error, do something like the following:
//assume $db has been set up to be an instance of the MySQL class
try {
$db->query("DELETE FROM parent WHERE id=123");
} catch (Exception $e) {
//uh-oh, maybe a foreign key restraint failed?
if ($e->getCode() == 'mysql foreign key error code') {
//yep, it failed. Do some stuff.
}
}
Edit
In response to the poster's comment below, you have some limited information available to you to help diagnose a foreign key issue. The error text created by a failed foreign key restraint and returned by mysql_error() looks something like this:
Cannot delete or update a parent row:
a foreign key constraint fails
(`dbname`.`childtable`, CONSTRAINT `FK_name_1` FOREIGN KEY
(`fieldName`) REFERENCES `parenttable` (`fieldName`));
If your foreign keys are complex enough that you can't be sure what might cause a foreign key error for a given query, then you could probably parse this error text to help figure it out. The command SHOW ENGINE INNODB STATUS returns a more detailed result for the latest foreign key error as well.
Otherwise, you're probably going to have to do some digging yourself. The following query will give you a list of foreign keys on a given table, which you can examine for information:
select * from information_schema.table_constraints
WHERE table_schema=schema() AND table_name='table_name';
Unfortunately, I don't think there's a magic bullet to your solution other than examining the errors and constraints very carefully.
I think the best bet would be for you to do a transaction. That way, the insert will always be valid, or not done at all. That can return an error message that you can work with as well. This will prevent you from having to manually check every table - the db does it for you.

Categories