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.
Related
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
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("Resources.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
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.
I need to fill query builder with filter conditions of page attributes. I got Enities "Page", "Attribute" and "Value"
Page: id, name, etc
Attribute: id, name, etc
Value: page_id, date, string, numeric
I need to get all pages having attribute values selected in form.
So i get Query Builder:
$qb->select('p')
->from(Page p)
->leftJoin(p.values);
Can I use "having" or "where" clauses to do like?
$qb->add('having', 'a.id = :attr1_id AND a.value = :attr1_val')
$qb->add('having', 'a.id = :attr2_id AND a.value = :attr2_val')
UPDATE: No i can't. in this way it can get page with attr1_id = attr2_value AND attr2_id = attr1_value and all conditions will be true, but result is wrong.
or I should add custom join for each value?
UPDATE: here's working exapmle:
//attr1.intval=:a1 (or LIKE, BETWEEN, etc compare)
$comp_expression=$aliace.'.'.$value_field.'=:a'.$this->getId();
//INNER JOIN attribute attr1 ON attr1.type=5 AND attr1.intval=:a1
$qb->innerJoin($value_class, $aliace , 'WITH', $aliace.'.type='.$this->getId().' AND '.$comp_expression);
//where attr1.intval IS NOT NULL
$qb->andWhere($aliace.'.'.$value_field.' IS NOT NULL');
$qb->setParameter('a'.$this->getId(),$value);
UPDATE: So the only way to do key+value filtring is to add join for each filter condition? I got 27 attributes now, so need to build query with 27 joins?
Is there another way to this better? Sorry if this is duplicate, cant find keywords to find same questions.
UPDATE: Maybe i shuld get out of sql query limits and create mysql procedure for it?
UPDATE: Can i use somthing like this in doctrine? MySQL optimization on filtering key-value pairs as records
Model
The model you are using here is called Entity-Attribute-Value (EAV). If you weren't aware of that I suggest you read about the pro's and con's first.
One of the con's is that searching in a EAV model is difficult and very inefficient.
AND queries
It seems you want to query for Entities that have a specific Value for a specific Attribute and another specific Value for a specific Attribute. These types of queries are not very efficient in the EAV model.
Let's assume your entities are set up as:
Entity: id, attributes (one-to-many), values (one-to-many)
Attribute: id, name, entity (many-to-one), values (one-to-many)
Value: id, content, entity (many-to-one), attribute (many-to-one)
Let's assume you want to query for all Entities that have an Attribute color with the value blue. A Doctrine query would look like this:
SELECT e FROM Entity e
JOIN e.values v JOIN v.attribute a
WHERE a.name = 'color' AND v.content = 'blue'
(Of course you should never use values in the DQL like this, but bind them as parameters.)
Now let's assume you want to query for all Entities that have an Attribute color with the value blue and an Attribute shape with the value square. The query becomes:
SELECT e FROM Entity e
JOIN e.values v1 JOIN v.attribute a1
JOIN e.values v2 JOIN v.attribute a2
WHERE a1.name = 'color' AND v1.content = 'blue'
AND a2.name = 'shape' AND v2.content = 'square'
Now if you want to query for 3 Attribute/Value pairs, you'll need 3 of those JOIN sets. If you want to query for 4 Attribute/Value pairs, you'll need 4 of those JOIN sets. Etc.
OR queries
Let's assume you want to query for all Entities that have an Attribute color with the value blue or an Attribute shape with the value square. The query becomes:
SELECT e FROM Entity e
JOIN e.values v JOIN v.attribute a
WHERE (a.name = 'color' AND v.content = 'blue')
OR (a.name = 'shape' AND v.content = 'square')
You can see this is more efficient, you won't need those additional JOIN sets for every additional Attribute/Value pair.
Mixing AND OR
I'm going to be short here: this will become a query nightmare. I strongly advise you not to do this.
Alternative
If your application is going to rely heavily on these types of queries (especially AND and mixing AND OR), I suggest you consider a different type of storage engine. Relational Databases are not really suited for this kind of thing.
You'll probably be better of using a Document Oriented Database, something like Elasticsearch, MongoDB, CouchDB, etc.
Hybrid alternative
You can also "copy" the EAV part to a Document Oriented Database and use it only for search functionality. I suggest you set up events for when changes are made to an Entity (create, update, delete). Then create listeners that persist those changes into the Document store.
This way your application can work with the Relational Database for normal operations, and the Document store for searching. In this case I advise Elasticsearch, which is extremely suitable for this kind of thing.
There is no such difference between the two methods, the two methods do exactly the same thing.
By the central point of question, I got the doubt is about the construction of query using like.
The first example you provide is workfull.
The second example you propose is not right.
On the second example, you need to define All the where clauses on andWhere method.
$qb->innerJoin('a1 ON p.id=a1.page_id');
$qb->andWhere('a1.attribute_id=:attr1_id ');
$qb->andWhere('a1.value=:attr1_val');
$qb->andWhere('a1 is not null');
All are you doing is creating a sql by a third api, Doctrine ORM is one of the most successful orm tool for php, knowing how to use it will let you to experience the ORM power, by doctrine implementation all you need to know are there:
Doctrine Create Query Builder -> The same are you using.
Doctrine Query Builder
Doctrine Create Query -> Simplest form
Query Language
My program is using the WanWizard DataMapper ORM with CodeIgniter, and needs to perform a simple query based on a field in a join table. Here is my code:
$d = new Deal();
$deals = $d
->where_join_field('networks', 'status', 'active')
->get();
Fairly straightforward, as you can see. A many-to-many relationship between "deals" and "networks", with a "status" field in the join table. However, this is the query it generates:
SELECT * FROM (`deal`) WHERE `deal_network`.`status` = 'active'
Obviously, this doesn't work at all, because there in no JOIN in place to introduce the deal_network table. I tried using include_related, but it aliases the deal_network table as networks_deal_network, resulting in an incorrect table reference. How can I get DataMapper to JOIN that table into the query properly?
It appears that this is table name bug in _join_field in libraries/datamapper.php. I forked and submitted a pull-request with the fix.
https://bitbucket.org/jonahbron/datamapper