Join with OneToMany relation - doctrine

I am trying to query an entity with a OneToMany relation but it doesn't work as the subquery DQL is not well converted.
I expect this code :
$subquery = $manager->createQueryBuilder();
$subquery
->select('s.occupant')
->from('MyBundle:Stay', 's')
->where('s.dateDeparture IS NULL')
;
$qb
->where($qb->expr()->notIn('o.id', ':subQuery'))
->setParameter('subQuery', $subquery->getDQL())
;
to produce :
WHERE o0_.id NOT IN (
SELECT s.occupant_id FROM Stay s WHERE s.date_departure IS NULL
)
But instead, I have this :
WHERE o0_.id NOT IN (
'SELECT s.occupant FROM MyBundle:Stay s WHERE s.dateDeparture IS NULL'
)
Here are the problems :
- The subquery is encapsulated between commas
- The SQL fields are not translated from their names in the entity (occupant, dateDeparture) to their MySQL equivalent (occupant_id, date_departure)
- The entity name is used (MyBundle:Stay) and is not converted to its SQL equivalent (Stay)
My other queries work perfectly, as well as the main query encapsulating this one.
I also tried to use the OneToMany relation to do this, as there is an Occupant.stays relation, but I couldn't make it work either.
Here is my Occupant class :
class Occupant
{
...
/**
* #ORM\OneToMany(targetEntity="EmmausBundle\Entity\Stay", mappedBy="occupant", cascade={"persist"})
* #ORM\OrderBy({"dateArrival" = "DESC"})
*/
private $stays;
...
}
And my Stay class :
class Stay
{
...
/**
* #ORM\ManyToOne(targetEntity="Occupant", inversedBy="stays")
* #ORM\JoinColumn(name="occupant_id", referencedColumnName="id")
*/
private $occupant;
/**
* #var \DateTime
*
* #ORM\Column(name="date_departure", type="datetime", nullable=true)
*/
private $dateDeparture;
...
}
Thanks for you help !

Thanks to this answer I found the solution :
$qb
->where(
$qb->expr()->notIn(
'o.id',
$manager->createQueryBuilder()
->select('IDENTITY (s.occupant)')
->from('EmmausBundle:Stay', 's')
->where('s.dateDeparture IS NULL')
->getDQL()
)
)
;

Related

hasManyThrough query without laravel_through_key

With a Laravel hasManyThrough relationship how can I prevent extra keys added to the select? Is there a withoutPivot or simiar?
I'm trying to perform a union join and need to the columns to be the same.
SQLSTATE[21000]: Cardinality violation: 1222 The used SELECT statements have a different number of columns
SELECT
count(*) AS aggregate
FROM ((
SELECT
`order_payments`.`id`,
`order_payments`.*,
`orders`.`user_id` AS `laravel_through_key`
FROM
`order_payments`
INNER JOIN `orders` ON `orders`.`id` = `order_payments`.`order_id`
WHERE
`orders`.`user_id` = 1)
UNION (
SELECT
`orders`.`id`
FROM
`orders`
WHERE
`orders`.`user_id` = 1
AND `orders`.`user_id` IS NOT NULL)) AS `temp_table`
I'm unable to use the makeHidden as suggested in another question
$payments = auth()->user()->orderPayments()->select('order_payments.id');
$orders = auth()->user()->orders()->select('orders.id');
$payments->union($orders)->paginate(50);
I guess I could do it without Eloquent manually but just wondering if there was another way?
class User extends Authenticatable implements UserContract {
/**
* Orders for this User
*/
public function orders()
{
return $this->hasMany(Order::class);
}
/**
* Order Payments for this User
*/
public function orderPayments()
{
return $this->hasManyThrough(OrderPayment::class, Order::class);
}
}

How to Write sub Query in laravel

Here is my Query
SELECT
SUM(A.total_price) AS total_sum
FROM
(
SELECT
*, (qty * cost) AS total_price
FROM
services_p_o_items_management
WHERE
services_pos_id = '.$id.'
) AS A
How to write in laravel anyone here to help me
If you want to run the query in a loop:
DB::table('services_p_o_items_management')
->selectRaw("SUM(qty * cost) as total_sum")
->where('services_pos_id', $id)
->first();
If you want to execute a single query for all the ids:
DB::table('services_p_o_items_management')
->selectRaw("services_pos_id, SUM(qty * cost) as total_sum")
->whereIn('services_pos_id', $ids)
->groupBy("services_pos_id")
->get();
You can use raw query in from() method
DB::query()->from('SELECT *, (qty * cost) AS total_price FROM services_p_o_items_management WHERE services_pos_id = '.$id, 'A')
->get();
Beside that, first argument of from() also accept a Builder object
/**
* Set the table which the query is targeting.
*
* #param \Closure|\Illuminate\Database\Query\Builder|string $table
* #param string|null $as
* #return $this
*/
public function from($table, $as = null)
Thus, you can change your raw query into a Builder object
$query = DB::table('services_p_o_items_management')
->select(['*', DB::raw('(qty * cost) AS total_price')])
->where('services_pos_id', $id);
DB::query()->from($query, 'A')->get();

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

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;
}

