Skip to content

Commit 24cbb80

Browse files
tonysmtaylorotwell
andauthored
[10.x] Make the firstOrCreate methods in relations use createOrFirst behind the scenes (#48192)
* Use createOrFirst inside firstOrCreate in BelongsToMany relations This should be the same but more robust, since it handles race conditions as well * Use createOrFirst too inside HasOneOrMany relations This should essentially be the same as before, but more robust * Fix tests using mocks * Ensure the firstOrCreate method handles unique violation when just attaching * Fix style * Update BelongsToMany.php --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 27958eb commit 24cbb80

File tree

5 files changed

+37
-3
lines changed

5 files changed

+37
-3
lines changed

src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,9 +622,13 @@ public function firstOrCreate(array $attributes = [], array $values = [], array
622622
{
623623
if (is_null($instance = (clone $this)->where($attributes)->first())) {
624624
if (is_null($instance = $this->related->where($attributes)->first())) {
625-
$instance = $this->create(array_merge($attributes, $values), $joining, $touch);
625+
$instance = $this->createOrFirst($attributes, $values, $joining, $touch);
626626
} else {
627-
$this->attach($instance, $joining, $touch);
627+
try {
628+
$this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch));
629+
} catch (UniqueConstraintViolationException $exception) {
630+
// Nothing to do, the model was already attached...
631+
}
628632
}
629633
}
630634

src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public function firstOrNew(array $attributes = [], array $values = [])
236236
public function firstOrCreate(array $attributes = [], array $values = [])
237237
{
238238
if (is_null($instance = $this->where($attributes)->first())) {
239-
$instance = $this->create(array_merge($attributes, $values));
239+
$instance = $this->createOrFirst($attributes, $values);
240240
}
241241

242242
return $instance;

tests/Database/DatabaseEloquentHasManyTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public function testFirstOrCreateMethodCreatesNewModelWithForeignKeySet()
155155
$relation = $this->getRelation();
156156
$relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery());
157157
$relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null);
158+
$relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope());
158159
$model = $this->expectCreatedModel($relation, ['foo']);
159160

160161
$this->assertEquals($model, $relation->firstOrCreate(['foo']));
@@ -165,6 +166,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewModelWithForeignKeySe
165166
$relation = $this->getRelation();
166167
$relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery());
167168
$relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null);
169+
$relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope());
168170
$model = $this->expectCreatedModel($relation, ['foo' => 'bar', 'baz' => 'qux']);
169171

170172
$this->assertEquals($model, $relation->firstOrCreate(['foo' => 'bar'], ['baz' => 'qux']));

tests/Database/DatabaseEloquentMorphTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ public function testFirstOrCreateMethodCreatesNewMorphModel()
195195
$relation = $this->getOneRelation();
196196
$relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery());
197197
$relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null);
198+
$relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope());
198199
$relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class));
199200
$model->shouldReceive('setAttribute')->once()->with('morph_id', 1);
200201
$model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent()));
@@ -208,6 +209,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewMorphModel()
208209
$relation = $this->getOneRelation();
209210
$relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery());
210211
$relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null);
212+
$relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope());
211213
$relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo' => 'bar', 'baz' => 'qux'])->andReturn($model = m::mock(Model::class));
212214
$model->shouldReceive('setAttribute')->once()->with('morph_id', 1);
213215
$model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent()));

tests/Integration/Database/EloquentHasManyTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,32 @@ public function testHasOneRelationshipFromHasMany()
6161
$this->assertEquals($latestLogin->id, $user->latestLogin->id);
6262
}
6363

64+
public function testFirstOrCreate()
65+
{
66+
$user = EloquentHasManyTestUser::create();
67+
68+
$post1 = $user->posts()->create(['title' => Str::random()]);
69+
$post2 = $user->posts()->firstOrCreate(['title' => $post1->title]);
70+
71+
$this->assertTrue($post1->is($post2));
72+
$this->assertCount(1, $user->posts()->get());
73+
}
74+
75+
public function testFirstOrCreateWithinTransaction()
76+
{
77+
$user = EloquentHasManyTestUser::create();
78+
79+
$post1 = $user->posts()->create(['title' => Str::random()]);
80+
81+
DB::transaction(function () use ($user, $post1) {
82+
$post2 = $user->posts()->firstOrCreate(['title' => $post1->title]);
83+
84+
$this->assertTrue($post1->is($post2));
85+
});
86+
87+
$this->assertCount(1, $user->posts()->get());
88+
}
89+
6490
public function testCreateOrFirst()
6591
{
6692
$user = EloquentHasManyTestUser::create();

0 commit comments

Comments
 (0)