Skip to content

Conversation

mpyw
Copy link
Contributor

@mpyw mpyw commented Aug 24, 2023

Summary

This PR proposes a modification to the updateOrCreate() method in line with recent changes brought by the introduction of the createOrFirst() method.

Details

In the previous PR by @tonysm, the firstOrCreate() method was enhanced to leverage createOrFirst(), making it more race-condition resistant, especially in highly concurrent environments. This was achieved by attempting to create a record first and, if encountering a unique constraint violation, falling back to find the matching record. This robust approach, however, was not mirrored in the updateOrCreate() method.

To address this and bring consistency:

public function updateOrCreate(array $attributes, array $values = [])
{
    return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) {
        if (!$instance->wasRecentlyCreated) {
            $instance->fill($values)->save();
        }
    });
}

By doing this, the updateOrCreate() method will now utilize the enhanced firstOrCreate(), making it more resilient against potential race conditions and ensuring that both methods share a unified and robust approach in concurrent situations.

@mpyw mpyw changed the title [10.x] Adopt firstOrCreate() for updateOrCreate() [10.x] Enhancing updateOrCreate() to Use firstOrCreate() Aug 24, 2023
@mpyw mpyw force-pushed the improve/update-or-create branch 2 times, most recently from c3c7c08 to ac5261a Compare August 24, 2023 06:09
@mpyw mpyw marked this pull request as ready for review August 24, 2023 06:15
@mpyw mpyw force-pushed the improve/update-or-create branch from ac5261a to 63524ba Compare August 25, 2023 11:58
@mpyw mpyw marked this pull request as draft August 25, 2023 15:19
@mpyw
Copy link
Contributor Author

mpyw commented Aug 25, 2023

@taylorotwell With reference to PR #47973, the only method that was internally changed to use createOrFirst is Builder::firstOrCreate from the following list:

  • Builder::firstOrCreate
  • HasOneOrMany::firstOrCreate
  • BelongsToMany::firstOrCreate

For this reason, in this PR, I've only made the adjustment to Builder::updateOrCreate.


Should we aim to establish the dependency chain createOrFirst ← firstOrCreate ← updateOrCreate across all Builder and Relation classes in another PR? Do you have any concerns or reservations about this approach?

@mpyw mpyw marked this pull request as ready for review August 25, 2023 15:30
@tonysm
Copy link
Contributor

tonysm commented Aug 25, 2023

@mpyw Oh, nice catch. I forgot to update the relation methods. I'll send a separate PR to address that. I think we better attempt to update all updateOrCreate implementations, so it's consistent across the board.

@mpyw
Copy link
Contributor Author

mpyw commented Aug 26, 2023

@taylorotwell taylorotwell merged commit 80e0262 into laravel:10.x Aug 27, 2023
@mpyw mpyw deleted the improve/update-or-create branch August 27, 2023 17:19
mpyw added a commit to mpyw/framework that referenced this pull request Aug 28, 2023
mpyw added a commit to mpyw/framework that referenced this pull request Aug 28, 2023
mpyw added a commit to mpyw/framework that referenced this pull request Aug 28, 2023
taylorotwell pushed a commit that referenced this pull request Aug 29, 2023
…ate` behind the scenes (#48213)

* Make the `updateOrCreate` methods in relations use `firstOrCreate`

Related to #48160, #48192

* Fix tests

* Fix: correctly apply unused `$joining` and `$touch`

* Fix: `save()` should always accepts `['touch' => false]`
@mpyw
Copy link
Contributor Author

mpyw commented Aug 30, 2023

@ceejayoz
Copy link
Contributor

I'm trying to generate a simple reproduction case, but this change has broken some code for us.

Route::get('test', function() {
    $user = User::findOrFail(11);

    $social = Social::withTrashed()
        ->updateOrCreate(
            [
                'type' => 'facebook_account',
                'external_id' => 'test'
            ],
            [
                'name' => 'test',
                'details' => null,
                'type' => 'facebook_account',
                'external_id' => 'test',
            ],
        );

    $connection = $user->connections()->updateOrCreate(
        [
            'social_id' => $social->id
        ],
        [
            'type' => 'facebook_account',
            'token' => 'test',
            'via_id' => null,
            'details' => null,
            'social_id' => $social->id
        ],
    );
});

This results in an error on the creation of $connection, with social_id being null in the query, despite being in the $fillable array. Reverting the change immediately resolves the error.

Simple repro example is proving tough because both our Social and Connection models are using https://github.com/tighten/parental, but I wanted to note this in case anyone else is encountering the same.

@tonysm
Copy link
Contributor

tonysm commented Sep 14, 2023

@ceejayoz from what I can see, I don't see why the Connection::updateOrCreate would fail. When you say "fail" what exactly happens? Does it set the social_id to null? Or do you get an exception?

I don't see why social_id would be set to null, since it's being used to find the connection record in the createOrFirst

Both Connection and Social are part of the same hierarchy? I mean, are these models pointing to the same table? If so, does Connection also use the soft-deletes trait?

Can you create an issue for it referencing this PR?

@ceejayoz
Copy link
Contributor

@tonysm The call results in a violation of the not-null constraint on my social_id column; it's null in the final query.

I've drilled down into internals a bit with dumps of the $attributes and $values arrays in a number of spots; it's getting nulled out somewhere but I haven't quite figured out the tap() stuff's impact there. I need to get xcode running on this particular project so I can track it a little more comprehensively.

$fillable is set on both models appropriately, and undoing the change in vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php definitely puts it back to normal operation.

Connection and Social are different models, each with same-table-inheritance submodels (using Parental), i.e. there's a TwitterSocial and a TwitterConnection. Socials have a hasMany Connections on them. Both have soft deletes enabled.

It may be something peculiar to Parental; I'll try and get a minimum example repo put together.

tonysm added a commit to tonysm/framework that referenced this pull request Sep 25, 2023
taylorotwell pushed a commit that referenced this pull request Sep 25, 2023
…#48531)

* Avoid using createOrFirst inside other *OrCreate methods

* Revert "[10.x] Make the `updateOrCreate` methods in relations use `firstOrCreate` behind the scenes (#48213)"

This reverts commit e1ae507.

* Revert "Enhancing `updateOrCreate()` to Use `firstOrCreate()` (#48160)"

This reverts commit 80e0262.

* Fix mocked firstOrCreate tests
@tonysm
Copy link
Contributor

tonysm commented Sep 25, 2023

@ceejayoz we reverted from using the createOrFirst in the updateOrCreate methods (already merged in the 10.x branch). Can you check if the issue was resolved?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants