Skip to content

Commit 26f8669

Browse files
committed
refactor(database): support relation-only updates
1 parent 583c821 commit 26f8669

File tree

3 files changed

+63
-12
lines changed

3 files changed

+63
-12
lines changed

packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ public function __construct(
6363
*/
6464
public function execute(mixed ...$bindings): ?PrimaryKey
6565
{
66-
$result = $this->build()->execute(...$bindings);
66+
// For relation-only updates, we need to resolve values to set up
67+
// callbacks but we won't actually execute an UPDATE statement.
68+
if ($this->hasOnlyRelationUpdates()) {
69+
$this->resolveValuesToUpdate();
70+
$result = $this->primaryKeyForRelations;
71+
} else {
72+
$result = $this->build()->execute(...$bindings);
73+
}
6774

6875
// Execute after callbacks for relation updates
6976
if ($this->model->hasPrimaryKey() && $this->after !== [] && $this->primaryKeyForRelations !== null) {
@@ -121,7 +128,7 @@ public function toRawSql(): ImmutableString
121128

122129
public function build(mixed ...$bindings): Query
123130
{
124-
$values = $this->resolveValues();
131+
$values = $this->resolveValuesToUpdate();
125132

126133
if ($this->model->hasPrimaryKey()) {
127134
unset($values[$this->model->getPrimaryKey()]);
@@ -148,7 +155,7 @@ public function build(mixed ...$bindings): Query
148155
return new Query($this->update, $allBindings)->onDatabase($this->onDatabase);
149156
}
150157

151-
private function resolveValues(): ImmutableArray
158+
private function resolveValuesToUpdate(): ImmutableArray
152159
{
153160
if ($this->hasRelationUpdates()) {
154161
$this->validateRelationUpdateConstraints();
@@ -527,6 +534,21 @@ public function where(string $field, mixed $value, string|WhereOperator $operato
527534
return $this;
528535
}
529536

537+
private function hasOnlyRelationUpdates(): bool
538+
{
539+
if (! $this->hasRelationUpdates()) {
540+
return false;
541+
}
542+
543+
foreach (array_keys($this->values) as $field) {
544+
if (! $this->isRelationField($field)) {
545+
return false;
546+
}
547+
}
548+
549+
return true;
550+
}
551+
530552
private function hasRelationUpdates(): bool
531553
{
532554
foreach (array_keys($this->values) as $field) {

packages/database/src/IsDatabaseModel.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public function load(string ...$relations): self
205205
}
206206

207207
/**
208-
* Saves the model to the database.
208+
* Saves the model to the database. If the model has no primary key, this method always inserts.
209209
*/
210210
public function save(): self
211211
{
@@ -250,17 +250,23 @@ public function save(): self
250250
*/
251251
public function update(mixed ...$params): self
252252
{
253-
inspect(self::class)->validate(...$params);
253+
$model = inspect($this);
254254

255-
foreach ($params as $key => $value) {
256-
$this->{$key} = $value;
255+
if (! $model->hasPrimaryKey()) {
256+
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'update');
257257
}
258258

259+
$model->validate(...$params);
260+
259261
query($this)
260262
->update(...$params)
261-
->build()
263+
->where($model->getPrimaryKey(), $model->getPrimaryKeyValue())
262264
->execute();
263265

266+
foreach ($params as $key => $value) {
267+
$this->{$key} = $value;
268+
}
269+
264270
return $this;
265271
}
266272

tests/Integration/Database/Builder/IsDatabaseModelTest.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ public function test_model_create_with_has_many_relations(): void
658658
$this->assertInstanceOf(PrimaryKey::class, $user->id);
659659

660660
$posts = TestPost::select()
661-
->where('testuser_id', $user->id->value)
661+
->where('test_user_id', $user->id->value)
662662
->all();
663663

664664
$this->assertCount(2, $posts);
@@ -667,6 +667,29 @@ public function test_model_create_with_has_many_relations(): void
667667
$this->assertSame('foo', $posts[1]->title);
668668
$this->assertSame('bar', $posts[1]->body);
669669
}
670+
671+
public function test_model_update_with_only_relations(): void
672+
{
673+
$this->migrate(
674+
CreateMigrationsTable::class,
675+
CreateTestUserMigration::class,
676+
CreateTestPostMigration::class,
677+
);
678+
679+
$user = TestUser::create(name: 'Frieren');
680+
$user->update(posts: [
681+
new TestPost('hello', 'world'),
682+
]);
683+
684+
$posts = TestPost::select()
685+
->where('test_user_id', $user->id->value)
686+
->all();
687+
688+
$this->assertCount(1, $posts);
689+
$this->assertSame('hello', $posts[0]->title);
690+
$this->assertSame('world', $posts[0]->body);
691+
$this->assertSame('Frieren', $user->name); // Ensure name wasn't changed
692+
}
670693
}
671694

672695
final class Foo
@@ -1048,7 +1071,7 @@ final class CreateTestUserMigration implements DatabaseMigration
10481071

10491072
public function up(): QueryStatement
10501073
{
1051-
return CreateTableStatement::forModel(TestUser::class)
1074+
return new CreateTableStatement('test_users')
10521075
->primary()
10531076
->text('name');
10541077
}
@@ -1065,9 +1088,9 @@ final class CreateTestPostMigration implements DatabaseMigration
10651088

10661089
public function up(): QueryStatement
10671090
{
1068-
return CreateTableStatement::forModel(TestPost::class)
1091+
return new CreateTableStatement('test_posts')
10691092
->primary()
1070-
->belongsTo('test_posts.testuser_id', 'test_users.id')
1093+
->foreignId('test_user_id', constrainedOn: 'test_users')
10711094
->string('title')
10721095
->text('body');
10731096
}

0 commit comments

Comments
 (0)