Laravel Mysql DB join 2 polymorphic parents - php

I am trying to create a query that can handle joining multiple polymorphic parents.
$this->query->join('time_records AS time', function($join) use($taskTable, $projectTable) {
$join->on('time.parent_id', '=', $taskTable . '.id', 'and')
->on('time.parent_type', '=', Task::class, 'and')
->on('time.parent_id', '=', $projectTable . '.id', 'or')
->on('time.parent_type', '=', Project::class, 'and');
});
This fails due to it not liking strings instead of column names. But I do feel this does not seem right anyway.
This is just one part of the overall report class. In this case there is 2 tables that the polymorphic table time_records needs to join to. but it needs to be 1 join basically. So all the time records that are joined are under the time name, so that the columns that are used all fall in line with the structure of the overall query.
I join many tables here, to create a kind of fake hierarchy.
categories.name as lev1, projects.name AS lev2, tasks.name AS lev3, time.time AS time
That is somewhat how I structure the select so a collection can then group the name and then groups the projects under it and down, ending up with a collection hierarchy that my application can read / understand.
All I am looking for is an end result where I can have a reference to the time_records table that contains data for the row based on if its the project or the task (so if its a project then lev3 would just be null/blank). I don't mind if its not a join, or if it is even just a raw SQL query. I just cannot wrap my head around the proper way to end up with the result I am looking for while keeping it to 1 query (since the collection is taxing enough).

I figured out a way and its more a change of mindset.
Rather then basing the query on the order you want, just adjust the select. So if you want data from a table, add select arguments for them, if not "hide" the table in the results, but always have it joined.
This part I did not show in my question and I do see now it was fairly important in isolating why this was a difficult one to figure out.
Reverse the order, rather then going from the top level down, go from the bottom up, so start with time_records as the main table for the query and join all the rest, that way both tables that relate to it can be joined simpler.
So in the end the answer was that I was totally looking at this the wrong way. it helped to write out the raw SQL output to find this out and manually query.

Related

Getting count fields of contained models using WHERE conditions

I have three models, Companies, events and assistances, where the assistances table stores the event_id and the company_id. I'd like to get a query in which the total assistances of the company to certain kind of events are stored. Nevertheless, as all these counts are linked to the same table, I don't really know how to build this query effectively. I have the ids of the assistances to each kind of event stored in some arrays, and then I do the following:
$query = $this->Companies->find('all')->where($conditions)->order(['name' => 'ASC']);
$query
->select(['total_assistances' => $query->func()->count('DISTINCT(Assistances.id)')])
->leftJoinWith('Assistances')
->group(['Companies.id'])
->autoFields(true);
Nevertheless, I don't know how to get the rest of the Assistance count, as I would need to count not all the distinct assistance Ids but only those taht fit to certain conditions, something like ->select(['assistances_conferences' => $query->func()->count('DISTINCT(Assistances.id)')])->where($conferencesConditions) (but obviously the previous line does not work. Is there any way of counting different kind of assistances in the query itself? (I need to do it this way because I then plan to use pagination and sort the table taking those fields into consideration).
The *JoinWith() methods accept a second argument, a callback that receives a query builder used for affecting the select list, as well as the conditions for the join.
->leftJoinWith('Assistances', function (\Cake\ORM\Query $query) {
return $query->where([
'Assistances.event_id IN' => [1, 2]
]);
})
This would generate a join statement like this, which would only include (and therefore count) the Assistances with an event_id of 1 or 2:
LEFT JOIN
assistances Assistances ON
Assistances.company_id = Companies.id AND
Assistances.event_id IN (1, 2)
The query builder passed to the callback really only supports selecting fields and adding conditions, more complex statements would need to be defined on the main query, or you'd possibly have to switch to using subqueries.
See also
Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data

Select rows using query builder in CakePHP based on the same end association but different relation to it

