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();
}
Related
We currently encounter a Duplicate entry QueryException when executing the following code:
Slug::firstOrCreate([
Slug::ENTITY_TYPE => $this->getEntityType(),
Slug::SLUG => $slug
], [
Slug::ENTITY_ID => $this->getKey()
]);
Since the firstOrCreate method by Laravel first checks if the entry with the attributes exist before inserting it, this exception should never occur. However, we have an application with million of visitors and million of actions every day and therefore also use a master DB connection with two slaves for reading. Therefore, it might be possible that some race conditions might occur.
We currently tried to separate the query and force the master connection for reading:
$slugModel = Slug::onWriteConnection()->where([
Slug::SLUG => $slug,
Slug::ENTITY_TYPE => $this->getEntityType()
])->first();
if ($slugModel && $slugModel->entity_id !== $this->getKey()) {
$class = get_class($this);
throw new \RuntimeException("The slug [{$slug}] already exists for a model of type [{$class}].");
}
if (!$slugModel) {
return $this->slugs()->create([
Slug::SLUG => $slug,
Slug::ENTITY_TYPE => $this->getEntityType()
]);
}
However the exception still occurs sometimes.
Our next approach would be to lock the table before the reading check and release the lock after the writing to prevent any inserts with the same slug from other database actions between our reading and our writing. Does anyone know how to solve this? I don`t really understand how Laravel's Pessimistic Locking can help solving the issue. We use MySql for our database.
I would not recommend to lock the table, especially if you have millions of viewers.
Most race-conditions can be fixed by locks, but this is not fixable with locks, because you cannot lock a row that does not exist (there is something like gap locking, but this won't help here.).
Laravel does not handle race-conditions by itself. If you call firstOrCreate it does two queries:
SELECT item where slug=X and entity_type=Y
If it does not exists, create it
Now because we have two queries, race condition is possible, meaning two user in parallel reach step 1, then both try to create the entry in step 2 and your system will crash.
Since you already have a Duplicate Key error, it means you aleady put a unique constrain on the tuple on the two columns that identify your row, which is good.
What you could do now, is to catch the duplicate key error like this:
try{
$slug = Slug::firstOrCreate([
Slug::ENTITY_TYPE => $this->getEntityType(),
Slug::SLUG => $slug
], [
Slug::ENTITY_ID => $this->getKey()
]);
}
catch (Illuminate\Database\QueryException $e){
$errorCode = $e->errorInfo[1];
if($errorCode == 1062){
$slug = Slug::where('slug','=', $slug)->where('entity_type','=', $this->getEntityType())->first();
}
}
one solution for this is to use Laravel queue and make sure that it runs one job at a time, in this way you will never have 2 identical queries at the same time.
for sure this will not work if you want to return back the result in the same request.
I assume that this should all be in one query in order to prevent duplicate data in the database. Is this correct?
How do I simplify this code into one Eloquent query?
$user = User::where( 'id', '=', $otherID )->first();
if( $user != null )
{
if( $user->requestReceived() )
accept_friend( $otherID );
else if( !$user->requestSent() )
{
$friend = new Friend;
$friend->user_1= $myID;
$friend->user_2 = $otherID;
$friend->accepted = 0;
$friend->save();
}
}
I assume that this should all be in one query in order to prevent
duplicate data in the database. Is this correct?
It's not correct. You prevent duplication by placing unique constraints on database level.
There's literally nothing you can do in php or any other language for that matter, that will prevent duplicates, if you don't have unique keys on your table(s). That's a simple fact, and if anyone tells you anything different - that person is blatantly wrong. I can explain why, but the explanation would be a lengthy one so I'll skip it.
Your code should be quite simple - just insert the data. Since it's not exactly clear how uniqueness is handled (it appears to be user_2, accepted, but there's an edge case), without a bit more data form you - it's not possible to suggest a complete solution.
You can always disregard what I wrote and try to go with suggested solutions, but they will fail miserably and you'll end up with duplicates.
I would say if there is a relationship between User and Friend you can simply employ Laravel's model relationship, such as:
$status = User::find($id)->friends()->updateOrCreate(['user_id' => $id], $attributes_to_update));
Thats what I would do to ensure that the new data is updated or a new one is created.
PS: I have used updateOrCreate() on Laravel 5.2.* only. And also it would be nice to actually do some check on user existence before updating else some errors might be thrown for null.
UPDATE
I'm not sure what to do. Could you explain a bit more what I should do? What about $attributes_to_update ?
Okay. Depending on what fields in the friends table marks the two friends, now using your example user_1 and user_2. By the example I gave, the $attributes_to_update would be (assuming otherID is the new friend's id):
$attributes_to_update = ['user_2' => otherID, 'accepted' => 0 ];
If your relationship between User and Friend is set properly, then the user_1 would already included in the insertion.
Furthermore,on this updateOrCreate function:
updateOrCreate($attributes_to_check, $attributes_to_update);
$attributes_to_check would mean those fields you want to check if they already exists before you create/update new one so if I want to ensure, the check is made when accepted is 0 then I can pass both say `['user_1' => 1, 'accepted' => 0]
Hope this is clearer now.
I'm assuming "friends" here represents a many-to-many relation between users. Apparently friend requests from one user (myID) to another (otherId).
You can represent that with Eloquent as:
class User extends Model
{
//...
public function friends()
{
return $this->belongsToMany(User::class, 'friends', 'myId', 'otherId')->withPivot('accepted');
}
}
That is, no need for Friend model.
Then, I think this is equivalent to what you want to accomplish (if not, please update with clarification):
$me = User::find($myId);
$me->friends()->syncWithoutDetaching([$otherId => ['accepted' => 0]]);
(accepted 0 or 1, according to your business logic).
This sync method prevents duplicate inserts, and updates or creates any row for the given pair of "myId - otherId". You can set any number of additional fields in the pivot table with this method.
However, I agree with #Mjh about setting unique constraints at database level as well.
For this kind of issue, First of all, you have to enjoy the code and database if you are working in laravel. For this first you create realtionship between both table friend and user in database as well as in Models . Also you have to use unique in database .
$data= array('accepted' => 0);
User::find($otherID)->friends()->updateOrCreate(['user_id', $otherID], $data));
This is query you can work with this . Also you can pass multiple condition here. Thanks
You can use firstOrCreate/ firstOrNew methods (https://laravel.com/docs/5.3/eloquent)
Example (from docs) :
// Retrieve the flight by the attributes, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// Retrieve the flight by the attributes, or instantiate a new instance...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
use `firstOrCreate' it will do same as you did manually.
Definition of FirstOrCreate copied from the Laravel Manual.
The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the given attributes.
So according to that you should try :
$user = User::where( 'id', '=', $otherID )->first();
$friend=Friend::firstOrCreate(['user_id' => $myId], ['user_2' => $otherId]);
It will check with both IDs if not exists then create record in friends table.
I ran into strange situation with Yii ActiveRecord.
I have model User (there are some relations, but no foreign keys). When I try to delete some rows from action:
$cr = new CDbCriteria();
$cr->addColumnCondition(array(
'status' => User::USER_STATUS_DELETED
));
$items = User::model()->findAll($cr);
if(!empty($items)){
foreach($items as $item){
$item->delete();
}
}
There is no effect. Users are still there. By the way, I can delete them manually with phpmyadmin. More interesting thing - $item->delete() returns true.
Where is the problem?
1) Make sure you do not have a function overwriting the delete function
2) if you have a relation, and it is improper created and you are trying to delete a user.id that is related to another record then the delete might fail. (but that should also fail in phpmyadmin)
3) I for example have a soft delete, I just replace the user.status with "deleted". I cannot do that if my model is not validating, so I have to make a ->save(false) to get around that (I actually do not do that but you get my point).
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();
}
recently I've created a simple registration form and when i try to save the data, if i enter an existing username or email i get an error saying that the query is stopped becasue the field already exists
In some cases this is a great feature, and i could use it in the email case.
Im not sure if i have to set this in the form validators or in the config.php or in my module.
here is my save method:
public function saveUser(User $user)
{
$data = array(
'username' => $user->username,
'email' => $user->email,
'password' => $user->password,
);
$id = (int) $user->user_id;
if ($id == 0) {
$this->insert($data);
} elseif ($this->getUser($id)) {
$this->update(
$data,
array(
'user_id' => $user_id,
)
);
} else {
throw new \Exception('Form id does not exist');
}
}
and here is the short error:
Statement could not be executed
...
QLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'admin' for key 'username'
....
like i said, i could use this error for the email, because i want unique email, but im not sure how to catch this error and display it in a nicer format, maybe like a validation error.
any ideas on this issue?
thanks
If you are using the Zend_Form to create the forms you can use the Zend_Validate_Db_NoRecordExists to check if the email already exists or not.
This is error returned by your SQL server. To get rid of this you need to remove UNIQUE KEY from your field username.
As for displaying "nice error message" if user email is duplicate, I'm afraid the only choice you have is to check its existence before executing INSERT.
I once had database model which was throwing Database_UniqueKey_Exception( $field) which allowed you to do this in quite stylish fashion, but AFAIK Zend doesn't support special handling for unique key issues and you have to either parse error message (I wouldn't go there) or check it in advance.
Here's an example on how to achieve what you want. Zend Framework: Validate duplicate database entry
It uses the MySQL PDOException code: 23000 to check if a duplicate entry exception occurred and attaches the error message to the form. This way you won't need to make an extra database query in order to check for duplicate username entry.
Hope this helps.