How does one go about escaping parameters passed to a raw query in Laravel 4? I expected something like DB::escape() (which rings a bell from Laravel 3) and also attempted DB::quote() (which I thought could be available through the PDO object)
$query = DB::select("SELECT * FROM users WHERE users.id = " . DB::escape($userId));
We can't use the select method with placeholders as the above is just a simplified example of what we are trying to achieve. We have a large custom query with a few nested select queries that cannot be adapted to the query builder.
What is the best approach to escaping something prior to inserting in Laravel 4?
EDIT:
I've just discovered that you can access the PDO object and use the quote function on it this way. Is this still the best approach, or is there an easier way to access this function?
DB::connection()->getPdo()->quote("string to quote");
You can quote your strings this way, through the DB facade.
DB::connection()->getPdo()->quote("string to quote");
I did put this answer in my question when I discovered it, however I've now put it in as an actual answer to make it easier for others to find.
$value = Input::get("userID");
$results = DB::select( DB::raw("SELECT * FROM users WHERE users.id = :value"), array(
'value' => $value,
));
More Details HERE
You may also try this, (Read Documentation)
$results = DB::select('SELECT * FROM users WHERE users.id = ?', array($userId));
Two answers here, that I use, have less verbose solutions built into the DB facade.
First, value quoting:
// From linked answer
DB::connection()->getPdo()->quote("string to quote");
// In the DB facade
DB::getPdo()->quote('string to quote');
Second, identifier quoting (table and column names):
// From linked answer
DB::table('x')->getGrammar()->wrap('table.column');
// In the DB facade
DB::getQueryGrammar()->wrap('table.column');
I found this question when looking for generic sql escaping in Laravel. What I actually needed though was table/column name escaping. So, for future reference:
/**
* Quotes database identifier, e.g. table name or column name.
* For instance:
* tablename -> `tablename`
* #param string $field
* #return string
*/
function db_quote_identifier($field) {
static $grammar = false;
if (!$grammar) {
$grammar = DB::table('x')->getGrammar(); // The table name doesn't matter.
}
return $grammar->wrap($field);
}
I'm using this in my helpers.php at Laravel 5:
if ( ! function_exists('esc_sql'))
{
function esc_sql($string)
{
return app('db')->getPdo()->quote($string);
}
}
Then I can use esc_sql function where I need to pergorm escaping for raw SQL queries.
Here's a fuller example, showing how to escape both values and columns and extend Laravel's querybuilder:
<?php
namespace App\Providers;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\ServiceProvider;
class DatabaseQueryBuilderMacroProvider extends ServiceProvider {
public function register() {
Builder::macro('whereInSet', function($columnName, $value) {
/** #var \Illuminate\Database\Query\Grammars\Grammar $grammar */
$grammar = $this->getGrammar();
return $this->whereRaw('FIND_IN_SET(?,' . $grammar->wrap($columnName) . ')', [$value]);
});
}
}
PHP Heredoc
<?php
use Illuminate\Support\Facades\DB;
$sql = <<<SQL
WITH table1 AS(SELECT...
SQL;
$parameters = [1,2,3,...]
$table = DB::select($sql, $parameters);
Related
⛳ What I need:
I am developing an application in Laravel 5.4 and I want a global scope that allows me to filter different elements of the application depending on the user that created them.
🌍 My global scope:
I have a class BaseEloquentModel.php who extends Eloquent and all my models extends from this class. I have a global scope as follow:
protected static function boot()
{
parent::boot();
static::addGlobalScope('', function(\Illuminate\Database\Eloquent\Builder $builder) use($userId) {
/**
* I get the name of the table with <code>(with(new static))->getTable()</code>
* and then filter the query for the <b>user_id</b> field
*/
$builder->where(
(with(new static))->getTable() . '.user_id',
'=',
$userId
);
});
}
⛔ The problem
When I have a query like this, with or operator, the global scope is "neutralized":
$qBuilderCars = Car::whereRaw("name like ? or id = ?", [
'%' . $searchCriteria. '%',
$searchCriteria
]);
If I call the toSql() method on $qBuilderCars I see that it "correctly" adds the AND operator to the end of the query.
select * from `cars` where name like ? or id = ? and user_id = ?
Maybe you've already noticed my problem ... If the element's builder, in this case cars, has used an OR operator, then the global scope will not help, since there is no parenthesis between where name like ? or id = ?. So the resulting query would be something similar to the following:
select * from `cars` where name like ? (or id = ? and user_id = ?)
So this query will return all cars whose name matches or whose ID is the one received and has been created by the user...
When what I need is:
select * from `cars` where (name like ? or id = ?) and user_id = ?
👎 My attempts
I tried to alter my global scope to try to make the AND operator that I add the most restrictive in the query, but without any success.
I can not manually add parentheses to all the application's queries, so ... Is there a way to add global parentheses from the global scope to the builder?
💡 The solution
The solution is to add parentheses to all the raw queries.
✅✅ You can see the #Styx solution which I consider the most successful
I will also leave my answer, which acts directly inside the global scope, and which I consider interesting to be able to see how an \Illuminate\Database\Eloquent\Builder object works
Well, it seems that your solution to add parentheses is the best workaround, but I have a suggestion how to do that slightly better way.
Create new class QueryBuilder. For example, in \App\Models\ namespace (app/Models/ folder):
namespace App\Models;
use Illuminate\Database\Query\Builder as EloquentQueryBuilder;
class QueryBuilder extends EloquentQueryBuilder {
public function whereRaw($sql, $bindings = [], $boolean = 'and')
{
return parent::whereRaw('('.$sql.')', $bindings, $boolean);
}
}
Add this code to your BaseEloquentModel class:
use Illuminate\Database\Eloquent\Model;
use App\Models\QueryBuilder; // <-- addition
class BaseEloquentModel extends Model {
// ...
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new QueryBuilder(
$connection,
$connection->getQueryGrammar(),
$connection->getPostProcessor()
);
}
// ...
}
Now, all whereRaw() calls will automatically have parentheses around query.
😕 The solution...?
I found a "solution", before apply my global scope I loop through all where clauses whose type is raw:
protected static function boot()
{
parent::boot();
static::addGlobalScope('', function(\Illuminate\Database\Eloquent\Builder $builder) use($userId) {
/**
* My workaround to prevent a raw query from neutralizing the global scope.
* We go through all the queries and, if they are raw, encapsulate them in parentheses.
*/
$wheres = $builder->getQuery()->wheres;
foreach ($wheres as $iWhere => $where) {
// If where clause is "raw" I wrap with parenthesis.
if ($where['type'] == 'raw') {
$builder->getQuery()->wheres[$iWhere]["sql"] = "({$where['sql']})";
}
}
/**
* I get the name of the table with <code>(with(new static))->getTable()</code>
* and then filter the query for the <b>user_id</b> field
*/
$builder->where(
(with(new static))->getTable() . '.user_id',
'=',
$userId
);
});
}
I do not know if this solution will have any unexpected repercussion or if it will affect in excess the performance of the queries...
Will return the next SQL:
select * from `cars` where (name like ? or id = ?) and `cars`.`user_id` = ?"
In my laravel 5 app, I'm using PostgreSQL's jsonb data type and it has ? operator.
But I can't get it work in my model, because laravel uses question marks as bindings.
Specifically, in whereRaw() method:
$query->whereRaw("jsonb_column ? 'a_key'")
How can I use question mark in my queries?
you can consider using the function call instead of operator.
First you should find out which function ? operator uses via following query on your PostgresSQL database:
SELECT oprname, oprcode FROM pg_operator WHERE oprname = '?'
on my development database it's jsonb_exists function,
then you can update your query as:
$query->whereRaw("jsonb_exists(jsonb_column, 'a_key')")
I hope it helps, happy coding.
Basically you have 2 options:
Getting your hand dirty by extending the current way Laravel's Query Builder implement whereRaw, otherwise stated doing it the hard way.
Send feature request to the Laravel team (ie asking them to extend all Query Builder component dealing with more PostgresQL specifics) and cross your fingers they will address it if you are patient enough.
Here are my takes for the [1.] option:
The namespace "Illuminate\Database\Query" is good for you:
You need particularly to delve into the following Laravel 5.0 source codes of interest:
Builder.php
Grammar.php
PostgresGrammar.php
Code fragments of interest:
whereRaw in Builder.php (excerpt):
/**
* Add a raw where clause to the query.
*
* #param string $sql
* #param array $bindings
* #param string $boolean
* #return $this
*/
public function whereRaw($sql, array $bindings = array(), $boolean = 'and')
{
$type = 'raw';
$this->wheres[] = compact('type', 'sql', 'boolean');
$this->addBinding($bindings, 'where');
return $this;
}
compileWheres in Grammar.php (excerpt):
/**
* Compile the "where" portions of the query.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileWheres(Builder $query)
{
$sql = array();
if (is_null($query->wheres)) return '';
// Each type of where clauses has its own compiler function which is responsible
// for actually creating the where clauses SQL. This helps keep the code nice
// and maintainable since each clause has a very small method that it uses.
foreach ($query->wheres as $where)
{
$method = "where{$where['type']}";
$sql[] = $where['boolean'].' '.$this->$method($query, $where);
}
// If we actually have some where clauses, we will strip off the first boolean
// operator, which is added by the query builders for convenience so we can
// avoid checking for the first clauses in each of the compilers methods.
if (count($sql) > 0)
{
$sql = implode(' ', $sql);
return 'where '.$this->removeLeadingBoolean($sql);
}
return '';
}
$operators array in PostgresGrammar.php (excerpt):
/**
* All of the available clause operators.
*
* #var array
*/
protected $operators = array(
'=', '<', '>', '<=', '>=', '<>', '!=',
'like', 'not like', 'between', 'ilike',
'&', '|', '#', '<<', '>>',
);
notice that ? is not a valid operator ;-)
Specialized PostgreSQL protected methods in PostgresGrammar.php (excerpt):
/**
* Compile the additional where clauses for updates with joins.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileUpdateWheres(Builder $query)
{
$baseWhere = $this->compileWheres($query);
if ( ! isset($query->joins)) return $baseWhere;
// Once we compile the join constraints, we will either use them as the where
// clause or append them to the existing base where clauses. If we need to
// strip the leading boolean we will do so when using as the only where.
$joinWhere = $this->compileUpdateJoinWheres($query);
if (trim($baseWhere) == '')
{
return 'where '.$this->removeLeadingBoolean($joinWhere);
}
return $baseWhere.' '.$joinWhere;
}
/**
* Compile the "join" clauses for an update.
*
* #param \Illuminate\Database\Query\Builder $query
* #return string
*/
protected function compileUpdateJoinWheres(Builder $query)
{
$joinWheres = array();
// Here we will just loop through all of the join constraints and compile them
// all out then implode them. This should give us "where" like syntax after
// everything has been built and then we will join it to the real wheres.
foreach ($query->joins as $join)
{
foreach ($join->clauses as $clause)
{
$joinWheres[] = $this->compileJoinConstraint($clause);
}
}
return implode(' ', $joinWheres);
}
consider these as a sort of specialization of the compileWheres mentioned above, the remaining cases apart from the two (only 2!!!) specific ones are compiled using the parent class method (Illuminate\Database\Query\Grammars\Grammar).
Other recommended relevant resources
You may find valuable posts in this blog (SOFTonSOFA).
I particularly recommend:
Laravel Query Builder global scope – how to use custom Connection and Query Builder in Laravel 4, not in right version but shows how to extend the Query Builder.
Laravel 5 Eloquent Global Scope how-to, in Laravel 5.0 but orthogonal to your question yet instructive (right version, plumbing details).
Last but not the least, the Laravel documentation is the best place to grab more about its Architecture Foundations (Service Providers, Service Container, Facades and so on). Mastering it is handy to devise a robust extension of the framework.
Hopefully my input gives you enough hints for the possible extension point of the Laravel Query Builder offered and may it serve as a good starting point for you to write the PostgreSQL extension addressing the ?operator issue in whereRaw.
Please give back/share when done.
Try escaping it using a backslash, like this
SELECT * FROM table WHERE id = \?;
Here is how I query my database for some words
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy('w.frequency', 'DESC')
->getQuery()
->setMaxResults(100);
I'm using mysql and I'd like to get random rows that match the criteria, I would use order by rand() in my query.
I found this similar question which basically suggests since ORDER BY RAND is not supported in doctrine, you can randomize the primary key instead. However, this can't be done in my case because I have a search criteria and a where clause so that not every primary key will satisfy that condition.
I also found a code snippet that suggests you use the OFFSET to randomize the rows like this:
$userCount = Doctrine::getTable('User')
->createQuery()
->select('count(*)')
->fetchOne(array(), Doctrine::HYDRATE_NONE);
$user = Doctrine::getTable('User')
->createQuery()
->limit(1)
->offset(rand(0, $userCount[0] - 1))
->fetchOne();
I'm a little confused as to whether this will help me work around the lack of support for order by random in my case or not. I was not able to add offset after setMaxResult.
Any idea how this can be accomplished?
The Doctrine team is not willing to implement this feature.
There are several solutions to your problem, each having its own drawbacks:
Add a custom numeric function: see this DQL RAND() function
(might be slow if you have lots of matching rows)
Use a native query
(I personally try to avoid this solution, which I found hard to maintain)
Issue a raw SQL query first to get some IDs randomly, then use the DQL WHERE x.id IN(?) to load the associated objects, by passing the array of IDs as a parameter.
This solution involves two separate queries, but might give better performance than the first solution (other raw SQL techniques than ORDER BY RAND() exist, I won't detail them here, you'll find some good resources on this website).
Follow these steps:
Define a new class at your project as:
namespace My\Custom\Doctrine2\Function;
use Doctrine\ORM\Query\Lexer;
class Rand extends \Doctrine\ORM\Query\AST\Functions\FunctionNode
{
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'RAND()';
}
}
Register the class config.yml:
doctrine:
orm:
dql:
numeric_functions:
Rand: My\Custom\Doctrine2\Function\Rand
Use it directly as:
$qb->addSelect('RAND() as HIDDEN rand')->orderBy('rand()'); //Missing curly brackets
In line with what Hassan Magdy Saad suggested, you can use the popular DoctrineExtensions library:
See mysql implementation here: https://github.com/beberlei/DoctrineExtensions/blob/master/src/Query/Mysql/Rand.php
# config.yml
doctrine:
orm:
dql:
numeric_functions:
rand: DoctrineExtensions\Query\Mysql\Rand
Tested in Doctrine ORM 2.6.x-dev, you can then actually do:
->orderBy('RAND()')
Or you could do this -->
$words = $em->getRepository('Entity\Word')->findAll();
shuffle($words);
Of course this would be very inefficient if you have many records so use with caution.
Why not to use repository?
<?php
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# make query
return $this->getEntityManager()->createNativeQuery("
SELECT p.id FROM {$table} p ORDER BY RAND() LIMIT 0, {$amount}
", $rsm);
}
}
For me, the most useful way was to create two arrays where i say order type and different properties of the Entity. For example:
$order = array_rand(array(
'DESC' => 'DESC',
'ASC' => 'ASC'
));
$column = array_rand(array(
'w.id' => 'w.id',
'w.date' => 'w.date',
'w.name' => 'w.name'
));
You could add more entries to array $column like criteria.
Afterwards, you can build your query with Doctrine adding $column and $order inside ->orderBy. For example:
$query = $qb->select('w')
->from('DbEntities\Entity\Word', 'w')
->where('w.indictionary = 0 AND w.frequency > 3')
->orderBy($column, $order)
->getQuery()
->setMaxResults(100);
This way improved the performance of my application. I hope this helps someone.
Shuffling can be done on the query (array) result, but shuffling does not pick randomly.
In order to pick randomly from an entity I prefer to do this in PHP, which might slow the random picking, but it allows me to keep control of testing what I am doing and makes eventual debugging easier.
The example below puts all IDs from the entity into an array, which I can then use to "random-treat" in php.
public function getRandomArt($nbSlotsOnPage)
{
$qbList=$this->createQueryBuilder('a');
// get all the relevant id's from the entity
$qbList ->select('a.id')
->where('a.publicate=true')
;
// $list is not a simple list of values, but an nested associative array
$list=$qbList->getQuery()->getScalarResult();
// get rid of the nested array from ScalarResult
$rawlist=array();
foreach ($list as $keyword=>$value)
{
// entity id's have to figure as keyword as array_rand() will pick only keywords - not values
$id=$value['id'];
$rawlist[$id]=null;
}
$total=min($nbSlotsOnPage,count($rawlist));
// pick only a few (i.e.$total)
$keylist=array_rand($rawlist,$total);
$qb=$this->createQueryBuilder('aw');
foreach ($keylist as $keyword=>$value)
{
$qb ->setParameter('keyword'.$keyword,$value)
->orWhere('aw.id = :keyword'.$keyword)
;
}
$result=$qb->getQuery()->getResult();
// if mixing the results is also required (could also be done by orderby rand();
shuffle($result);
return $result;
}
#Krzysztof's solution is IMHO best here, but RAND() is very slow on large queries, so i updated #Krysztof's solution to gives less "random" results, but they are still random enough. Inspired by this answer https://stackoverflow.com/a/4329492/839434.
namespace Project\ProductsBundle\Entity;
use Doctrine\ORM;
class ProductRepository extends ORM\EntityRepository
{
/**
* #param int $amount
* #return Product[]
*/
public function getRandomProducts($amount = 7)
{
return $this->getRandomProductsNativeQuery($amount)->getResult();
}
/**
* #param int $amount
* #return ORM\NativeQuery
*/
public function getRandomProductsNativeQuery($amount = 7)
{
# set entity name
$table = $this->getClassMetadata()
->getTableName();
# create rsm object
$rsm = new ORM\Query\ResultSetMapping();
$rsm->addEntityResult($this->getEntityName(), 'p');
$rsm->addFieldResult('p', 'id', 'id');
# sql query
$sql = "
SELECT * FROM {$table}
WHERE id >= FLOOR(1 + RAND()*(
SELECT MAX(id) FROM {$table})
)
LIMIT ?
";
# make query
return $this->getEntityManager()
->createNativeQuery($sql, $rsm)
->setParameter(1, $amount);
}
}
I hope this would help others:
$limit = $editForm->get('numberOfQuestions')->getData();
$sql = "Select * from question order by RAND() limit $limit";
$statement = $em->getConnection()->prepare($sql);
$statement->execute();
$questions = $statement->fetchAll();
Note here the table question is an AppBundle:Question Entity. Change the details accordingly. The number of questions is taken from the edit form, make sure to check the variable for the form builder and use accordingly.
First get the MAX value from DB table & then use this as offset in PHP i.e
$offset = mt_rand(1, $maxId)
I know this is an old question. But I used the following solution to get the random row.
Using an EntityRepository method:
public function findOneRandom()
{
$id_limits = $this->createQueryBuilder('entity')
->select('MIN(entity.id)', 'MAX(entity.id)')
->getQuery()
->getOneOrNullResult();
$random_possible_id = rand($id_limits[1], $id_limits[2]);
return $this->createQueryBuilder('entity')
->where('entity.id >= :random_id')
->setParameter('random_id', $random_possible_id)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
Probably the easiest (but not necessarily the smartest) way to get a single object result ASAP would be implementing this in your Repository class:
public function findOneRandom()
{
$className = $this->getClassMetadata()->getName();
$counter = (int) $this->getEntityManager()->createQuery("SELECT COUNT(c) FROM {$className} c")->getSingleScalarResult();
return $this->getEntityManager()
->createQuery("SELECT ent FROM {$className} ent ORDER BY ent.id ASC")
->setMaxResults(1)
->setFirstResult(mt_rand(0, $counter - 1))
->getSingleResult()
;
}
Just add the following:
->orderBy('RAND()')
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.
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.
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);
}
}
Updated answer for Laravel Eloquent in 2018
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']]);
Answer for Laravel 5.8.33+
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.
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' => 'mpdrza#gmail.com'
]);
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.
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.
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.
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
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);
$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.
While working on a mapping structure for our applications we ran into some trouble regarding the code consistency. While it's easy to make select query's with the Zend_Db_Select class (with functions like: $select->from('table')->where('id=?,1), it wouldn't work for update/delete query's. There isn't a class like Zend_Db_Update or Zend_Db_Delete to build the update and delete query's like you build the select. To fix this we extended the Zend_Db_Select class as you can see in the code below. The code shows the custom class that extends the Zend_Db_Select class with some minimal example code at the bottom to show how it's used.
<?php
class Unica_Model_Statement extends Zend_Db_Select
{
public function __construct($oMapper)
{
parent::__construct($oMapper->getAdapter());
$this->from($oMapper->getTableName());
}
/**
* #desc Make a string that can be used for updates and delete
* From the string "SELECT `column` FROM `tabelnaam` WHERE `id` = 1" only the part "`id = `" is returned.
* #return string
*/
public function toAltString()
{
$sQuery = $this->assemble(); // Get the full query string
$sFrom = $this->_renderFrom(''); // Get the FROM part of the string
// Get the text after the FROM (and therefore not using the "SELECT `colname`" part)
$sString = strstr($sQuery,$sFrom);
// Delete the FROM itself from the query (therefore deleting the "FROM `tabelnaam`" part)
$sString = str_replace($sFrom, '', $sString);
// Delete the word "WHERE" from the string.
$sString = str_replace('WHERE', '', $sString);
return $sString;
}
}
################################################
# Below code is just to demonstrate the usage. #
################################################
class Default_IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$person = new Unica_Model_Person_Entity();
$statement = new Unica_Model_Statement($person->getMapper());
$statement->where('id = ?' , 1);
$person->getMapper()->delete($statement);
}
}
class Unica_Model_Person_Mapper
{
public function delete($statement)
{
$where = $statement->toAltString();
$this->getAdapter()->delete($this->_tableName,$where);
}
}
Everything works fine using this class but it got us wondering if we were maybe missing something. Is there a reason there aren't default update/delete classes like there is for select and will using this class give us trouble elsewhere?
Advice would be appreciated.
Thanks in advance,
Ilians
The class is fine if you are sure you are not going to make it evolve too much in the future. I assume your approach is to benefit from the automatic quoting in the Zend_Db_Select class. In my humble opinion, however, it has some design pitfalls that could lead to extensibility troubles if you need to modify or extend it:
It's receiving some data that is discarded afterwards (the entity object, to use the "from" clause).
It's manipulating directly the SQL output of the select query, which can be something dangerous to rely upon. If the format changes, and also if you need to include more elements to the where clause, your code could get quite "muddy" to adapt to the changes.
My approach would just be to use the where clauses directly in the code, and quote them wherever it's necessary. It doesn't look particularly less clean to me. Something like the following:
$db->delete('tablename', 'id = ' . $db->quote('1'));
Eventually, you could even abstract it into a method or class, to avoid having to spread $db->quote() calls all over the place, something like that:
private function myDelete($tableName, $fieldName, $fieldValue)
{
$this->db->delete($tableName, $fieldName . ' = ' . $db->quote($fieldValue));
}
EDIT: Including multiple clauses in the where part would make it a bit more involved, and how flexible you want to be will depend on your particular application. For instance, a possible solution for several "ANDed" clauses could be the following:
private function myDelete($tableName, array $fields)
{
$whereClauses = array();
foreach ($fields as $fieldName => $fieldValue) {
$whereClauses[] = $fieldName . ' = ' . $db->quote($fieldValue);
}
$this->db->delete($tableName, implode(' AND ', $whereClauses));
}
$this->myDelete('tablename', array('id' => '1', 'name' => 'john'));
Hope that helps,
I dont know the exact reasoning behind Zend_Db_Select not offering CUD methods. The obvious explanation is that Select means you should use it for just that: dynamic query building. To insert, update and delete you would either use Zend_Db_Adapter or the proxy methods to it in Zend_Db_Table and Zend_Db_Table_Row or the generic Zend_Db_Statement.
However, with that said, I think there is nothing wrong with extending Zend_Db_Select and adjusting it your needs (just like any other component that doesnt do what you want initially)