In Laravel Eloquent why are some SQL parameters not bound in? - laravel

Question:
I Noticed interesting behavior in Laravel 7.x where eager loaded relationships don't always have bindings. Is this expected behavior and why would that be the case?
Code:
Actual Queries Laravel Runs:
select top 100 * from task_view
select id, name from task_view where active = ? and student_id in (?, ?, ?)
select id, name from task_view where active = ? and teacher_id in (1 ,2 ,3)
Relationships on Model:
public function studentTasks()
{
return $this->hasMany(StudentTasks::class, 'student_id', 'id');
}
public function teacherTasks()
{
return $this->hasMany(TeacherTasks::class, 'teacher_id', 'teacher_id');
}
Calling Code:
TaskView::query()->with(['studentTasks', 'teacherTasks']);
Additional Points:
I think it may have to do with that where the localkey of the relationship (the 3rd argument) is 'id' then the values aren't bound.
My assumption is that bindings are to prevent sql injection and the Docs seem to confirm that. If that's the case then why would id's of the model that the relationship is on not need to be bound? I would assume there's still an issue of SQL Injection there.
I have not seen anyone discussing this from my searching around, (Stackoverflow, Laracasts, Laravel docs)
(I printed out the queries using the below code in AppServiceProvider:boot)
$counter = 0;
\DB::listen(function ($query) use (&$counter) {
echo 'count: '.++$counter.PHP_EOL;
// echo memory_get_usage();
echo $query->sql.PHP_EOL;
echo implode(',', $query->bindings).PHP_EOL;
});

