Last active
November 2, 2023 19:51
-
-
Save bretto36/b9fd23d902d6b52715c0e9c8e6c6c38d to your computer and use it in GitHub Desktop.
Upsert Without Updating `updated_at` column
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Database; | |
use Illuminate\Database\Eloquent\Builder; | |
class EloquentQueryBuilder extends Builder | |
{ | |
/** | |
* Insert new records or update the existing ones. | |
* | |
* @param array $values | |
* @param array|string $uniqueBy | |
* @param array|null $update | |
* @return int | |
*/ | |
public function upsertWithoutUpdatedAt(array $values, $uniqueBy, $update = null) | |
{ | |
if (empty($values)) { | |
return 0; | |
} | |
if (!is_array(reset($values))) { | |
$values = [$values]; | |
} | |
if (is_null($update)) { | |
$update = array_keys(reset($values)); | |
} | |
return $this->toBase()->upsertWithoutUpdatedAt( | |
$this->addTimestampsToUpsertValues($values), | |
$uniqueBy, | |
$this->addUpdatedAtToUpsertColumns($update) | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Models; | |
use App\Database\EloquentQueryBuilder; | |
use App\Database\QueryBuilder; | |
use Illuminate\Database\Eloquent\Model; | |
class Invoice extends Model | |
{ | |
protected function newBaseQueryBuilder() | |
{ | |
$connection = $this->getConnection(); | |
return new QueryBuilder( | |
$connection, | |
$connection->getQueryGrammar(), | |
$connection->getPostProcessor() | |
); | |
} | |
public function newEloquentBuilder($query) | |
{ | |
return new EloquentQueryBuilder($query); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Database; | |
use Illuminate\Support\Arr; | |
use Illuminate\Support\Facades\DB; | |
class QueryBuilder extends \Illuminate\Database\Query\Builder | |
{ | |
/** | |
* Insert new records or update the existing ones without modifying the updated at column everytime | |
* | |
* @param array $values | |
* @param array|string $uniqueBy | |
* @param array|null $update | |
* @return int | |
*/ | |
public function upsertWithoutUpdatedAt(array $values, $uniqueBy, $update = null) | |
{ | |
if (empty($values)) { | |
return 0; | |
} elseif ($update === []) { | |
return (int)$this->insert($values); | |
} | |
if (!is_array(reset($values))) { | |
$values = [$values]; | |
} else { | |
foreach ($values as $key => $value) { | |
ksort($value); | |
$values[$key] = $value; | |
} | |
} | |
if (is_null($update)) { | |
$update = array_keys(reset($values)); | |
} | |
if ($updatedAtKey = array_search('updated_at', $update)) { | |
$updatedAtValue = $values[array_key_first($values)]['updated_at']; | |
// Replace the item and then move it to the front | |
unset($update[$updatedAtKey]); | |
$parts = []; | |
foreach ($update as $key => $value) { | |
if (is_numeric($key)) { | |
// Use <=> to ensure that it compares NULL values too | |
$parts[] = 'NOT(' . $this->grammar->wrap($value) . ' <=> VALUES(' . $this->grammar->wrap($value) . '))'; | |
} | |
} | |
$newUpdatedAtValue = DB::raw('CASE WHEN ' . implode(' OR ', $parts) . ' THEN "' . $updatedAtValue . '" ELSE ' . $this->grammar->wrap('updated_at') . ' END'); | |
// This MUST be at the front otherwise the query performs the updates before it compares the value | |
$update = array_merge(['updated_at' => $newUpdatedAtValue], $update); | |
} | |
$this->applyBeforeQueryCallbacks(); | |
$bindings = $this->cleanBindings(array_merge( | |
Arr::flatten($values, 1), | |
collect($update)->reject(function ($value, $key) { | |
return is_int($key); | |
})->all() | |
)); | |
return $this->connection->affectingStatement( | |
$this->grammar->compileUpsert($this, $values, (array)$uniqueBy, $update), | |
$bindings | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$rows = [ | |
['unique_field' => 123, 'name' => 'Test1'], | |
['unique_field' => 124, 'name' => 'Test2'], | |
]; | |
Invoice::upsertWithoutUpdatedAt($rows, ['unique_field']) |
@ctoma Thanks for the update, that's a great addition. Using your way, a package would be very easy, but it's just as easy to add the one file now too without me having to try and maintain a package.
One thing i noticed the other day, if it's setting a value to null from a previous non null value it won't update the updated_at
column
$parts[] = $this->grammar->wrap($value) . ' <> VALUES(' . $this->grammar->wrap($value) . ')';
needs to be come this
$parts[] = 'NOT(' . $this->grammar->wrap($value) . ' <=> VALUES(' . $this->grammar->wrap($value) . '))';
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This gist works perfectly, although it was conflicting with staudenmeir/laravel-cte
An alternative solution I found was to register it as a macro in a service provider, ie;
And now everything works great, no conflicts.
Have you considered making this into a package? It's very useful and I imagine a lot of people would love it. Thanks for your work.