Skip to content

Instantly share code, notes, and snippets.

@bretto36
Last active November 2, 2023 19:51
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bretto36/b9fd23d902d6b52715c0e9c8e6c6c38d to your computer and use it in GitHub Desktop.
Save bretto36/b9fd23d902d6b52715c0e9c8e6c6c38d to your computer and use it in GitHub Desktop.
Upsert Without Updating `updated_at` column
<?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)
);
}
}
<?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);
}
}
<?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
);
}
}
<?php
$rows = [
['unique_field' => 123, 'name' => 'Test1'],
['unique_field' => 124, 'name' => 'Test2'],
];
Invoice::upsertWithoutUpdatedAt($rows, ['unique_field'])
@ctoma
Copy link

ctoma commented Oct 30, 2023

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;

<?php

namespace App\Providers;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class UpsertServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        Builder::macro('upsertWithoutUpdatedAt', function (array $values, $uniqueBy, $update = null) {
           // Your code here
        });

        EloquentBuilder::macro('upsertWithoutUpdatedAt', function (array $values, $uniqueBy, $update = null) {
            // Your code here
        });
    }
}

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.

@bretto36
Copy link
Author

bretto36 commented Nov 2, 2023

@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