Why delete method of Eloquent (Laravel) fires a select?

I have got this:
$obj = DeliveryNote::find($row->note_id);
$obj->products()->delete();
$obj->delete();
$obj = Order::find($row->order_id);
$obj->delete();
The log file of sql queries is this:
- select * from `delivery_notes` where `delivery_notes`.`id` = ? limit 1
- delete from `delivery_note_products` where `delivery_note_products`.`delivery_note_id` = ? and `delivery_note_products`.`delivery_note_id` is not null
- delete from `delivery_notes` where `id` = ?
- select * from `orders` where `orders`.`id` = ? limit 1
- delete from `order_products` where `order_products`.`order_id` = ? and `order_products`.`order_id` is not null
¿¿?? ==> select * from `delivery_notes` where `delivery_notes`.`id` = ? limit 1
I dont understand that, the Eloquent models havent got any strange.
class DeliveryNote extends Model
{
protected $table = 'delivery_notes';
/**
* #var array
*/
protected $fillable = ['...'];
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function order()
{
return $this->belongsTo('App\Models\Order');
}
...
I thought to comment "order()" method but it does nothing.
Any ideas???
You are using find which queries the database and returns an Eloquent model you then use delete on. If you use this way you can utilize Eloquent events.
If you want to delete an entry in your database without getting an Eloquent response you need to skip get and find and just use delete instead.
Model::where('id', $id)->delete();

Doctrine select all users that manage other users via column

Maybe I am missing something completely, but I cannot get it working. I only want to select User objects that are linked to User objects.
User:
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// other fields ...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
*/
private $firstManager;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
*/
private $secondManager;
}
I want to select those who are firstManager or secondManager for a User. Sounds simple eh?
I thought, this would do:
public function findAllManagers()
{
$qb = $this->createQueryBuilder('user')
->join('user.firstManager', 'first_manager')
->join('user.secondManager', 'second_manager')
->orWhere('first_manager = user')
->orWhere('second_manager = user');
$qb = $qb->getQuery();
return $qb->getResult();
}
But only got one result, not all three I needed. I think this is valid SQL?
For what i see your sql query should look like:
SELECT user.* FROM user_table user
INNER JOIN user_table first_manager ON first_manager.id = user.first_manager_id
INNER JOIN user_table second_manager ON second_manager.id = user.second_manager_id
WHERE first_manager.id = user.id
OR second_manager.id = user.id
The result of this would be users who are their own first or second manager.
I think what you are looking for is this:
public function findAllManagers()
{
$qb = $this->createQueryBuilder('m')
->leftJoin('u1', 'AppBundle\Entity\User', 'WITH', m = u1.firstManager)
->leftJoin('u2', 'AppBundle\Entity\User', 'WITH', m = u2.secondManager)
->where('u1.firstManager IS NOT NULL')
->orWhere('u2.secondManager IS NOT NULL')
->getQuery()
;
return $qb->getResult();
}
The equivalent SQL query should be:
SELECT m.* FROM user_table m
LEFT JOIN user_table u1 ON u1.first_manager_id = m.id
LEFT JOIN user_table u2 ON u2.second_manager_id = m.id
WHERE u1.first_manager_id IS NOT NULL
OR u2.second_manager_id IS NOT NULL
->join() results in an INNER JOIN. As you can read here, this will only give results, that are "in the center". If you have multiple JOINs, this will only give results of all those joined tables. In your case, this means users that are firstManager and secondManager.
If you change to ->leftJoin(), this will give you all users (with additional info), so it is a good place to start. Then you can filter those out that are neither firstManager or secondManager.
Something like this should work (untested)
$result = $this->createQueryBuilder('user')
->leftJoin('user.firstManager', 'fm')
->leftJoin('user.secondManager', 'sm')
->where('fm.id IS NOT NULL')
->orWhere('sm.id IS NOT NULL')
->getQuery()
->getResult()
;

Resources