What I want to do is to get all rows related with user_id but in a different way.
First condition is to get all Books that are related with the User via Resources table where user_id is stored (in other words - Books owned by the User). Second condition is to get all Books that are related with the User through the Cities model again which is stored in the Resources table as well (Books that belong to Cities owned by the User).
I tried really a lot of things and I simply cannot make this two conditions work because I use JOIN (tried different combinations of innerJoinWith and leftJoinWith) on the same "end" model (User).
What I've done so far:
$userBooks = $this->Books->find()
->leftJoinWith("Resources.Users")
->leftJoinWith("Cities.Resources.Users")
->where(["Resources.Users" => 1])
->orWhere(["Cities.Resources.Users" => 1])
->all();
This of course does not work, but I hope you get the point about what I'm trying to achieve. The best what I was able to get with trying different approaches is the result of only one JOIN statement what is logical.
Basically, this can be separated into 2 parts which gives expected result (but I do not prefer it because I want it done with one query of course):
$userBooks = $this->Books->find()
->innerJoinWith("Resources.Users", function($q) {
return $q->where(["Users.id" => 1]);
})
->all();
$userBooks2 = $this->Books->find()
->innerJoinWith("Cities.Resources.Users", function($q) {
return $q->where(["Users.id" => 1]);
})
->all();
Also, before this I created an SQL script which works well and result is like expected:
SELECT books.id FROM books, cities, users_resources WHERE
(users_resources.resource_id = books.resource_id AND users_resources.user_id = 1)
OR
(users_resources.resource_id = cities.resource_id AND books.city_id = cities.id AND users_resources.user_id = 1)
This query works and I want to transfer it into ORM styled query in CakePHP to get both Books that are owned by the user and the ones that are connected with the User via Cities. I want somehow to separate these joins to individually filter data like I did in the SQL query.
EDIT
I've tried #ndm solution but the result is the same as where there is only 1 association (User) - I was still able to get data based on only one join statement (second one was ignored). Due to the fact I had to move on, I ended up with
$userBooks = $this->Books->find()
->innerJoinWith("Cities.Resources.Users"‌​)
->where(["Users.id" => $userId])
->union($this->Books->find()
->innerJoinWith("Resour‌​ces.Users")
->where([‌​"Users.id" => $userId])
)
->all();
which outputs correct result but not in very effective way (by union of 2 queries). I would really like to know the best way to approach this as this is a very common case (filtering by related model (user) that is associated with other models).
The ORM (specifically the eager loader) doesn't allow joining the same alias multiple times.
This can be worked around in various ways, the most simple one probaly being creating a separate association with a unique alias. For example in your ResourcesTable, create another association to Users with a different alias, say Users2, like:
$this->belongsToMany('Users2', [
'className' => 'Users'
]);
Then you can use that association in the second leftJoinWith(), and apply the conditions accordingly:
$this->Books
->find()
->leftJoinWith('Resources.Users')
->leftJoinWith('Cities.Resources.Users2')
->where(['Users.id' => 1])
->orWhere(['Users2.id' => 1])
->group('Books.id')
->all();
And don't forget to group your books to avoid duplicate results.
You could also create the joins manually using leftJoin() or join() instead, where you can define the aliases on your own (or don't use any at all) so that there are no conflicts, for more complex queries that can be a tedious task though.
You could also use your two separate queries as subqueries for conditions on Books, or even create a union query from them, which however might perform worse...
See also
Cookbook > Database Access & ORM > Query Builder > Adding Joins
CakePHP Issues > Improve association data fetching

Complex query join with doctrine

I'm building a query to show items with user and then show highest Bid on the item.
Example:
Xbox 360 by james. - the highest bid was $55.
art table by mario. - the highest bid was $25.
Query
SELECT i, u
FROM AppBundle:Item i
LEFT JOIN i.user u
I have another table bids (one to many relationship). I'm not sure how can I include single highest bid of the item in the same query with join.
I know I can just run another query after this query, with function (relationship), but I'm avoiding to do that for optimisation reasons.
Solution
SQL
https://stackoverflow.com/a/16538294/75799 - But how is this possible in doctrine DQL?
You can use IN with a sub query in such cases.
I am not sure if I understood your model correctly, but I attempted to make your query with a QueryBuilder and I am sure you will manage to make it work with this example:
$qb = $this->_em->createQueryBuilder();
$sub = $qb;
$sub->select('mbi') // max bid item
->where('i.id = mbi.id')
->leftJoin('mbi.bids', 'b'))
->andWhere($qb->expr()->max('b.value'))
->getQuery();
$qb = $qb->select('i', 'u')
->where($qb->expr()->in('i', $sub->getDQL()))
->leftJoin('i.user', 'u');
$query = $qb->getQuery();
return $query->getResult();
Your SQL query may look something like
select i,u
from i
inner join bids u on i.id = u.item_id
WHERE
i.value = (select max(value) from bids where item_id = i.id)
group by i
DQL, I don't think supports subqueries, so you could try using a Having clause or see if Doctrine\ORM\Query\Expr offers anything.
To solve this for my own case, I added a method to the origin entity (item) to find the max entity in a list of entities (bids), using Doctrine's Collections' Criteria I've written about it here.
Your Item entity would contain
public function getMaxBid()
{
$criteria = Criteria::create();
$criteria->orderBy(['bid.value' => Criteria::ASC]);
$criteria->setLimit(1);
return $this->bids->matching($criteria);
}
Unfortunately, there's no way that i know to find the maximum bid and the bidder with one grouping query, but there's several techniques to making the logic work with several queries. You could do a sub select and that might work fine depending on the size of the table. If you're planning on growing to the point where that's not going to work, you're probably already looking at sharing your relational databases, moving some data to a less transactional, higher performance db technology, or denormalizing, but if you want to keep this implemented in pure MySQL, you could use a procedure to express in multiple commands how to check for a bid and optionally add to the list, also updating the current high bidder in a denormalized high bids table. This keeps the complex logic of how to verify the bid in one, the most rigorously managed place - the database. Just make sure you use transactions properly to stop 2 bids from being recorded concurrently ( eg, SELECT FOR UPDATE).
I used to ask prospective programmers to write this query to see how experienced with MySQL they were, many thought just a max grouping was sufficient, and a few left the interview still convinced that it would work fine and i was wrong. So good question!

