Reputation: 63687
Is there a quick way to modify a SQL query generated by Laravel's Fluent to have an INSERT IGNORE
instead of the usual INSERT
?
I'm trying to insert an array with fifty elements. Writing out the entire query manually will bloat up the code and make it more susceptible to human errors.
Upvotes: 13
Views: 31148
Reputation: 2464
If anyone reads this nowadays: there's no need for any hacks or Query Builder extensions. The query builder natively provides an insertOrIgnore
method that does just that.
Just use
DB::table('tablename')->insertOrIgnore([
['column_name' => 'row1', 'column2_name' => 'row1'],
['column_name' => 'row2', 'column2_name' => 'row2']
]);
See the documentation or the API docs for details.
Upvotes: 4
Reputation: 11
Not sure if helpful for anybody but recently I have adapted hakre's approach to Laravel 5:
You have to change following 3 files to have your Insert Ignore working:
In Builder.php (vendor/laravel/framework/src/illuminate/database/query/Builder.php) you have to clon the function insert, with the change in name to insertIgnore and change in the grammar call function to: $sql = $this->grammar->compileInsertIgnore($this, $values);)
In Grammar.php (vendor/laravel/framework/src/illuminate/database/query/grammars/Grammar.php) you have to clone the compileInsert function and rename it to compileInsertIgnore, where you change return to: return "insert ignore into $table ($columns) values $parameters";
In Connection.php (vendor/laravel/framework/src/illuminate/database/Connection.php) you have to simply clone the function insert and rename it to insertIgnore
Now you should be done, connection is able to recognise the function insertIgnore, builder is able to point it to correct grammar and grammar includes 'ignore' in the statement. Please note this works well for MySQL, might not be this smooth for other databases.
Upvotes: 1
Reputation: 7391
Option to avoid writing code is: https://github.com/guidocella/eloquent-insert-on-duplicate-key
I have tested it just now - it works with my 5000 inserts at a time sometimes with duplicates...
With it you will get these functions:
User::insertOnDuplicateKey($data);
User::insertIgnore($data);
Upvotes: 0
Reputation: 24063
This also handles multiple simultaneous inserts (instead of one record at a time).
Warning: Eric's comment below is probably correct. This code worked for my past project, but before using this code again, I'd take a closer look at it and add test cases and adjust the function until it always works as intended. It might be as simple as moving the TODO line down outside the
if
braces.
Either put this in your model's class or in a BaseModel class that your model extends:
/**
* @see https://stackoverflow.com/a/25472319/470749
*
* @param array $arrayOfArrays
* @return bool
*/
public static function insertIgnore($arrayOfArrays) {
$static = new static();
$table = with(new static)->getTable(); //https://github.com/laravel/framework/issues/1436#issuecomment-28985630
$questionMarks = '';
$values = [];
foreach ($arrayOfArrays as $k => $array) {
if ($static->timestamps) {
$now = \Carbon\Carbon::now();
$arrayOfArrays[$k]['created_at'] = $now;
$arrayOfArrays[$k]['updated_at'] = $now;
if ($k > 0) {
$questionMarks .= ',';
}
$questionMarks .= '(?' . str_repeat(',?', count($array) - 1) . ')';
$values = array_merge($values, array_values($array));//TODO
}
}
$query = 'INSERT IGNORE INTO ' . $table . ' (' . implode(',', array_keys($array)) . ') VALUES ' . $questionMarks;
return DB::insert($query, $values);
}
Use like this:
Shop::insertIgnore([['name' => 'myShop'], ['name' => 'otherShop']]);
Upvotes: 4
Reputation: 272
I lastly found this one https://github.com/yadakhov/insert-on-duplicate-key which helped me alot
User::insertIgnore($users); this is the method I am using, giving array of rows to it and its returning effected rows
install it through composer: composer require yadakhov/insert-on-duplicate-key
Upvotes: 0
Reputation: 198118
For the job you need to create a new Grammar that will have the right string in there:
grammar.php (1)
The grammar is a public property of the DB
or in this case Database
stored connection. This is not really straight forward, but from the visibility of the properties you should be able to inject your special grammar into the database layer.
I also suggest you bring the issue up with the project, they probably have got a better idea how to make that more flexible for cases like these.
(1) This was a former, to the date of the answer reference. If you see this today, you need to adopt to the Laravel version you use, e.g. Grammar.php for 4.0, these classes have moved into laravel/framework
.
Upvotes: 1
Reputation: 20492
I couldn't monkey patch as suggested in Rastislav's answer.
This is what worked for me:
Override compileInsert
method in a custom Query Grammar class, which extends the framework's MySqlGrammar class.
Use an instance of this custom grammar class by calling the setQueryGrammar
method from the DB connection instance.
So, the class code is like this:
<?php
namespace My\Namespace;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
/**
* Changes "INSERT" to "INSERT IGNORE"
*/
class CustomMySqlGrammar extends MySqlGrammar
{
/**
* Compile an insert statement into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @return string
*/
public function compileInsert(Builder $query, array $values)
{
// Essentially we will force every insert to be treated as a batch insert which
// simply makes creating the SQL easier for us since we can utilize the same
// basic routine regardless of an amount of records given to us to insert.
$table = $this->wrapTable($query->from);
if (! is_array(reset($values))) {
$values = [$values];
}
$columns = $this->columnize(array_keys(reset($values)));
// We need to build a list of parameter place-holders of values that are bound
// to the query. Each insert should have the exact same amount of parameter
// bindings so we will loop through the record and parameterize them all.
$parameters = collect($values)->map(function ($record) {
return '('.$this->parameterize($record).')';
})->implode(', ');
return "insert ignore into $table ($columns) values $parameters";
}
}
I copied the compileInsert
method from the framework's class and then, inside the method, I have only changed insert
to insert ignore
. Everything else has been kept the same.
Then, in the specific spot of code, in the application (a scheduled task), where I needed "insert ignore", I have simply done as follows:
<?php
use DB;
use My\Namespace\CustomMySqlGrammar;
class SomeClass
{
public function someMethod()
{
// Changes "INSERT" to "INSERT IGNORE"
DB::connection()->setQueryGrammar(new CustomMySqlGrammar());
// et cetera... for example:
ModelClass::insert($data);
}
}
Upvotes: 6
Reputation: 79
Add the follow method insertIgnore to your Model
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'password'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
public static function insertIgnore(array $attributes = [])
{
$model = new static($attributes);
if ($model->usesTimestamps()) {
$model->updateTimestamps();
}
$attributes = $model->getAttributes();
$query = $model->newBaseQueryBuilder();
$processor = $query->getProcessor();
$grammar = $query->getGrammar();
$table = $grammar->wrapTable($model->getTable());
$keyName = $model->getKeyName();
$columns = $grammar->columnize(array_keys($attributes));
$values = $grammar->parameterize($attributes);
$sql = "insert ignore into {$table} ({$columns}) values ({$values})";
$id = $processor->processInsertGetId($query, $sql, array_values($attributes));
$model->setAttribute($keyName, $id);
return $model;
}
}
You can use:
App\User::insertIgnore([
'name' => 'Marco Pedraza',
'email' => '[email protected]'
]);
The next query it will be executed:
insert ignore into `users` (`name`, `email`, `updated_at`, `created_at`) values (?, ?, ?, ?)
This method automatically add/remove the Eloquent timestamps if you have enabled or disabled.
Upvotes: 2
Reputation: 30699
Try this magic, in your model:
public static function insertIgnore($array){
$a = new static();
if($a->timestamps){
$now = \Carbon\Carbon::now();
$array['created_at'] = $now;
$array['updated_at'] = $now;
}
DB::insert('INSERT IGNORE INTO '.$a->table.' ('.implode(',',array_keys($array)).
') values (?'.str_repeat(',?',count($array) - 1).')',array_values($array));
}
Use like this:
Shop::insertIgnore(array('name' => 'myshop'));
This is a great way to prevent constraint violations that may occur with firstOrCreate in a multi-user environment, if that 'name' property was a unique key.
Upvotes: 14
Reputation: 5740
$your_array = array('column' => 'value', 'second_column' => 'value');
DB::table('your_table')->insert($your_array);
Keep in mind, I don't know where your data is coming from, but you should sanitize it, always. If you have more than one record, just iterate over in a loop.
As far as the INSERT IGNORE
, find the INSERT
method in the fluent library, make a new method called insert_ignore the exact same way as insert and just modify with the IGNORE
.
Upvotes: -18