This is a change introduced into Laravel 5.7.14. The initial pull request can be found here. From there you can find more pull requests making updates to the functionality.
It was done as a performance enhancement when needing to eager load a large number of records (many thousands). Instead of having thousands of bound parameters, it puts the raw ids directly in the query. Initially it was done to work around a MySQL PDO bug, but really all database drivers can benefit with not having thousands of bound parameters.
The reason why it does not introduce a SQL injection vulnerability is that:
It only replaces the bindings with the raw values when the ids are integers, and
It runs all the ids through an integer conversion before adding them to the query.
This is the function that ultimately determines if parameters will be used or if raw ids will be used (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Eloquent/Relations/Relation.php#L310-L323):
/**
* Get the name of the "where in" method for eager loading.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #param string $key
* #return string
*/
protected function whereInMethod(Model $model, $key)
{
return $model->getKeyName() === last(explode('.', $key))
&& in_array($model->getKeyType(), ['int', 'integer'])
? 'whereIntegerInRaw'
: 'whereIn';
}
And here is the whereIntegerInRaw() function that shows the keys are int cast before being added into the raw query (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Query/Builder.php#L961-L985):
/**
* Add a "where in raw" clause for integer values to the query.
*
* #param string $column
* #param \Illuminate\Contracts\Support\Arrayable|array $values
* #param string $boolean
* #param bool $not
* #return $this
*/
public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
{
$type = $not ? 'NotInRaw' : 'InRaw';
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
foreach ($values as &$value) {
$value = (int) $value;
}
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
return $this;
}

Related

Laravel 5.4 save() in a loop

I am trying to 'conquer' Laravel, but some concepts still elude me.
Lets take this code:
public function updateMenuGroup($groupId, $label, $icon, $sort, $userId)
{
/*get model to be updated by id*/
$updateGroups = MenuGroups::findMany($groupId);
/**
* set attributes to be updated
* I loop, since I get collection of models fetched by findMany()
*/
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
}
$update->label carries an array, with different values for different language - say I have 'en', 'de' languages.
When I run code above, only 'en' is being written - overwriting values of 'de'.
Can someone, please, give me some pointers, as to why above happens?
You need to instantiate update model class inside loop.
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update=New ModelClass();
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
You can't insert array to one attribute.
Try to use implode function.
like this.
$update->label = implode(",", $label[$update->lang]);

eloquent use ->with() with only retrieving one column?

e.g.
i want to retrieve all users with the relation roles, but only the field of role name.
something like this:
User::with('user_role', 'user_role.name')
does something like this exist? i have looked around and don't seem to find something related. The performance might be better if you can filter down the returned columns
Yes, you can use something like this:
$user = User::with('user_role:foreign_key,name')->find(1);
In this case, the foreign_key should be the name of the foreign key that is used to build the relation and it's required here and then you may pass other field names to select them by separating with comma.
This is not documented so be careful, it could be removed in the newer versions. It exists there and below is the code sample, taken from Laravel-5.3 (Illuminate\Database\Eloquent\Builder), it works tho (This is how I've used it: User::with('messages:recipient_id,body')->get()):
/**
* Parse a list of relations into individuals.
*
* #param array $relations
* #return array
*/
protected function parseWithRelations(array $relations)
{
$results = [];
foreach ($relations as $name => $constraints) {
// If the "relation" value is actually a numeric key, we can assume that no
// constraints have been specified for the eager load and we'll just put
// an empty Closure with the loader so that we can treat all the same.
if (is_numeric($name)) {
if (Str::contains($constraints, ':')) {
list($constraints, $columns) = explode(':', $constraints);
$f = function ($q) use ($columns) {
$q->select(explode(',', $columns));
};
} else {
$f = function () {
//
};
}
list($name, $constraints) = [$constraints, $f];
}
// We need to separate out any nested includes. Which allows the developers
// to load deep relationships using "dots" without stating each level of
// the relationship with its own key in the array of eager load names.
$results = $this->parseNestedWith($name, $results);
$results[$name] = $constraints;
}
return $results;
}
You can add constraints to eager loaded relations by supplying a with array with closure as the value with the relation as the key.
$user = User::with(['user_role' => function($query) {
return $query->select('name');
}]);

How to use WHERE in yii2 joinWith() that is doing eager loading

I have tables: document and document_content. One document can have many contents.
I am using joinWith() method to get data from document_content table together with document using model relations.
The queries executed are these :
SELECT document.* FROM document INNER JOIN document_content ON document.id = document_content.document_id WHERE (lang='1') ORDER BY id DESC LIMIT 10
SELECT * FROM document_content WHERE document_id IN (665566, 665034, 664961, 664918, 664910, 664898, 664896, 664893, 664882, 664880)
I have a problem with this second query. I want it to include this WHERE clause from the first one: WHERE (lang='1')
So I want yii to generate this query:
SELECT * FROM document_content WHERE (lang='1') AND document_id IN (665566, 665034, 664961, 664918, 664910, 664898, 664896, 664893, 664882, 664880)
I have managed somehow to achieve this, but I have code repetition and I do not like it. There must be some better way to do this. This is my code that works, but it's not that good I think:
/**
* Returns documents by params.
*
* #param array $params the query params.
* #return ActiveDataProvider
*/
public function findDocuments($params)
{
/** #var $query ActiveQuery */
$query = Document::find();
// store params to use in other class methods.
self::$_params = $params;
// build dynamic conditions for document table
$this->buildDocumentQuery($query);
// build dynamic conditions for document_content table
$this->buildDocumentContentQuery($query);
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => ['defaultOrder' => ['id' => SORT_DESC]],
'pagination' => [
'pageSize' => 10,
],
]);
return $dataProvider;
}
/**
* Relation with document_content table.
*
* #return DocumentContent
*/
public function getDocumentContent()
{
$query = $this->hasMany(DocumentContent::className(), ['document_id' => 'id']);
if (isset(self::$_params['lang'])) {
$query->andFilterWhere([
'lang' => self::$_params['lang'],
]);
}
}
/**
* Method that is responsible for building query conditions for document_content table.
*
* #param object $query ActiveQuery instance.
* #return ActiveQuery
*/
public function buildDocumentContentQuery($query)
{
if (isset(self::$_params['lang'])) {
$query->innerJoinWith('documentContent');
}
return $query;
}
As you can see I am checking for params['lang'] on two places. In my relation method and in buildDocumentContentQuery() method. So I am repeating same code on two places, and lang param is not going to be the only one that I want to test, there can be 10 or more.
Basically, I had to do all of this because I could not send any params through yii2 joinWith() method. I do not know what is the best way to add WHERE to query that is generated by eager loading of joinWith(). I made it work somehow, but I think this is dirty.
Does anyone have any idea for better/cleaner solution to this problem ?
Model#Document
public function getDocuments($params)
{
/** #var $query ActiveQuery */
$query = Document::find();
$query->getDocumentContentsByLanguage($params['lang']);
}
public function getDocumentContentsByLanguage($lang = null)
{
return $this->hasMany(DocumentContent::className(), ['document_id' => 'id'])->where('lang = :lang', [':lang'=>$lang]);
}
Try this:
$query = $this
->hasMany(DocumentContent::className(), ['document_id' => 'id']);
if (isset(self::$_params['lang']) && self::$_params['lang']==1) {
$query
->joinWith('document')
->andWhere([
Document::tablename().'.lang' => self::$_params['lang']
]);
}

Check if entity already exists in database before inserting with Doctrine

Whenever I insert an entity that already exists in the database, I get an error because there is a unique constraint on one of the fields (email).
So I want to check if it already exists; if not, I insert it.
My code looks like this:
$q = Doctrine_Query::create()
->from('User u')
->where('u.email = ?', $email);
$object = $q->fetchOne();
if( ! is_object($object)) {
$user = new User();
$user-email = $email;
$user->save();
}
Is there an easier way to do this?
Put the code you have into a method in your UserTable class eg insertIfNotExists():
public function insertIfNotExists(User $user)
{
// Check if it exists first
$q = self::create("u")
->where("u.email = ?", $user->email)
->execute();
// Do we have any?
if ($q->count())
{
// Yes, return the existing one
return $q->getFirst();
}
// No, save and return the newly created one
$user->save();
return $user;
}
Now you can call the method, and the object returned will be the existing record (if there is one), or the one you've just created.
I was faced with a similar problem while building a database-backed logger. To prevent warning fatigue, I assign each log message a UID which is a hash of its identifying content and made the UID a unique key.
Naturally, this requires that I determine whether a record already exists that matches that UID value (in my case, I increment a count value for that log record and touch its updated_at timestamp).
I ended up overriding Doctrine_Record::save() in my model class, similarly to this (code adjusted to be more relevant to your situation):
/** Persists the changes made to this object and its relations into the
* database.
*
* #param $conn Doctrine_Connection
* #return void
*/
public function save( Doctrine_Connection $conn = null )
{
/* Invoke pre-save hooks. */
$this->invokeSaveHooks('pre', 'save');
/* Check to see if a duplicate object already exists. */
if( $existing = $this->getTable()->findDuplicate($this) )
{
/* Handle duplicate. In this case, we will return without saving. */
return;
}
parent::save($conn);
}
UserTable::findDuplicate() looks like this:
/** Locates an existing record that matches the specified user's email (but
* without matching its PK value, if applicable).
*
* #param $user User
*
* #return User|bool
*/
public function findDuplicate( User $user )
{
$q =
$this->createQuery('u')
->andWhere('u.email = ?', $user->email)
->limit(1);
if( $user->exists() )
{
$q->andWhere('u.id != ?', $user->id);
}
return $q->fetchOne();
}
Note that it is probably a better approach to overwrite preSave() rather than save() in your model. In my case, I had to wait until the pre-save hooks executed (the UID was set using a Doctrine template that I had created), so I had to overwrite save() instead.
You should use Memcached or Redis queue in order to check if item exists or not.

Automatically selecting dates from databases as Zend_Date objects

From what I understand, the best way to deal with dates in the Zend Framework is to select them as a Unix timestamp from the database.
Quick Creation of Dates from Database Date Values
// SELECT UNIX_TIMESTAMP(my_datetime_column) FROM my_table
$date = new Zend_Date($unixtimestamp, Zend_Date::TIMESTAMP);
I think it's a pain that there is actually no easy way in Oracle to either select dates as Unix timestamps or in ISO-8601 format - which are the two formats Zend_Date knows best.
But I did write a function to select dates as unix timestamps in PL/SQL, so I can actually do this now.
Using Zend_Db_Expr, I can now select my dates as Unix timestamps:
$select = $db->select()
->from(array('p' => 'products'),
array(
'product_id',
'product_date' => new Zend_Db_Expr('toUnixTimestamp(product_date)')
)
);
$results = $db->fetchAll($select);
You would use a similar query for any RDMS - most have a timestamp function.
I find this anoying because now I have to loop through $results to transform the timestamp to a Zend_Date object manually:
foreach($results as $result){
$productDate = new Zend_Date($result['product_date'], Zend_Date::TIMESTAMP);
echo $productDate->toString('dd/MMM/yyyy HH:mm:ss');
}
I want my Model to return $results where the timestamps are already transformed to Zend_Date. I don't want to have to write a loop in every data-access function to do this for me.
So to get to the point of my actual question:
Does anyone know of a way with Zend_Db, to set up some sort of post-processing on the result set, thus converting the timestamps to Zend_Date objects automatically?
I've encountered scenarios where I've wanted to do this. Here is the solution that I've used:
Created an extended row class for Zend_Db_Table_Row and overloaded the __get() and __set() super-methods
In the specific classes/tables that I want to use date objects, created the appropriate methods to do the heavy lifting
Here is a dumbed-down version of the extended row class that I use on my projects:
/**
* #category FireUp
* #package FireUp_Db
* #copyright Copyright (c) 2007-2009 Fire Up Media, Inc. (http://www.fireup.net)
* #license http://dev.fireup.net/license/mit MIT License
* #uses Zend_Db_Table_Row
*/
class FireUp_Db_Table_Row extends Zend_Db_Table_Row_Abstract
{
/**
* Retrieve row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The user-specified column name.
* #return string The corresponding column value.
* #throws Zend_Db_Table_Row_Exception if the $columnName is not a column in the row.
*/
public function __get($key)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_get' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}();
}
return parent::__get($key);
}
/**
* Set row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The column key.
* #param mixed $value The value for the property.
* #return void
* #throws Zend_Db_Table_Row_Exception
*/
public function __set($key, $value)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_set' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}($value);
}
return parent::__set($key, $value);
}
}
For our individual table classes, we override the functions as such:
class EntityRecord extends FireUp_Db_Table_Row
{
protected function _getDateCreated()
{
return new Zend_Date($this->_data['date_created'], Zend_Date::ISO_8601);
}
protected function _setDateCreated($value)
{
if ($value instanceof Zend_Date)
{
$value = $value->toString('YYYY-MM-dd HH:mm:ss');
}
$this->_data['date_created'] = $value;
$this->_modifiedFields['date_created'] = true;
}
}
Now, creating a new Zend_Date object everytime that the field would be accessed has some overhead, so in our classes, we take additional measures to cache the date objects, etc, but I didn't want that to get in the way of showing you the solution.
Use Zend_Table and have your table return you custom row objects that extend Zend_Db_Table_Row_Abstract. Then just have a method on that row like
function getDate() {
return new Zend_Date($this->datecol, Zend_Date::TIMESTAMP);
}

Resources