Yii - Query Manipulation for Custom CGridView with Advanced Search

So, I've extended CGridView to include an Advanced Search feature tailored to the needs of my organization.
Filter - lets you show/hide columns in the table, and you can also reorder columns by dragging the little drag icon to the left of each item.
Sort - Allows for the selection of multiple columns, specify Ascending or Descending.
Search - Select your column and insert search parameters. Operators tailored to data type of selected column.
Version 1 works, albeit slowly. Basically, I had my hands in the inner workings of CGridView, where I snatch the results from the DataProvider and do the searching and sorting in PHP before rendering the table contents.
Now writing Version 2, where I aim to focus on clever CDbCriteria creation, allowing MySQL to do the heavy lifting so it will run quicker. The implementation is trivial when dealing with a single database table. The difficulty arises when I'm dealing with 2 or more tables... For example, if the user intends to search on a field that is a STAT relation, I need that relation to be present in my query so that I may include comparisons.
Here's the question. How do I assure that Yii includes all with relations in my query so that I include comparisons? I've included all my relations with my criteria in the model's search function and I've tried CDbCriteria's together set to true ...
public function search() {
$criteria=new CDbCriteria;
$criteria->compare('id', $this->id);
$criteria->compare( ...
...
$criteria->with = array('relation0','relation1','relation3');
$criteria->together = true;
return new CActiveDataProvider(
get_class($this), array(
'criteria'=>$criteria,
'pagination' => array('pageSize' => 50)
));}
Then I'll snatch the criteria from the DataProvider and add a few conditions, for example, looking for dates > 1234567890. But I still get errors like this...
CDbCommand failed to execute the SQL statement:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 't.relation3' in 'where clause'.
The SQL statement executed was:
SELECT COUNT(DISTINCT `t`.`id`) FROM `table` `t`
LEFT OUTER JOIN `relation_table` `relation0` ON (`t`.`id`=`relation0`.`id`)
LEFT OUTER JOIN `relation_table` `relation1` ON (`t`.`id`=`relation1`.`id`)
WHERE (`t`.`relation3` > 1234567890)
Where relation0 and relation1 are BELONGS_TO relations, but any STAT relations, here depicted as relation3, are missing. Furthermore, why is the query a SELECT COUNT(DISTINCT 't'.'id') ?
Edit #DCoder Here's the specific relation I'm working with now. The main table is Call, which has a HAS_MANY relation to CallSegments, which keeps the times. So the startTime of the Call is the minimum start_time of all the related CallSegments. And startTime is the hypothetical relation3 in my anonymized query error.
'startTime' => array(self::STAT, 'CallSegments', 'call_id',
'select' => 'min(`start_time`)'),
Edit Other people have sent me to CDbCriteria's together property, but as you can see above, I am currently trying that to no avail.
Edit Looks like the issue has may have been reported: Yii and github tickets.
It is not a good idea to snatch the sql from a criteria and use it by yourself.
If you are using the "with" property then you could easily use comparisons like:
$criteria->compare("`relation1`.`id`", $yourVarHere);
Also Yii doesn't behave well with grouping.
My approach with STAT relations is using an subquery in the selects of Yii, followed by having:
$criteria->select = array("`t`.*", "(SELECT COUNT(*) FROM `relation3` WHERE `id` = `t`.id_relation3) AS `rel3`");
$criteria->having = "`rel3` > " . $yourValue;
The above method creates a bug in the gridview pagination because the count is done on a different query. A workaround will be to drop the "with" property and write the joins by yourself in the "join" property like:
$criteria->join = "LEFT OUTER JOIN `relation_table` `relation0` ON (`t`.`id`=`relation0`.`id`)
LEFT OUTER JOIN `relation_table` `relation1` ON (`t`.`id`=`relation1`.`id`)
LEFT OUTER JOIN `relation_table` `relation3` ON (`t`.`id`=`relation3`.`id`)";
If the bug is a little difficult to get working could you use the stat relation as a simple HAS_ONE with :
'select'=>'count(relation3.id)',
'joinType'=>'left join',
'group'=>'relation3.id',
'on'=>'t.id = relation3.id',
'together'=>true
to get the count value out along side everything else?
Not sure how well this would work for your case but it's been helpful for me from time to time.

MySQL and PHP: Searching for rows through a field in an undefined number of joined tables

There is an example table, 'main_table' with the fields 'ID', 'some_data'.
There is an aggregate table, 'agg' with the fields 'id', 'main_table_id', 'joinee_id'.
And then there is the final table, 'joinee', with the fields 'id', 'email'.
The tables 'main_table' and 'joinee' are in a many:many relationship, through 'agg'.
I would like to be able to search all the 'main_table' entries by 'email' from 'joinee', without doing a left join and then group by 'main_table'.'id'.
The final result needs to list all the 'main_table' entries, once per entry. Imagine it like this - I would like 'main_table' to get a temporary field "participants" which would contain all the 'emails' - I would then perform a LIKE match on this field in the same query that does this, in order to find the 'main_table' entries that have anything to do with the email I entered.
Is this possible?
Mind you, this is only a fragment of a much larger query. 'main_table' is already joined with 5 other tables, and their fields are already used as filters. Thing is, I know there can be only one joined table in each of those cases - with 'joinee', the number of connected entries varies.
Thank you.
Combining row results into single cells is something the relational model does not support, and most DBMSes are exceptionally bad at it. If you were to go down that road, you'd need to pull a considerable amount of hacking, like using user-defined functions or nonstandard syntax features to combine the values.
But if I understand you correctly, your problem is that you need to find entries in the main table that have desirable entries in their related email rows, and the problem is that there's a many-to-many relation.
How about this:
Write a query that finds the emails you're interested in and inner join that on the agg table. This will give you the related main_table_ids. Use distinct or group by to remove doubles.
Use the query from 1 as a subquery, and plug it into the query as you have it now, using something like WHERE main_table.id IN (/* subquery */), or, alternatively, inner join your existing query on the subquery from step 1. Which of these you use depends on the circumstances; traditionally, subqueries are slower than joins (all else being equal), but it may be the other way around, depending on your structure and actual data. On some older DMBSes, buffering the subquery result in a temporary table can prove beneficial.

Categories